mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-23 17:15:38 +01:00
Added initial database support for cross-session memory
This commit is contained in:
parent
3c1e72342b
commit
b2ca8515cc
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@ dist/
|
||||
.byebug_history
|
||||
testbed
|
||||
.idea/
|
||||
data/*.sqlite
|
||||
|
@ -7,6 +7,7 @@ set privkey "data/pwncat"
|
||||
# Set the pwncat backdoor user and password
|
||||
set backdoor_user "pwncat"
|
||||
set backdoor_pass "pwncat"
|
||||
set db "sqlite:///data/pwncat.sqlite"
|
||||
|
||||
set on_load {
|
||||
# Run a command upon a stable connection
|
||||
|
@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
from typing import Optional
|
||||
|
||||
from pwncat import db
|
||||
|
||||
victim: Optional["pwncat.remote.Victim"] = None
|
||||
|
@ -5,6 +5,8 @@ import selectors
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from sqlalchemy.exc import InvalidRequestError
|
||||
|
||||
import pwncat
|
||||
from pwncat.remote import Victim
|
||||
from pwncat import util
|
||||
@ -125,6 +127,11 @@ def main():
|
||||
finally:
|
||||
# Restore the shell
|
||||
pwncat.victim.restore_local_term()
|
||||
try:
|
||||
# Make sure everything was committed
|
||||
pwncat.victim.session.commit()
|
||||
except InvalidRequestError:
|
||||
pass
|
||||
util.success("local terminal restored")
|
||||
|
||||
|
||||
|
@ -19,7 +19,7 @@ from prompt_toolkit.lexers import PygmentsLexer
|
||||
from prompt_toolkit.document import Document
|
||||
from pygments.styles import get_style_by_name
|
||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
||||
from prompt_toolkit.history import InMemoryHistory
|
||||
from prompt_toolkit.history import InMemoryHistory, History
|
||||
from typing import Dict, Any, List, Iterable
|
||||
from colorama import Fore
|
||||
from enum import Enum, auto
|
||||
@ -32,6 +32,7 @@ import re
|
||||
from pprint import pprint
|
||||
|
||||
import pwncat
|
||||
import pwncat.db
|
||||
from pwncat.commands.base import CommandDefinition, Complete
|
||||
from pwncat.util import State
|
||||
from pwncat import util
|
||||
@ -94,6 +95,24 @@ def resolve_blocks(source: str):
|
||||
return "".join(result).split("\n")
|
||||
|
||||
|
||||
class DatabaseHistory(History):
|
||||
""" Yield history from the host entry in the database """
|
||||
|
||||
def load_history_strings(self) -> Iterable[str]:
|
||||
""" Load the history from the database """
|
||||
for history in (
|
||||
pwncat.victim.session.query(pwncat.db.History)
|
||||
.order_by(pwncat.db.History.id.desc())
|
||||
.all()
|
||||
):
|
||||
yield history.command
|
||||
|
||||
def store_string(self, string: str) -> None:
|
||||
""" Store a command in the database """
|
||||
history = pwncat.db.History(host_id=pwncat.victim.host.id, command=string)
|
||||
pwncat.victim.session.add(history)
|
||||
|
||||
|
||||
class CommandParser:
|
||||
""" Handles dynamically loading command classes, parsing input, and
|
||||
dispatching commands. """
|
||||
@ -110,7 +129,17 @@ class CommandParser:
|
||||
loader.find_module(module_name).load_module(module_name).Command()
|
||||
)
|
||||
|
||||
history = InMemoryHistory()
|
||||
self.prompt: PromptSession = None
|
||||
self.toolbar: PromptSession = None
|
||||
self.loading_complete = False
|
||||
self.aliases: Dict[str, CommandDefinition] = {}
|
||||
self.shortcuts: Dict[str, CommandDefinition] = {}
|
||||
|
||||
def setup_prompt(self):
|
||||
""" This needs to happen after __init__ when the database is fully
|
||||
initialized. """
|
||||
|
||||
history = DatabaseHistory()
|
||||
completer = CommandCompleter(self.commands)
|
||||
lexer = PygmentsLexer(CommandLexer.build(self.commands))
|
||||
style = style_from_pygments_cls(get_style_by_name("monokai"))
|
||||
@ -144,10 +173,6 @@ class CommandParser:
|
||||
history=history,
|
||||
)
|
||||
|
||||
self.loading_complete = False
|
||||
self.aliases: Dict[str, CommandDefinition] = {}
|
||||
self.shortcuts: Dict[str, CommandDefinition] = {}
|
||||
|
||||
@property
|
||||
def loaded(self):
|
||||
return self.loading_complete
|
||||
|
@ -20,10 +20,10 @@ class Command(CommandDefinition):
|
||||
def get_user_choices(self):
|
||||
""" Get the user options """
|
||||
current = pwncat.victim.current_user
|
||||
if current["name"] == "root" or current["uid"] == 0:
|
||||
if current.id == 0:
|
||||
return [name for name in pwncat.victim.users]
|
||||
else:
|
||||
return [current["name"]]
|
||||
return [current.name]
|
||||
|
||||
PROG = "persist"
|
||||
ARGS = {
|
||||
@ -92,7 +92,7 @@ class Command(CommandDefinition):
|
||||
if method.system and method.installed():
|
||||
yield (method.name, None, method)
|
||||
elif not method.system:
|
||||
if me["uid"] == 0:
|
||||
if me.id == 0:
|
||||
for user in pwncat.victim.users:
|
||||
util.progress(f"checking {method.name} for: {user}")
|
||||
if method.installed(user):
|
||||
@ -100,14 +100,14 @@ class Command(CommandDefinition):
|
||||
yield (method.name, user, method)
|
||||
util.erase_progress()
|
||||
else:
|
||||
if method.installed(me["name"]):
|
||||
yield (method.name, me["name"], method)
|
||||
if method.installed(me.name):
|
||||
yield (method.name, me.name, method)
|
||||
|
||||
def run(self, args):
|
||||
|
||||
if args.action == "status":
|
||||
ninstalled = 0
|
||||
for name, user, method in self.installed_methods:
|
||||
for user, method in pwncat.victim.persist.installed:
|
||||
print(f" - {method.format(user)} installed")
|
||||
ninstalled += 1
|
||||
if not ninstalled:
|
||||
@ -122,12 +122,12 @@ class Command(CommandDefinition):
|
||||
return
|
||||
elif args.action == "clean":
|
||||
util.progress("cleaning persistence methods: ")
|
||||
for name, user, method in self.installed_methods:
|
||||
for user, method in pwncat.victim.persist.installed:
|
||||
try:
|
||||
util.progress(
|
||||
f"cleaning persistance methods: {method.format(user)}"
|
||||
)
|
||||
method.remove(user)
|
||||
pwncat.victim.persist.remove(method.name, user)
|
||||
util.success(f"removed {method.format(user)}")
|
||||
except PersistenceError as exc:
|
||||
util.erase_progress()
|
||||
@ -140,13 +140,6 @@ class Command(CommandDefinition):
|
||||
self.parser.error("no method specified")
|
||||
return
|
||||
|
||||
# Lookup the method
|
||||
try:
|
||||
method = pwncat.victim.persist.find(args.method)
|
||||
except KeyError:
|
||||
self.parser.error(f"{args.method}: no such persistence method")
|
||||
return
|
||||
|
||||
# Grab the user we want to install the persistence as
|
||||
if args.user:
|
||||
user = args.user
|
||||
@ -154,31 +147,10 @@ class Command(CommandDefinition):
|
||||
# Default is to install as current user
|
||||
user = pwncat.victim.whoami()
|
||||
|
||||
if args.action == "install":
|
||||
try:
|
||||
|
||||
# Check that the module isn't already installed
|
||||
if method.installed(user):
|
||||
util.error(f"{method.format(user)} already installed")
|
||||
return
|
||||
|
||||
util.success(f"installing {method.format(user)}")
|
||||
|
||||
# Install the persistence
|
||||
method.install(user)
|
||||
except PersistenceError as exc:
|
||||
util.error(f"{method.format(user)}: install failed: {exc}")
|
||||
elif args.action == "remove":
|
||||
try:
|
||||
|
||||
# Check that the module isn't already installed
|
||||
if not method.installed(user):
|
||||
util.error(f"{method.format(user)} not installed")
|
||||
return
|
||||
|
||||
util.success(f"removing {method.format(user)}")
|
||||
|
||||
# Remove the method
|
||||
method.remove(user)
|
||||
except PersistenceError as exc:
|
||||
util.error(f"{method.format(user)}: removal failed: {exc}")
|
||||
try:
|
||||
if args.action == "install":
|
||||
pwncat.victim.persist.install(args.method, args.user)
|
||||
elif args.action == "remove":
|
||||
pwncat.victim.persist.remove(args.method, args.user)
|
||||
except PersistenceError as exc:
|
||||
util.error(f"{exc}")
|
||||
|
@ -22,6 +22,11 @@ class Command(CommandDefinition):
|
||||
type=int,
|
||||
help="Tamper ID to revert (IDs found in tamper list)",
|
||||
),
|
||||
"--all,-a": parameter(
|
||||
Complete.NONE,
|
||||
action="store_true",
|
||||
help="Attempt to revert all tampered files",
|
||||
),
|
||||
"--revert,-r": parameter(
|
||||
Complete.NONE,
|
||||
action=StoreConstOnce,
|
||||
@ -43,14 +48,30 @@ class Command(CommandDefinition):
|
||||
def run(self, args):
|
||||
|
||||
if args.action == "revert":
|
||||
if args.tamper not in range(len(pwncat.victim.tamper.tampers)):
|
||||
self.parser.error("invalid tamper id")
|
||||
tamper = pwncat.victim.tamper.tampers[args.tamper]
|
||||
try:
|
||||
tamper.revert()
|
||||
pwncat.victim.tamper.tampers.pop(args.tamper)
|
||||
except RevertFailed as exc:
|
||||
util.error(f"revert failed: {exc}")
|
||||
if args.all:
|
||||
removed_tampers = []
|
||||
util.progress(f"reverting tamper")
|
||||
for tamper in pwncat.victim.tamper:
|
||||
try:
|
||||
util.progress(f"reverting tamper: {tamper}")
|
||||
tamper.revert()
|
||||
removed_tampers.append(tamper)
|
||||
except RevertFailed as exc:
|
||||
util.warn(f"{tamper}: revert failed: {exc}")
|
||||
for tamper in removed_tampers:
|
||||
pwncat.victim.tamper.remove(tamper)
|
||||
util.success("tampers reverted!")
|
||||
pwncat.victim.session.commit()
|
||||
else:
|
||||
if args.tamper not in range(len(pwncat.victim.tamper)):
|
||||
self.parser.error("invalid tamper id")
|
||||
tamper = pwncat.victim.tamper[args.tamper]
|
||||
try:
|
||||
tamper.revert()
|
||||
pwncat.victim.tamper.remove(tamper)
|
||||
except RevertFailed as exc:
|
||||
util.error(f"revert failed: {exc}")
|
||||
pwncat.victim.session.commit()
|
||||
else:
|
||||
for id, tamper in enumerate(pwncat.victim.tamper.tampers):
|
||||
for id, tamper in enumerate(pwncat.victim.tamper):
|
||||
print(f" {id} - {tamper}")
|
||||
|
@ -62,6 +62,7 @@ class Config:
|
||||
"backdoor_user": {"value": "pwncat", "type": str},
|
||||
"backdoor_pass": {"value": "pwncat", "type": str},
|
||||
"on_load": {"value": "", "type": str},
|
||||
"db": {"value": "sqlite:///:memory:", "type": str},
|
||||
}
|
||||
|
||||
# Map ascii escape sequences or printable bytes to lists of commands to
|
||||
|
10
pwncat/db/__init__.py
Normal file
10
pwncat/db/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from pwncat.db.base import Base
|
||||
from pwncat.db.binary import Binary
|
||||
from pwncat.db.history import History
|
||||
from pwncat.db.host import Host
|
||||
from pwncat.db.persist import Persistence
|
||||
from pwncat.db.suid import SUID
|
||||
from pwncat.db.tamper import Tamper
|
||||
from pwncat.db.user import User, Group, SecondaryGroupAssociation
|
5
pwncat/db/base.py
Normal file
5
pwncat/db/base.py
Normal file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base = declarative_base()
|
16
pwncat/db/binary.py
Normal file
16
pwncat/db/binary.py
Normal file
@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from pwncat.db.base import Base
|
||||
|
||||
|
||||
class Binary(Base):
|
||||
|
||||
__tablename__ = "binary"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
host_id = Column(Integer, ForeignKey("host.id"))
|
||||
host = relationship("Host", back_populates="binaries")
|
||||
# The path to the binary on the remote host
|
||||
path = Column(String)
|
15
pwncat/db/history.py
Normal file
15
pwncat/db/history.py
Normal file
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from pwncat.db.base import Base
|
||||
|
||||
|
||||
class History(Base):
|
||||
|
||||
__tablename__ = "history"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
host_id = Column(Integer, ForeignKey("host.id"))
|
||||
host = relationship("Host", back_populates="history")
|
||||
command = Column(String)
|
36
pwncat/db/host.py
Normal file
36
pwncat/db/host.py
Normal file
@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from pwncat.db.base import Base
|
||||
|
||||
|
||||
class Host(Base):
|
||||
|
||||
__tablename__ = "host"
|
||||
|
||||
# Database identifier
|
||||
id = Column(Integer, primary_key=True)
|
||||
# A unique hash identifying this host
|
||||
hash = Column(String)
|
||||
# The remote architecture (uname -m)
|
||||
arch = Column(String)
|
||||
# The remote kernel version (uname -r)
|
||||
kernel = Column(String)
|
||||
# The remote distro (probed from /etc/*release), or "unknown"
|
||||
distro = Column(String)
|
||||
# A list of groups this host has
|
||||
groups = relationship("Group")
|
||||
# A list of users this host has
|
||||
users = relationship("User")
|
||||
# A list of persistence methods applied to this host
|
||||
persistence = relationship("Persistence")
|
||||
# A list of tampers applied to this host
|
||||
tampers = relationship("Tamper")
|
||||
# A list of resolved binaries for the remote host
|
||||
binaries = relationship("Binary")
|
||||
# Command history for local prompt
|
||||
history = relationship("History")
|
||||
# A list of SUID binaries found across all users (may have overlap, and may not be
|
||||
# accessible by the current user).
|
||||
suid = relationship("SUID")
|
18
pwncat/db/persist.py
Normal file
18
pwncat/db/persist.py
Normal file
@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from pwncat.db.base import Base
|
||||
|
||||
|
||||
class Persistence(Base):
|
||||
|
||||
__tablename__ = "persistence"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
host_id = Column(Integer, ForeignKey("host.id"))
|
||||
host = relationship("Host", back_populates="persistence")
|
||||
# The type of persistence
|
||||
method = Column(String)
|
||||
# The user this persistence was applied as (ignored for system persistence)
|
||||
user = Column(String)
|
20
pwncat/db/suid.py
Normal file
20
pwncat/db/suid.py
Normal file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
from sqlalchemy import ForeignKey, Integer, Column, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from pwncat.db.base import Base
|
||||
|
||||
|
||||
class SUID(Base):
|
||||
|
||||
__tablename__ = "suid"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
host_id = Column(Integer, ForeignKey("host.id"))
|
||||
host = relationship("Host", back_populates="suid", foreign_keys=[host_id])
|
||||
user_id = Column(Integer, ForeignKey("users.id"))
|
||||
user = relationship("User", backref="suid", foreign_keys=[user_id])
|
||||
# Path to this SUID binary
|
||||
path = Column(String)
|
||||
owner_id = Column(Integer, ForeignKey("users.id"))
|
||||
owner = relationship("User", foreign_keys=[owner_id], backref="owned_suid")
|
16
pwncat/db/tamper.py
Normal file
16
pwncat/db/tamper.py
Normal file
@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python3
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, LargeBinary
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from pwncat.db.base import Base
|
||||
|
||||
|
||||
class Tamper(Base):
|
||||
|
||||
__tablename__ = "tamper"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
host_id = Column(Integer, ForeignKey("host.id"))
|
||||
host = relationship("Host", back_populates="tampers")
|
||||
name = Column(String)
|
||||
data = Column(LargeBinary)
|
58
pwncat/db/user.py
Normal file
58
pwncat/db/user.py
Normal file
@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Table
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from pwncat.db.base import Base
|
||||
|
||||
SecondaryGroupAssociation = Table(
|
||||
"secondary_group_association",
|
||||
Base.metadata,
|
||||
Column("group_id", Integer, ForeignKey("groups.id")),
|
||||
Column("user_id", ForeignKey("users.id")),
|
||||
)
|
||||
|
||||
|
||||
class Group(Base):
|
||||
|
||||
__tablename__ = "groups"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
host_id = Column(Integer, ForeignKey("host.id"))
|
||||
host = relationship("Host", back_populates="groups")
|
||||
name = Column(String)
|
||||
members = relationship(
|
||||
"User", back_populates="groups", secondary=SecondaryGroupAssociation
|
||||
)
|
||||
|
||||
|
||||
class User(Base):
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
# The users UID
|
||||
id = Column(Integer)
|
||||
host_id = Column(Integer, ForeignKey("host.id"))
|
||||
host = relationship("Host", back_populates="users")
|
||||
# The users GID
|
||||
gid = Column(Integer, ForeignKey("groups.id"))
|
||||
# The actual DB Group object representing that group
|
||||
group = relationship("Group")
|
||||
# The name of the user
|
||||
name = Column(String, primary_key=True)
|
||||
# The user's full name
|
||||
fullname = Column(String)
|
||||
# The user's home directory
|
||||
homedir = Column(String)
|
||||
# The user's password, if known
|
||||
password = Column(String)
|
||||
# The hash of the user's password, if known
|
||||
hash = Column(String)
|
||||
# The user's default shell
|
||||
shell = Column(String)
|
||||
# The user's secondary groups
|
||||
groups = relationship(
|
||||
"Group", back_populates="members", secondary=SecondaryGroupAssociation
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"""User(uid={self.id}, gid={self.gid}, name={repr(self.name)})"""
|
@ -1,13 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
import functools
|
||||
import pkgutil
|
||||
from typing import Optional, Dict, Iterator
|
||||
from colorama import Fore
|
||||
|
||||
import pwncat
|
||||
|
||||
|
||||
class PersistenceError(Exception):
|
||||
""" Indicates a problem in adding/removing a persistence method """
|
||||
|
||||
|
||||
def persistence_tamper_removal(name: str, user: Optional[str] = None):
|
||||
pwncat.victim.persist.remove(name, user, from_tamper=True)
|
||||
|
||||
|
||||
class Persistence:
|
||||
def __init__(self):
|
||||
|
||||
@ -20,10 +27,40 @@ class Persistence:
|
||||
def install(self, name: str, user: Optional[str] = None):
|
||||
""" Add persistence as the specified user. If the specified persistence
|
||||
method is system method, the "user" argument is ignored. """
|
||||
method = self.find(name)
|
||||
try:
|
||||
method = next(self.find(name))
|
||||
except StopIteration:
|
||||
raise PersistenceError(f"{name}: no such persistence method")
|
||||
if not method.system and user is None:
|
||||
raise PersistenceError("non-system methods require a user argument")
|
||||
raise PersistenceError(
|
||||
f"{method.format(user)}: non-system methods require a user argument"
|
||||
)
|
||||
if method.installed(user):
|
||||
raise PersistenceError(f"{method.format(user)}: already installed")
|
||||
method.install(user)
|
||||
self.register(name, user)
|
||||
|
||||
def register(self, name: str, user: Optional[str] = None):
|
||||
""" Register a persistence method as pre-installed. This is useful for some privilege escalation
|
||||
which automatically adds things equivalent to persistent, but without the
|
||||
persistence module itself (e.g. backdooring /etc/passwd or SSH keys). """
|
||||
|
||||
method = next(self.find(name))
|
||||
|
||||
persist = pwncat.db.Persistence(method=name, user=user)
|
||||
pwncat.victim.host.persistence.append(persist)
|
||||
|
||||
# Also register a tamper to track in both places
|
||||
pwncat.victim.tamper.custom(
|
||||
f"Persistence: {method.format(user)}",
|
||||
functools.partial(persistence_tamper_removal, name=name, user=user),
|
||||
)
|
||||
|
||||
@property
|
||||
def installed(self) -> Iterator["PersistenceMethod"]:
|
||||
""" Retrieve a list of installed persistence methods """
|
||||
for persist in pwncat.victim.host.persistence:
|
||||
yield persist.user, self.methods[persist.method]
|
||||
|
||||
def find(
|
||||
self,
|
||||
@ -52,17 +89,39 @@ class Persistence:
|
||||
# All checks passed. Yield the method.
|
||||
yield method
|
||||
|
||||
def remove(self, name: str, user: Optional[str] = None):
|
||||
def remove(self, name: str, user: Optional[str] = None, from_tamper: bool = False):
|
||||
""" Remove the specified persistence method from the remote victim
|
||||
if the given persistence method is a system method, the "user"
|
||||
argument is ignored. """
|
||||
method = self.find(name)
|
||||
try:
|
||||
method = next(self.find(name))
|
||||
except StopIteration:
|
||||
raise PersistenceError(f"{name}: no such persistence method")
|
||||
if not method.system and user is None:
|
||||
raise PersistenceError("non-system methods require a user argument")
|
||||
raise PersistenceError(
|
||||
f"{method.format(user)}: non-system methods require a user argument"
|
||||
)
|
||||
if not method.installed(user):
|
||||
raise PersistenceError("not installed")
|
||||
raise PersistenceError(f"{method.format(user)}: not installed")
|
||||
method.remove(user)
|
||||
|
||||
# Grab this from the database
|
||||
persist = (
|
||||
pwncat.victim.session.query(pwncat.db.Persistence)
|
||||
.filter_by(host_id=pwncat.victim.host.id, method=name, user=user)
|
||||
.first()
|
||||
)
|
||||
if persist is not None:
|
||||
pwncat.victim.session.delete(persist)
|
||||
pwncat.victim.session.commit()
|
||||
|
||||
# Remove the tamper as well
|
||||
if not from_tamper:
|
||||
for tamper in pwncat.victim.tamper:
|
||||
if str(tamper) == f"Persistence: {method.format(user)}":
|
||||
pwncat.victim.tamper.remove(tamper)
|
||||
break
|
||||
|
||||
def __iter__(self) -> Iterator["PersistenceMethod"]:
|
||||
yield from self.methods.values()
|
||||
|
||||
@ -98,7 +157,14 @@ class PersistenceMethod:
|
||||
raise NotImplementedError
|
||||
|
||||
def installed(self, user: Optional[str] = None) -> bool:
|
||||
raise NotImplementedError
|
||||
if (
|
||||
pwncat.victim.session.query(pwncat.db.Persistence)
|
||||
.filter_by(method=self.name, user=user)
|
||||
.first()
|
||||
is not None
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def escalate(self, user: Optional[str] = None) -> bool:
|
||||
""" If this is a local method, this should escalate to the given user if
|
||||
|
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import crypt
|
||||
from typing import Optional
|
||||
from colorama import Fore
|
||||
|
||||
import pwncat
|
||||
from pwncat.persist import PersistenceMethod, PersistenceError
|
||||
@ -18,10 +19,6 @@ class Method(PersistenceMethod):
|
||||
def system(self) -> bool:
|
||||
return True
|
||||
|
||||
def installed(self, user: Optional[str] = None):
|
||||
pwncat.victim.reload_users()
|
||||
return pwncat.victim.config["backdoor_user"] in pwncat.victim.users
|
||||
|
||||
def install(self, user: Optional[str] = None):
|
||||
|
||||
try:
|
||||
@ -49,9 +46,6 @@ class Method(PersistenceMethod):
|
||||
except (PermissionError, FileNotFoundError) as exc:
|
||||
raise PersistenceError(str(exc))
|
||||
|
||||
# Track this modification in our tamper as well
|
||||
pwncat.victim.tamper.modified_file("/etc/passwd", added_lines=passwd[-1:])
|
||||
|
||||
# Ensure user cache is up to date
|
||||
pwncat.victim.reload_users()
|
||||
|
||||
@ -108,10 +102,6 @@ class Method(PersistenceMethod):
|
||||
|
||||
def escalate(self, user: Optional[str] = None) -> bool:
|
||||
|
||||
# We can always check this, so we might as well give it a try
|
||||
if not self.installed(user):
|
||||
return False
|
||||
|
||||
# First, escalate to the backdoor user
|
||||
pwncat.victim.run(f"su {pwncat.victim.config['backdoor_user']}", wait=False)
|
||||
pwncat.victim.recvuntil(b": ")
|
||||
|
@ -16,46 +16,9 @@ class Method(PersistenceMethod):
|
||||
name = "authorized_keys"
|
||||
local = False
|
||||
|
||||
def installed(self, user: Optional[str] = None) -> bool:
|
||||
|
||||
homedir = pwncat.victim.users[user]["home"]
|
||||
if not homedir or homedir == "":
|
||||
return False
|
||||
|
||||
# Create .ssh directory if it doesn't exist
|
||||
access = pwncat.victim.access(os.path.join(homedir, ".ssh"))
|
||||
if Access.DIRECTORY not in access or Access.EXISTS not in access:
|
||||
return False
|
||||
|
||||
# Create the authorized_keys file if it doesn't exist
|
||||
access = pwncat.victim.access(os.path.join(homedir, ".ssh", "authorized_keys"))
|
||||
if Access.EXISTS not in access:
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
# Read in the current authorized keys if it exists
|
||||
with pwncat.victim.open(
|
||||
os.path.join(homedir, ".ssh", "authorized_keys"), "r"
|
||||
) as filp:
|
||||
authkeys = filp.readlines()
|
||||
except (FileNotFoundError, PermissionError) as exc:
|
||||
return False
|
||||
try:
|
||||
# Read our public key
|
||||
with open(pwncat.victim.config["privkey"] + ".pub", "r") as filp:
|
||||
pubkey = filp.readlines()
|
||||
except (FileNotFoundError, PermissionError) as exc:
|
||||
return False
|
||||
|
||||
# Ensure we read a public key
|
||||
if not pubkey:
|
||||
return False
|
||||
|
||||
return pubkey[0] in authkeys
|
||||
|
||||
def install(self, user: Optional[str] = None):
|
||||
|
||||
homedir = pwncat.victim.users[user]["home"]
|
||||
homedir = pwncat.victim.users[user].homedir
|
||||
if not homedir or homedir == "":
|
||||
raise PersistenceError("no home directory")
|
||||
|
||||
@ -119,7 +82,7 @@ class Method(PersistenceMethod):
|
||||
|
||||
def remove(self, user: Optional[str] = None):
|
||||
|
||||
homedir = pwncat.victim.users[user]["home"]
|
||||
homedir = pwncat.victim.users[user].homedir
|
||||
if not homedir or homedir == "":
|
||||
raise PersistenceError("no home directory")
|
||||
|
||||
|
@ -127,9 +127,9 @@ class Finder:
|
||||
|
||||
current_user = pwncat.victim.current_user
|
||||
if (
|
||||
target_user == current_user["name"]
|
||||
or current_user["uid"] == 0
|
||||
or current_user["name"] == "root"
|
||||
target_user == current_user.name
|
||||
or current_user.id == 0
|
||||
or current_user.name == "root"
|
||||
):
|
||||
with pwncat.victim.open(filename, "wb", length=len(data)) as filp:
|
||||
filp.write(data)
|
||||
@ -201,9 +201,9 @@ class Finder:
|
||||
|
||||
current_user = pwncat.victim.current_user
|
||||
if (
|
||||
target_user == current_user["name"]
|
||||
or current_user["uid"] == 0
|
||||
or current_user["name"] == "root"
|
||||
target_user == current_user.name
|
||||
or current_user.id == 0
|
||||
or current_user.name == "root"
|
||||
):
|
||||
pipe = pwncat.victim.open(filename, "rb")
|
||||
return pipe, chain
|
||||
@ -351,7 +351,7 @@ class Finder:
|
||||
"/etc/passwd", added_lines=lines[-1:]
|
||||
)
|
||||
|
||||
pwncat.victim.users[user]["password"] = pwncat.victim.config[
|
||||
pwncat.victim.users[user].password = pwncat.victim.config[
|
||||
"backdoor_pass"
|
||||
]
|
||||
self.backdoor_user = pwncat.victim.users[user]
|
||||
@ -430,7 +430,7 @@ class Finder:
|
||||
# AuthorizedKeysFile is normally relative to the home directory
|
||||
if not authkeys_path.startswith("/"):
|
||||
# Grab the user information from /etc/passwd
|
||||
home = pwncat.victim.users[techniques[0].user]["home"]
|
||||
home = pwncat.victim.users[techniques[0].user].homedir
|
||||
|
||||
if home == "" or home is None:
|
||||
raise PrivescError("no user home directory, can't add ssh keys")
|
||||
@ -460,12 +460,12 @@ class Finder:
|
||||
# authenticate without a password and without clobbering their
|
||||
# keys.
|
||||
ssh_key_glob = os.path.join(
|
||||
pwncat.victim.users[reader.user]["home"], ".ssh", "*.pub"
|
||||
pwncat.victim.users[reader.user].homedir, ".ssh", "*.pub"
|
||||
)
|
||||
# keys = pwncat.victim.run(f"ls {ssh_key_glob}").strip().decode("utf-8")
|
||||
keys = ["id_rsa.pub"]
|
||||
keys = [
|
||||
os.path.join(pwncat.victim.users[reader.user]["home"], ".ssh", key)
|
||||
os.path.join(pwncat.victim.users[reader.user].homedir, ".ssh", key)
|
||||
for key in keys
|
||||
]
|
||||
|
||||
@ -553,9 +553,10 @@ class Finder:
|
||||
authkeys_path, ("\n".join(authkeys) + "\n").encode("utf-8"), writer
|
||||
)
|
||||
|
||||
if len(authkeys) > 1:
|
||||
if len(readers) > 0:
|
||||
# We read the content in, so we know what we added
|
||||
pwncat.victim.tamper.modified_file(
|
||||
authkeys_path, added_lines=pubkey + "\n"
|
||||
authkeys_path, added_lines=[pubkey + "\n"]
|
||||
)
|
||||
else:
|
||||
# We couldn't read their authkeys, but log that we clobbered it. The user asked us to. :shrug:
|
||||
@ -622,11 +623,11 @@ class Finder:
|
||||
|
||||
current_user = pwncat.victim.current_user
|
||||
if (
|
||||
target_user == current_user["name"]
|
||||
or current_user["uid"] == 0
|
||||
or current_user["name"] == "root"
|
||||
target_user == current_user.name
|
||||
or current_user.id == 0
|
||||
or current_user.name == "root"
|
||||
):
|
||||
raise PrivescError(f"you are already {current_user['name']}")
|
||||
raise PrivescError(f"you are already {current_user.name}")
|
||||
|
||||
if starting_user is None:
|
||||
starting_user = current_user
|
||||
@ -639,9 +640,9 @@ class Finder:
|
||||
|
||||
# Check if we have a persistence method for this user
|
||||
util.progress(f"checking local persistence implants")
|
||||
for persist in pwncat.victim.persist.find(
|
||||
installed=True, local=True, user=target_user
|
||||
):
|
||||
for user, persist in pwncat.victim.persist.installed:
|
||||
if not persist.local or (user != target_user and user is not None):
|
||||
continue
|
||||
util.progress(
|
||||
f"checking local persistence implants: {persist.format(target_user)}"
|
||||
)
|
||||
@ -699,35 +700,29 @@ class Finder:
|
||||
|
||||
# Try to use persistence as other users
|
||||
util.progress(f"checking local persistence implants")
|
||||
for user in pwncat.victim.users:
|
||||
for user, persist in pwncat.victim.persist.installed:
|
||||
if self.in_chain(user, chain):
|
||||
continue
|
||||
util.progress(f"checking local persistence implants: {user}")
|
||||
for persist in pwncat.victim.persist.find(
|
||||
installed=True, local=True, system=False, user=user
|
||||
):
|
||||
util.progress(
|
||||
f"checking local persistence implants: {persist.format(user)}"
|
||||
)
|
||||
if persist.escalate(user):
|
||||
if pwncat.victim.whoami() != user:
|
||||
if pwncat.victim.getenv("SHLVL") != shlvl:
|
||||
pwncat.victim.run("exit", wait=False)
|
||||
continue
|
||||
|
||||
chain.append(
|
||||
(f"persistence - {persist.format(target_user)}", "exit")
|
||||
)
|
||||
|
||||
try:
|
||||
return self.escalate(target_user, depth, chain, starting_user)
|
||||
except PrivescError:
|
||||
chain.pop()
|
||||
util.progress(
|
||||
f"checking local persistence implants: {persist.format(user)}"
|
||||
)
|
||||
if persist.escalate(user):
|
||||
if pwncat.victim.whoami() != user:
|
||||
if pwncat.victim.getenv("SHLVL") != shlvl:
|
||||
pwncat.victim.run("exit", wait=False)
|
||||
continue
|
||||
|
||||
# Don't retry later
|
||||
if user in techniques:
|
||||
del techniques[user]
|
||||
chain.append((f"persistence - {persist.format(target_user)}", "exit"))
|
||||
|
||||
try:
|
||||
return self.escalate(target_user, depth, chain, starting_user)
|
||||
except PrivescError:
|
||||
chain.pop()
|
||||
pwncat.victim.run("exit", wait=False)
|
||||
|
||||
# Don't retry later
|
||||
if user in techniques:
|
||||
del techniques[user]
|
||||
|
||||
# We can't escalate directly to the target. Instead, try recursively
|
||||
# against other users.
|
||||
|
@ -95,12 +95,12 @@ class SuMethod(Method):
|
||||
for user, info in self.pty.users.items():
|
||||
if user == current_user:
|
||||
continue
|
||||
if info.get("password") is not None or current_user == "root":
|
||||
if info.password is not None or current_user == "root":
|
||||
result.append(
|
||||
Technique(
|
||||
user=user,
|
||||
method=self,
|
||||
ident=info["password"],
|
||||
ident=info.password,
|
||||
capabilities=Capability.SHELL,
|
||||
)
|
||||
)
|
||||
@ -113,7 +113,7 @@ class SuMethod(Method):
|
||||
|
||||
password = technique.ident.encode("utf-8")
|
||||
|
||||
if current_user["name"] != "root":
|
||||
if current_user.name != "root":
|
||||
# Send the su command, and check if it succeeds
|
||||
self.pty.run(
|
||||
f'su {technique.user} -c "echo good"', wait=False,
|
||||
@ -133,7 +133,7 @@ class SuMethod(Method):
|
||||
|
||||
self.pty.process(f"su {technique.user}", delim=False)
|
||||
|
||||
if current_user["name"] != "root":
|
||||
if current_user.name != "root":
|
||||
self.pty.recvuntil(": ")
|
||||
self.pty.client.sendall(technique.ident.encode("utf-8") + b"\n")
|
||||
self.pty.flush_output()
|
||||
|
@ -10,6 +10,7 @@ import socket
|
||||
from io import StringIO, BytesIO
|
||||
import functools
|
||||
|
||||
import pwncat
|
||||
from pwncat.util import CTRL_C
|
||||
from pwncat.privesc.base import Method, PrivescError, Technique
|
||||
from pwncat.file import RemoteBinaryPipe
|
||||
@ -29,16 +30,6 @@ class DirtycowMethod(Method):
|
||||
super(DirtycowMethod, self).__init__(pty)
|
||||
self.ran_before = False
|
||||
|
||||
with open("data/dirtycow/mini_dirtycow.c") as h:
|
||||
self.dc_source = h.read()
|
||||
|
||||
self.dc_source = self.dc_source.replace(
|
||||
"PWNCAT_USER", self.pty.privesc.backdoor_user_name
|
||||
)
|
||||
self.dc_source = self.dc_source.replace(
|
||||
"PWNCAT_PASS", self.pty.privesc.backdoor_password
|
||||
)
|
||||
|
||||
def enumerate(self, capability: int = Capability.ALL) -> List[Technique]:
|
||||
""" Find all techniques known at this time """
|
||||
|
||||
@ -65,6 +56,16 @@ class DirtycowMethod(Method):
|
||||
def execute(self, technique: Technique):
|
||||
""" Run the specified technique """
|
||||
|
||||
with open("data/dirtycow/mini_dirtycow.c") as h:
|
||||
dc_source = h.read()
|
||||
|
||||
dc_source = dc_source.replace(
|
||||
"PWNCAT_USER", pwncat.victim.config["backdoor_user"]
|
||||
)
|
||||
dc_source = dc_source.replace(
|
||||
"PWNCAT_PASS", pwncat.victim.config["backdoor_pass"]
|
||||
)
|
||||
|
||||
self.ran_before = True
|
||||
|
||||
writer = gtfobins.Binary.find_capability(self.pty.which, Capability.WRITE)
|
||||
@ -75,7 +76,7 @@ class DirtycowMethod(Method):
|
||||
dc_binary = self.pty.run("mktemp").decode("utf-8").strip()
|
||||
|
||||
# Write the file
|
||||
self.pty.run(writer.write_file(dc_source, self.dc_source))
|
||||
self.pty.run(writer.write_file(dc_source, dc_source))
|
||||
|
||||
# Compile Dirtycow
|
||||
self.pty.run(f"cc -pthread {dc_source_file} -o {dc_binary} -lcrypt")
|
||||
|
@ -7,6 +7,7 @@ import os
|
||||
from colorama import Fore, Style
|
||||
import io
|
||||
|
||||
import pwncat
|
||||
from pwncat.privesc.base import Method, PrivescError, Technique
|
||||
from pwncat.gtfobins import Binary, Stream, Capability, MethodWrapper, BinaryNotFound
|
||||
from pwncat.file import RemoteBinaryPipe
|
||||
@ -26,18 +27,15 @@ class SetuidMethod(Method):
|
||||
|
||||
def find_suid(self):
|
||||
|
||||
current_user = self.pty.whoami()
|
||||
current_user: "pwncat.db.User" = pwncat.victim.current_user
|
||||
|
||||
# Only re-run the search if we haven't searched as this user yet
|
||||
if current_user in self.users_searched:
|
||||
# We've already searched for SUID binaries as this user
|
||||
if len(current_user.suid):
|
||||
return
|
||||
|
||||
# Note that we already searched for binaries as this user
|
||||
self.users_searched.append(current_user)
|
||||
|
||||
# Spawn a find command to locate the setuid binaries
|
||||
files = []
|
||||
with self.pty.subprocess(
|
||||
with pwncat.victim.subprocess(
|
||||
"find / -perm -4000 -print 2>/dev/null", mode="r", no_job=True
|
||||
) as stream:
|
||||
util.progress("searching for setuid binaries")
|
||||
@ -45,7 +43,7 @@ class SetuidMethod(Method):
|
||||
path = path.strip().decode("utf-8")
|
||||
util.progress(
|
||||
(
|
||||
f"searching for setuid binaries as {Fore.GREEN}{current_user}{Fore.RESET}: "
|
||||
f"searching for setuid binaries as {Fore.GREEN}{current_user.name}{Fore.RESET}: "
|
||||
f"{Fore.CYAN}{os.path.basename(path)}{Fore.RESET}"
|
||||
)
|
||||
)
|
||||
@ -53,17 +51,17 @@ class SetuidMethod(Method):
|
||||
|
||||
util.success("searching for setuid binaries: complete", overlay=True)
|
||||
|
||||
for path in files:
|
||||
user = (
|
||||
self.pty.run(f"stat -c '%U' {shlex.quote(path)}")
|
||||
.strip()
|
||||
.decode("utf-8")
|
||||
)
|
||||
if user not in self.suid_paths:
|
||||
self.suid_paths[user] = []
|
||||
# Only add new binaries
|
||||
if path not in self.suid_paths[user]:
|
||||
self.suid_paths[user].append(path)
|
||||
with pwncat.victim.subprocess(
|
||||
f"stat -c '%U' {' '.join(files)}", mode="r", no_job=True
|
||||
) as stream:
|
||||
for file, user in zip(files, stream):
|
||||
user = user.strip().decode("utf-8")
|
||||
binary = pwncat.db.SUID(path=file,)
|
||||
pwncat.victim.host.suid.append(binary)
|
||||
pwncat.victim.users[user].owned_suid.append(binary)
|
||||
current_user.suid.append(binary)
|
||||
|
||||
pwncat.victim.session.commit()
|
||||
|
||||
def enumerate(self, caps: Capability = Capability.ALL) -> List[Technique]:
|
||||
""" Find all techniques known at this time """
|
||||
@ -72,15 +70,16 @@ class SetuidMethod(Method):
|
||||
self.find_suid()
|
||||
|
||||
known_techniques = []
|
||||
for user, paths in self.suid_paths.items():
|
||||
for path in paths:
|
||||
try:
|
||||
binary = self.pty.gtfo.find_binary(path, caps)
|
||||
except BinaryNotFound:
|
||||
continue
|
||||
for suid in pwncat.victim.host.suid:
|
||||
try:
|
||||
binary = pwncat.victim.gtfo.find_binary(suid.path, caps)
|
||||
except BinaryNotFound:
|
||||
continue
|
||||
|
||||
for method in binary.iter_methods(path, caps, Stream.ANY):
|
||||
known_techniques.append(Technique(user, self, method, method.cap))
|
||||
for method in binary.iter_methods(suid.path, caps, Stream.ANY):
|
||||
known_techniques.append(
|
||||
Technique(suid.owner.name, self, method, method.cap)
|
||||
)
|
||||
|
||||
return known_techniques
|
||||
|
||||
@ -90,14 +89,16 @@ class SetuidMethod(Method):
|
||||
method = technique.ident
|
||||
|
||||
# Build the payload
|
||||
payload, input_data, exit_cmd = method.build(shell=self.pty.shell, suid=True)
|
||||
payload, input_data, exit_cmd = method.build(
|
||||
shell=pwncat.victim.shell, suid=True
|
||||
)
|
||||
|
||||
# Run the start commands
|
||||
# self.pty.process(payload, delim=False)
|
||||
self.pty.run(payload, wait=False)
|
||||
# pwncat.victim.process(payload, delim=False)
|
||||
pwncat.victim.run(payload, wait=False)
|
||||
|
||||
# Send required input
|
||||
self.pty.client.send(input_data.encode("utf-8"))
|
||||
pwncat.victim.client.send(input_data.encode("utf-8"))
|
||||
|
||||
return exit_cmd # remember how to close out of this privesc
|
||||
|
||||
@ -112,7 +113,7 @@ class SetuidMethod(Method):
|
||||
mode += "b"
|
||||
|
||||
# Send the read payload
|
||||
pipe = self.pty.subprocess(
|
||||
pipe = pwncat.victim.subprocess(
|
||||
payload,
|
||||
mode,
|
||||
data=input_data.encode("utf-8"),
|
||||
@ -138,7 +139,7 @@ class SetuidMethod(Method):
|
||||
mode += "b"
|
||||
|
||||
# Send the read payload
|
||||
pipe = self.pty.subprocess(
|
||||
pipe = pwncat.victim.subprocess(
|
||||
payload,
|
||||
mode,
|
||||
data=input_data.encode("utf-8"),
|
||||
|
@ -26,7 +26,7 @@ class SudoMethod(Method):
|
||||
def __init__(self, pty: "pwncat.pty.PtyHandler"):
|
||||
super(SudoMethod, self).__init__(pty)
|
||||
|
||||
def send_password(self, current_user):
|
||||
def send_password(self, current_user: "pwncat.db.User"):
|
||||
|
||||
# peak the output
|
||||
output = self.pty.peek_output(some=False).lower()
|
||||
@ -36,10 +36,10 @@ class SudoMethod(Method):
|
||||
or b"password for " in output
|
||||
or output.endswith(b"password: ")
|
||||
):
|
||||
if current_user["password"] is None:
|
||||
if current_user.password is None:
|
||||
self.pty.client.send(CTRL_C) # break out of password prompt
|
||||
raise PrivescError(
|
||||
f"user {Fore.GREEN}{current_user['name']}{Fore.RESET} has no known password"
|
||||
f"user {Fore.GREEN}{current_user.name}{Fore.RESET} has no known password"
|
||||
)
|
||||
else:
|
||||
return # it did not ask for a password, continue as usual
|
||||
@ -50,7 +50,7 @@ class SudoMethod(Method):
|
||||
# Reset the timeout to allow for sudo to pause
|
||||
old_timeout = self.pty.client.gettimeout()
|
||||
self.pty.client.settimeout(5)
|
||||
self.pty.client.send(current_user["password"].encode("utf-8") + b"\n")
|
||||
self.pty.client.send(current_user.password.encode("utf-8") + b"\n")
|
||||
|
||||
output = self.pty.peek_output(some=True)
|
||||
|
||||
@ -68,7 +68,7 @@ class SudoMethod(Method):
|
||||
# Flush all the output
|
||||
self.pty.recvuntil(b"\n")
|
||||
raise PrivescError(
|
||||
f"user {Fore.GREEN}{current_user['name']}{Fore.RESET} could not sudo"
|
||||
f"user {Fore.GREEN}{current_user.name}{Fore.RESET} could not sudo"
|
||||
)
|
||||
|
||||
return
|
||||
@ -101,7 +101,7 @@ class SudoMethod(Method):
|
||||
|
||||
sudo_values = "\n".join(
|
||||
[
|
||||
f"{current_user['name']} ALL={l.decode('utf-8').strip()}"
|
||||
f"{current_user.name} ALL={l.decode('utf-8').strip()}"
|
||||
for l in sudo_output_lines[sudo_output_index:]
|
||||
]
|
||||
)
|
||||
@ -115,8 +115,6 @@ class SudoMethod(Method):
|
||||
|
||||
sudo_rules = self.find_sudo()
|
||||
|
||||
current_user = self.pty.current_user
|
||||
|
||||
if not sudo_rules:
|
||||
return []
|
||||
|
||||
@ -171,7 +169,7 @@ class SudoMethod(Method):
|
||||
|
||||
techniques = []
|
||||
for sudo_privesc in [*sudo_no_password, *sudo_all_users, *sudo_other_commands]:
|
||||
if current_user["password"] is None and sudo_privesc["password"]:
|
||||
if current_user.password is None and sudo_privesc["password"]:
|
||||
continue
|
||||
|
||||
# Split the users on a comma
|
||||
|
147
pwncat/remote.py
147
pwncat/remote.py
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
import shlex
|
||||
@ -9,7 +10,10 @@ from typing import Dict, Optional, IO, Any, List, Tuple
|
||||
|
||||
import requests
|
||||
from colorama import Fore
|
||||
from sqlalchemy.engine import Engine, create_engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
import pwncat.db
|
||||
from pwncat import privesc
|
||||
from pwncat import persist
|
||||
from pwncat import util
|
||||
@ -91,15 +95,68 @@ class Victim:
|
||||
self.shell: str = "unknown"
|
||||
self.privesc: privesc.Finder = None
|
||||
self.persist: persist.Persistence = persist.Persistence()
|
||||
self.engine: Engine = None
|
||||
self.session: Session = None
|
||||
self.host: pwncat.db.Host = None
|
||||
|
||||
def connect(self, client: socket.SocketType):
|
||||
|
||||
self.engine = create_engine(self.config["db"], echo=False)
|
||||
pwncat.db.Base.metadata.create_all(self.engine)
|
||||
|
||||
if self.session is None:
|
||||
self.session_maker = sessionmaker(bind=self.engine)
|
||||
self.session = self.session_maker()
|
||||
|
||||
# Initialize the socket connection
|
||||
self.client = client
|
||||
|
||||
# We should always get a response within 3 seconds...
|
||||
self.client.settimeout(1)
|
||||
|
||||
# Attempt to grab the remote hostname and mac address
|
||||
hostname_path = self.run("which hostname").strip().decode("utf-8")
|
||||
if hostname_path.startswith("/"):
|
||||
hostname = self.run("hostname -f")
|
||||
else:
|
||||
util.warn("hostname command not found; using peer address")
|
||||
hostname = client.getpeername().encode("utf-8")
|
||||
mac = None
|
||||
|
||||
# Use ifconfig if available or ip link show.
|
||||
ifconfig = self.run("which ifconfig").strip().decode("utf-8")
|
||||
if ifconfig.startswith("/"):
|
||||
ifconfig_a = self.run(f"{ifconfig} -a").strip().decode("utf-8").lower()
|
||||
for line in ifconfig_a.split("\n"):
|
||||
if "hwaddr" in line and "00:00:00:00:00:00" not in line:
|
||||
mac = line.split("hwaddr ")[1].split("\n")[0].strip()
|
||||
break
|
||||
if mac is None:
|
||||
ip = self.run("which ip").strip().decode("utf-8")
|
||||
if ip.startswith("/"):
|
||||
ip_link_show = self.run("ip link show").strip().decode("utf-8").lower()
|
||||
for line in ip_link_show.split("\n"):
|
||||
if "link/ether" in line and "00:00:00:00:00:00" not in line:
|
||||
mac = line.split("link/ether ")[1].split(" ")[0]
|
||||
break
|
||||
|
||||
if mac is None:
|
||||
util.warn("no mac address detected; host id only based on hostname!")
|
||||
|
||||
# Calculate the remote host's hash entry for lookup/storage in the database
|
||||
host_hash = hashlib.md5(hostname + str(mac).encode("utf-8")).hexdigest()
|
||||
|
||||
# Lookup the remote host in our database. If it's not there, create an entry
|
||||
self.host = self.session.query(pwncat.db.Host).filter_by(hash=host_hash).first()
|
||||
if self.host is None:
|
||||
self.host = pwncat.db.Host(hash=host_hash)
|
||||
self.session.add(self.host)
|
||||
self.session.commit()
|
||||
|
||||
# We initialize this here, because it needs the database to initialize
|
||||
# the history objects
|
||||
self.command_parser.setup_prompt()
|
||||
|
||||
# Attempt to identify architecture
|
||||
self.arch = self.run("uname -m").decode("utf-8").strip()
|
||||
if self.arch == "amd64":
|
||||
@ -959,42 +1016,86 @@ class Victim:
|
||||
|
||||
def reload_users(self):
|
||||
""" Clear user cache and reload it """
|
||||
self.known_users = None
|
||||
|
||||
# Clear the user cache
|
||||
with self.open("/etc/passwd", "r") as filp:
|
||||
for line in filp:
|
||||
line = line.strip()
|
||||
if line == "" or line[0] == "#":
|
||||
continue
|
||||
line = line.strip().split(":")
|
||||
user = (
|
||||
self.session.query(pwncat.db.User)
|
||||
.filter_by(host_id=self.host.id, id=int(line[2]))
|
||||
.first()
|
||||
)
|
||||
if user is None:
|
||||
user = pwncat.db.User(id=int(line[2]))
|
||||
user.name = line[0]
|
||||
user.id = int(line[2])
|
||||
user.gid = int(line[3])
|
||||
user.fullname = line[4]
|
||||
user.homedir = line[5]
|
||||
user.shell = line[6]
|
||||
if user not in self.host.users:
|
||||
self.host.users.append(user)
|
||||
|
||||
with self.open("/etc/group", "r") as filp:
|
||||
for line in filp:
|
||||
line = line.strip()
|
||||
if line == "" or line.startswith("#"):
|
||||
continue
|
||||
|
||||
line = line.split(":")
|
||||
group = (
|
||||
self.session.query(pwncat.db.Group)
|
||||
.filter_by(host_id=self.host.id, id=int(line[2]))
|
||||
.first()
|
||||
)
|
||||
if group is None:
|
||||
group = pwncat.db.Group(name=line[0], id=int(line[2]))
|
||||
|
||||
group.name = line[0]
|
||||
group.id = int(line[2])
|
||||
|
||||
for username in line[3].split(","):
|
||||
user = (
|
||||
self.session.query(pwncat.db.User)
|
||||
.filter_by(host_id=self.host.id, name=username)
|
||||
.first()
|
||||
)
|
||||
if user is not None and user not in group.members:
|
||||
group.members.append(user)
|
||||
|
||||
if group not in self.host.groups:
|
||||
self.host.groups.append(group)
|
||||
|
||||
return self.users
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
def users(self) -> Dict[str, pwncat.db.User]:
|
||||
|
||||
if self.client is None:
|
||||
return {}
|
||||
|
||||
if self.known_users:
|
||||
return self.known_users
|
||||
if len(self.host.users) == 0:
|
||||
self.reload_users()
|
||||
|
||||
self.known_users = {}
|
||||
known_users = {}
|
||||
|
||||
passwd = self.run("cat /etc/passwd").decode("utf-8")
|
||||
for line in passwd.split("\n"):
|
||||
line = line.strip()
|
||||
if line == "" or line[0] == "#":
|
||||
continue
|
||||
line = line.strip().split(":")
|
||||
for user in self.host.users:
|
||||
known_users[user.name] = user
|
||||
|
||||
user_data = {
|
||||
"name": line[0],
|
||||
"password": None,
|
||||
"uid": int(line[2]),
|
||||
"gid": int(line[3]),
|
||||
"description": line[4],
|
||||
"home": line[5],
|
||||
"shell": line[6],
|
||||
}
|
||||
self.known_users[line[0]] = user_data
|
||||
return known_users
|
||||
|
||||
return self.known_users
|
||||
def find_user_by_id(self, uid: int):
|
||||
for user in self.users:
|
||||
if user.id == uid:
|
||||
return user
|
||||
raise KeyError
|
||||
|
||||
@property
|
||||
def current_user(self):
|
||||
def current_user(self) -> Optional[pwncat.db.User]:
|
||||
name = self.whoami()
|
||||
if name in self.users:
|
||||
return self.users[name]
|
||||
|
@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
import pickle
|
||||
from typing import List, Optional, Callable, Iterator
|
||||
from enum import Enum, auto
|
||||
from colorama import Fore
|
||||
|
||||
import pwncat
|
||||
from pwncat.util import Access
|
||||
|
||||
|
||||
class Action(Enum):
|
||||
@ -34,6 +36,8 @@ class CreatedFile(Tamper):
|
||||
def revert(self):
|
||||
try:
|
||||
pwncat.victim.run(f"rm -f {self.path}")
|
||||
if Access.EXISTS in pwncat.victim.access(self.path):
|
||||
raise RevertFailed(f"{self.path}: unable to remove file")
|
||||
except (PermissionError, FileNotFoundError) as exc:
|
||||
raise RevertFailed(str(exc))
|
||||
|
||||
@ -57,34 +61,33 @@ class ModifiedFile(Tamper):
|
||||
self.original_content = original_content
|
||||
|
||||
def revert(self):
|
||||
if self.added_lines:
|
||||
# Read the current lines
|
||||
with pwncat.victim.open(self.path, "r") as filp:
|
||||
lines = filp.readlines()
|
||||
try:
|
||||
if self.added_lines:
|
||||
# Read the current lines
|
||||
with pwncat.victim.open(self.path, "r") as filp:
|
||||
lines = filp.readlines()
|
||||
|
||||
# Remove matching lines
|
||||
for line in self.added_lines:
|
||||
try:
|
||||
lines.remove(line)
|
||||
except ValueError:
|
||||
pass
|
||||
# Remove matching lines
|
||||
for line in self.added_lines:
|
||||
try:
|
||||
lines.remove(line)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Write the new original_content
|
||||
file_data = "".join(lines)
|
||||
with pwncat.victim.open(self.path, "w", length=len(file_data)) as filp:
|
||||
filp.write(file_data)
|
||||
# Write the new original_content
|
||||
file_data = "".join(lines)
|
||||
with pwncat.victim.open(self.path, "w", length=len(file_data)) as filp:
|
||||
filp.write(file_data)
|
||||
|
||||
elif self.original_content:
|
||||
# Write the given original original_content back to the remote file
|
||||
try:
|
||||
elif self.original_content:
|
||||
with pwncat.victim.open(
|
||||
self.path, "wb", length=len(self.original_content)
|
||||
) as filp:
|
||||
filp.write(self.original_content)
|
||||
except (PermissionError, FileNotFoundError) as exc:
|
||||
raise RevertFailed(str(exc))
|
||||
else:
|
||||
raise RevertFailed("no original_content or added_lines specified")
|
||||
else:
|
||||
raise RevertFailed("no original_content or added_lines specified")
|
||||
except (PermissionError, FileNotFoundError) as exc:
|
||||
raise RevertFailed(str(exc))
|
||||
|
||||
def __str__(self):
|
||||
return f"{Fore.RED}Modified{Fore.RESET} {Fore.CYAN}{self.path}{Fore.RESET}"
|
||||
@ -127,7 +130,7 @@ class TamperManager:
|
||||
added_lines: Optional[List[str]] = None,
|
||||
):
|
||||
""" Add a new modified file tamper """
|
||||
self.tampers.append(
|
||||
self.add(
|
||||
ModifiedFile(
|
||||
path, added_lines=added_lines, original_content=original_content
|
||||
)
|
||||
@ -135,20 +138,39 @@ class TamperManager:
|
||||
|
||||
def created_file(self, path: str):
|
||||
""" Register a new added file on the remote system """
|
||||
self.tampers.append(CreatedFile(path))
|
||||
self.add(CreatedFile(path))
|
||||
|
||||
def add(self, tamper: Tamper):
|
||||
""" Register a custom tamper tracker """
|
||||
self.tampers.append(tamper)
|
||||
serialized = pickle.dumps(tamper)
|
||||
tracker = pwncat.db.Tamper(name=str(tamper), data=serialized)
|
||||
pwncat.victim.host.tampers.append(tracker)
|
||||
pwncat.victim.session.commit()
|
||||
|
||||
def custom(self, name: str, revert: Optional[Callable] = None):
|
||||
self.tampers.append(LambdaTamper(name, revert))
|
||||
self.add(LambdaTamper(name, revert))
|
||||
|
||||
def __iter__(self) -> Iterator[Tamper]:
|
||||
yield from self.tampers
|
||||
for tracker in pwncat.victim.host.tampers:
|
||||
yield pickle.loads(tracker.data)
|
||||
|
||||
def __len__(self):
|
||||
return len(pwncat.victim.host.tampers)
|
||||
|
||||
def __getitem__(self, item: int):
|
||||
if not isinstance(item, int):
|
||||
raise KeyError(f"{item}: not an integer")
|
||||
return pickle.loads(pwncat.victim.host.tampers[item].data)
|
||||
|
||||
def remove(self, tamper: Tamper):
|
||||
""" Pop a tamper from the list of known tampers. This does not revert the tamper.
|
||||
It removes the tracking for this tamper. """
|
||||
|
||||
return self.tampers.remove(tamper)
|
||||
tracker = (
|
||||
pwncat.victim.session.query(pwncat.db.Tamper)
|
||||
.filter_by(name=str(tamper))
|
||||
.first()
|
||||
)
|
||||
if tracker is not None:
|
||||
pwncat.victim.session.delete(tracker)
|
||||
pwncat.victim.session.commit()
|
||||
|
@ -5,4 +5,5 @@ netifaces==0.10.9
|
||||
pygments==2.6.1
|
||||
base64io
|
||||
commentjson
|
||||
requests
|
||||
requests
|
||||
sqlalchemy
|
Loading…
Reference in New Issue
Block a user