diff --git a/pwncat/__init__.py b/pwncat/__init__.py index de21680..e90c147 100644 --- a/pwncat/__init__.py +++ b/pwncat/__init__.py @@ -14,14 +14,13 @@ victim: Optional["pwncat.remote.Victim"] = None from .config import Config from .commands import parser from .util import console -from .db import get_session from .tamper import TamperManager tamper: TamperManager = TamperManager() def interactive(platform): - """ Run the interactive pwncat shell with the given initialized victim. + """Run the interactive pwncat shell with the given initialized victim. This function handles the pwncat and remote prompts and does not return until explicitly exited by the user. diff --git a/pwncat/commands/base.py b/pwncat/commands/base.py index 625f598..b6e3175 100644 --- a/pwncat/commands/base.py +++ b/pwncat/commands/base.py @@ -121,6 +121,21 @@ def RemoteFileType(file_exist=True, directory_exist=False): return _type +def get_module_choices(command): + """Yields a list of module choices to be used which command argument + choices to select a valid module for the current target""" + + if command.manager.target is None: + return + + yield from [ + module.name.removeprefix("agnostic.").removeprefix( + command.manager.target.platform.name + "." + ) + for module in command.manager.target.find_module("*") + ] + + class Parameter: """Generic parameter definition for commands. diff --git a/pwncat/commands/connect.py b/pwncat/commands/connect.py index 7fb389e..2c70f1e 100644 --- a/pwncat/commands/connect.py +++ b/pwncat/commands/connect.py @@ -20,7 +20,6 @@ from pwncat.commands.base import ( ) from pwncat.modules import PersistError -from pwncat.db import get_session class Command(CommandDefinition): diff --git a/pwncat/commands/info.py b/pwncat/commands/info.py index a371ac5..1538ac8 100644 --- a/pwncat/commands/info.py +++ b/pwncat/commands/info.py @@ -5,19 +5,18 @@ from rich.table import Table from rich import box import pwncat -from pwncat.commands.base import CommandDefinition, Complete, Parameter +from pwncat.commands.base import ( + CommandDefinition, + Complete, + Parameter, + get_module_choices, +) from pwncat.util import console class Command(CommandDefinition): """ View info about a module """ - def get_module_choices(self): - if self.manager.target is None: - return - - yield from [module.name for module in self.manager.target.find_module("*")] - PROG = "info" ARGS = { "module": Parameter( @@ -37,15 +36,21 @@ class Command(CommandDefinition): if args.module: try: - module = list(manager.target.find_module(args.module, exact=True))[0] - except IndexError: + module = next(manager.target.find_module(args.module, exact=True)) + module_name = args.module + except StopIteration: console.log(f"[red]error[/red]: {args.module}: no such module") return else: module = manager.config.module + module_name = module.name.removeprefix("agnostic.") + if self.manager.target is not None: + module_name = module_name.removeprefix( + self.manager.target.platform.name + "." + ) console.print( - f"[bold underline]Module [cyan]{module.name}[/cyan][/bold underline]" + f"[bold underline]Module [cyan]{module_name}[/cyan][/bold underline]" ) console.print( textwrap.indent(textwrap.dedent(module.__doc__.strip("\n")), " ") + "\n" diff --git a/pwncat/commands/run.py b/pwncat/commands/run.py index 9db1ba2..ec19b3b 100644 --- a/pwncat/commands/run.py +++ b/pwncat/commands/run.py @@ -4,7 +4,12 @@ import textwrap import pwncat import pwncat.modules from pwncat.util import console -from pwncat.commands.base import CommandDefinition, Complete, Parameter +from pwncat.commands.base import ( + CommandDefinition, + Complete, + Parameter, + get_module_choices, +) class Command(CommandDefinition): @@ -22,12 +27,6 @@ class Command(CommandDefinition): arguments, you can use the `info` command. """ - def get_module_choices(self): - if self.manager.target is None: - return - - yield from [module.name for module in self.manager.target.find_module("*")] - PROG = "run" ARGS = { "--raw,-r": Parameter( diff --git a/pwncat/commands/search.py b/pwncat/commands/search.py index f8f2e87..ac883d7 100644 --- a/pwncat/commands/search.py +++ b/pwncat/commands/search.py @@ -12,12 +12,6 @@ from pwncat.util import console class Command(CommandDefinition): """ View info about a module """ - def get_module_choices(self): - if self.manager.target is None: - return - - yield from [module.name for module in self.manager.target.find_module("*")] - PROG = "search" ARGS = { "module": Parameter( @@ -42,8 +36,15 @@ class Command(CommandDefinition): # the easiest way to do that, so we use a large size for # width. description = module.__doc__ if module.__doc__ is not None else "" + module_name = module.name.removeprefix("agnostic.") + + if self.manager.target is not None: + module_name = module_name.removeprefix( + self.manager.target.platform.name + "." + ) + table.add_row( - f"[cyan]{module.name}[/cyan]", + f"[cyan]{module_name}[/cyan]", textwrap.shorten( description.replace("\n", " "), width=200, placeholder="..." ), diff --git a/pwncat/commands/set.py b/pwncat/commands/set.py index 0078806..bf8e04e 100644 --- a/pwncat/commands/set.py +++ b/pwncat/commands/set.py @@ -6,7 +6,6 @@ from sqlalchemy.orm import sessionmaker import pwncat from pwncat.commands.base import CommandDefinition, Complete, Parameter from pwncat.util import console, State -from pwncat.db import get_session, reset_engine class Command(CommandDefinition): diff --git a/pwncat/commands/use.py b/pwncat/commands/use.py index a7883b3..9162e07 100644 --- a/pwncat/commands/use.py +++ b/pwncat/commands/use.py @@ -1,16 +1,18 @@ #!/usr/bin/env python3 import pwncat -from pwncat.commands.base import CommandDefinition, Complete, Parameter +from pwncat.commands.base import ( + CommandDefinition, + Complete, + Parameter, + get_module_choices, +) from pwncat.util import console class Command(CommandDefinition): """ Set the currently used module in the config handler """ - def get_module_choices(self): - yield from [module.name for module in self.manager.target.find_module("*")] - PROG = "use" ARGS = { "module": Parameter( diff --git a/pwncat/db/__init__.py b/pwncat/db/__init__.py index 6f9616c..1995985 100644 --- a/pwncat/db/__init__.py +++ b/pwncat/db/__init__.py @@ -1,12 +1,10 @@ #!/usr/bin/env python3 import pwncat -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 +from pwncat.db.user import User, Group from pwncat.db.fact import Fact diff --git a/pwncat/db/fact.py b/pwncat/db/fact.py index b71ba79..91d963e 100644 --- a/pwncat/db/fact.py +++ b/pwncat/db/fact.py @@ -1,25 +1,38 @@ #!/usr/bin/env python3 - -import persistent from typing import Optional +import persistent +from persistent.list import PersistentList -class Fact(persistent.Persistent): - """Store enumerated facts. The pwncat.enumerate.Fact objects are pickled and - stored in the "data" column. The enumerator is arbitrary, but allows for - organizations based on the source enumerator.""" +from pwncat.modules import Result - def __init__(self, arg_type, source): + +class Fact(Result, persistent.Persistent): + """Abstract enumerated facts about an enumerated target. Individual + enumeration modules will create subclasses containing the data for + the fact. A generic fact is guaranteed to have a list of types, a + module source, a __repr__ implementation, a __str__ implementation. + + By default, a category property is defined which is the first type + in the list of types. This can be overloaded if needed, and is used + when formatted and displaying enumeration results. + + Lastly, if the description property is not None, it indicates that + the fact has a "long form" description as opposed to a single-line + content. This only effects the way reports are generated. + """ + + def __init__(self, types, source): + super().__init__() + + if not isinstance(types, PersistentList): + types = PersistentList(types) # The type of fact (e.g.., "system.user") - self.type: Optional[str] = arg_type + self.types: PersistentList = types # The original procedure that found this fact - self.source: Optional[str] = source - - # The original SQLAlchemy-style code held a property, "data", - # which was a pickle object. We will re-implement that as a subclass - # but that may need to include the class properties used previously. + self.source: str = source @property def category(self) -> str: - return f"{self.type}" + return f"{self.types[0]} facts" diff --git a/pwncat/db/user.py b/pwncat/db/user.py index 63ea449..73121c0 100644 --- a/pwncat/db/user.py +++ b/pwncat/db/user.py @@ -1,38 +1,51 @@ #!/usr/bin/env python3 - -import persistent -import persistent.list from typing import Optional +from persistent.list import PersistentList -class Group(persistent.Persistent): - """ - Stores a record of changes on the target (i.e., things that have been - tampered with) - """ +from pwncat.db.fact import Fact - def __init__(self, name, members): - self.name: Optional[str] = name - self.members: persistent.list.PersistentList = persistent.list.PersistentList() +class Group(Fact): + """Basic representation of a user group on the target system. Individual + platform enumeration modules may subclass this to implement other user + properties as needed for their platform.""" + + def __init__(self, source: str, name: str, gid, members): + super().__init__(["group"], source) + + self.name: str = name + self.id = gid + self.members: PersistentList = PersistentList(members) def __repr__(self): - return f"""Group(gid={self.id}, name={repr(self.name)}), members={repr(",".join(m.name for m in self.members))})""" + return f"""Group(gid={self.id}, name={repr(self.name)}), members={repr(",".join(m for m in self.members))})""" -class User(persistent.Persistent): - def __init__(self, name, gid, fullname, homedir, password, hash, shell, groups): +class User(Fact): + """Basic representation of a user on the target system. Individual platform + enumeration modules may subclass this to implement other user properties as + needed for their platform.""" - self.name: Optional[str] = name - self.gid: Optional[int] = gid - self.fullname: Optional[str] = fullname - self.homedir: Optional[str] = homedir - self.password: Optional[str] = password - self.hash: Optional[str] = hash - self.shell: Optional[str] = shell - self.groups: persistent.list.PersistentList = persistent.list.PersistentList( - groups - ) + def __init__( + self, + source: str, + name, + uid, + password: Optional[str] = None, + hash: Optional[str] = None, + ): + super().__init__(["user"], source) + + self.name: str = name + self.id = uid + self.password: Optional[str] = None + self.hash: Optional[str] = None def __repr__(self): - return f"""User(uid={self.id}, gid={self.gid}, name={repr(self.name)})""" + if self.password is None and self.hash is None: + return f"""User(uid={self.id}, name={repr(self.name)})""" + elif self.password is not None: + return f"""User(uid={repr(self.id)}, name={repr(self.name)}, password={repr(self.password)})""" + else: + return f"""User(uid={repr(self.id)}, name={repr(self.name)}, hash={repr(self.hash)})""" diff --git a/pwncat/manager.py b/pwncat/manager.py index b925133..4f8a7d3 100644 --- a/pwncat/manager.py +++ b/pwncat/manager.py @@ -114,6 +114,7 @@ class Session: target.guid = self.hash # Add the target to the database + self.db.transaction_manager.begin() self.db.root.targets.append(target) self.db.transaction_manager.commit() @@ -122,19 +123,22 @@ class Session: def run(self, module: str, **kwargs): """ Run a module on this session """ - if module not in self.manager.modules: - raise pwncat.modules.ModuleNotFound(module) + module_name = module + module = self.manager.modules.get(module_name) + if module is None: + module = self.manager.modules.get(self.platform.name + "." + module_name) + if module is None: + module = self.manager.modules.get("agnostic." + module_name) + if module is None: + raise pwncat.modules.ModuleNotFound(module_name) - if ( - self.manager.modules[module].PLATFORM is not None - and type(self.platform) not in self.manager.modules[module].PLATFORM - ): - raise pwncat.modules.IncorrectPlatformError(module) + if module.PLATFORM is not None and type(self.platform) not in module.PLATFORM: + raise pwncat.modules.IncorrectPlatformError(module_name) # Ensure that our database connection is up to date - self.db.begin() + self.db.transaction_manager.begin() - return self.manager.modules[module].run(self, **kwargs) + return module.run(self, **kwargs) def find_module(self, pattern: str, base=None, exact: bool = False): """Locate a module by a glob pattern. This is an generator @@ -150,14 +154,22 @@ class Session: and type(self.platform) not in module.PLATFORM ): continue - if ( - not exact - and fnmatch.fnmatch(name, pattern) - and isinstance(module, base) - ): - yield module - elif exact and name == pattern and isinstance(module, base): - yield module + if not isinstance(module, base): + continue + if not exact: + if ( + fnmatch.fnmatch(name, pattern) + or fnmatch.fnmatch(name, f"agnostic.{pattern}") + or fnmatch.fnmatch(name, f"{self.platform.name}.{pattern}") + ): + yield module + elif exact: + if ( + name == pattern + or name == f"agnostic.{pattern}" + or name == f"{self.platform.name}.{pattern}" + ): + yield module def log(self, *args, **kwargs): """Log to the console. This utilizes the active sessions @@ -326,9 +338,8 @@ class Manager: self.db = ZODB.DB(storage, **factory_args) conn = self.db.open() - try: - conn.root.targets - except AttributeError: + + if not hasattr(conn.root, "targets"): conn.root.targets = persistent.list.PersistentList() conn.transaction_manager.commit() conn.close() diff --git a/pwncat/modules/__init__.py b/pwncat/modules/__init__.py index 889e67c..1995866 100644 --- a/pwncat/modules/__init__.py +++ b/pwncat/modules/__init__.py @@ -82,8 +82,8 @@ class Argument: def List(_type=str): - """ Argument list type, which accepts a list of the provided - type. """ + """Argument list type, which accepts a list of the provided + type.""" def _ListType(value): if isinstance(value, list): @@ -96,9 +96,9 @@ def List(_type=str): def Bool(value: str): - """ Argument of type "bool". Accepts true/false (case-insensitive) + """Argument of type "bool". Accepts true/false (case-insensitive) as well as 1/0. The presence of an argument of type "Bool" with no - assignment (e.g. run module arg) is equivalent to `run module arg=true`. """ + assignment (e.g. run module arg) is equivalent to `run module arg=true`.""" if isinstance(value, bool): return value @@ -115,27 +115,27 @@ def Bool(value: str): class Result: - """ This is a module result. Modules can return standard python objects, + """This is a module result. Modules can return standard python objects, but if they need to be formatted when displayed, each result should - implement this interface. """ + implement this interface.""" @property def category(self) -> str: - """ Return a "categry" of object. Categories will be grouped. + """Return a "categry" of object. Categories will be grouped. If this returns None or is not defined, this result will be "uncategorized" """ return None @property def title(self) -> str: - """ Return a short-form description/title of the object. If not defined, - this defaults to the object converted to a string. """ - raise NotImplementedError + """Return a short-form description/title of the object. If not defined, + this defaults to the object converted to a string.""" + return str(self) @property def description(self) -> str: - """ Returns a long-form description. If not defined, the result is assumed - to not be a long-form result. """ + """Returns a long-form description. If not defined, the result is assumed + to not be a long-form result.""" return None def is_long_form(self) -> bool: @@ -147,12 +147,9 @@ class Result: return False return True - def __str__(self) -> str: - return self.title - class Status(str): - """ A result which isn't actually returned, but simply updates + """A result which isn't actually returned, but simply updates the progress bar. It is equivalent to a string, so this is valid: ``yield Status("module status update")``""" @@ -206,8 +203,8 @@ def run_decorator(real_run): class BaseModuleMeta(type): - """ Ensures that type-checking is done on all "run" functions - of sub-classes """ + """Ensures that type-checking is done on all "run" functions + of sub-classes""" def __new__(cls, name, bases, local): if "run" in local: @@ -216,10 +213,10 @@ class BaseModuleMeta(type): class BaseModule(metaclass=BaseModuleMeta): - """ Generic module class. This class allows to easily create + """Generic module class. This class allows to easily create new modules. Any new module must inherit from this class. The run method is guaranteed to receive as key-word arguments any - arguments specified in the ``ARGUMENTS`` dictionary. """ + arguments specified in the ``ARGUMENTS`` dictionary.""" ARGUMENTS = { # "name": Argument(int, default="value"), @@ -248,7 +245,7 @@ class BaseModule(metaclass=BaseModuleMeta): self.name = None def run(self, session, progress=None, **kwargs): - """ The run method is called via keyword-arguments with all the + """The run method is called via keyword-arguments with all the parameters specified in the ``ARGUMENTS`` dictionary. If ``ALLOW_KWARGS`` was True, then other keyword arguments may also be passed. Any types specified in ``ARGUMENTS`` will already have been checked. diff --git a/pwncat/modules/agnostic/__init__.py b/pwncat/modules/agnostic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pwncat/modules/agnostic/enumerate/__init__.py b/pwncat/modules/agnostic/enumerate/__init__.py index bd28e25..4401d12 100644 --- a/pwncat/modules/agnostic/enumerate/__init__.py +++ b/pwncat/modules/agnostic/enumerate/__init__.py @@ -8,7 +8,6 @@ import time import pwncat from pwncat.platform.linux import Linux from pwncat.modules import BaseModule, Status, Argument, List -from pwncat.db import get_session class Schedule(Enum): @@ -56,90 +55,78 @@ class EnumerateModule(BaseModule): ensure enumeration modules aren't re-run. """ - marker_name = self.name - if self.SCHEDULE == Schedule.PER_USER: - marker_name += f".{session.platform.current_user().id}" + # Retrieve the DB target object + target = session.target - with session.db as db: + if clear: + # Filter out all facts which were generated by this module + target.facts = persistent.list.PersistentList( + (f for f in target.facts if f.source != self.name) + ) - if clear: - # Delete enumerated facts - session.target.facts = persistent.list.PersistentList( - (f for f in session.target.facts if f.source != self.name) + # Remove the enumeration state if available + del target.enumerate_state[self.name] + + # Commit database changes + session.db.transaction_manager.commit() + + return + + # Yield all the know facts which have already been enumerated + if types: + yield from ( + f + for f in target.facts + if f.source == self.name + and any( + any(fnmatch.fnmatch(item_type, req_type) for req_type in types) + for item_type in f.types ) + ) + else: + yield from (f for f in target.facts if f.source == self.name) - # Delete our marker - #### We aren't positive how to recreate this in ZODB yet - # if self.SCHEDULE != Schedule.ALWAYS: - # query = ( - # db.query(pwncat.db.Fact) - # .filter_by(host_id=session.host, type="marker") - # .filter(pwncat.db.Fact.source.startswith(self.name)) - # ) - # query.delete(synchronize_session=False) - return + # Check if the module is scheduled to run now + if (self.SCHEDULE == Schedule.ONCE and self.name in target.enumerate_state) or ( + self.SCHEDULE == Schedule.PER_USER + and session.platform.current_user().id in target.enumerate_state[self.name] + ): + return - # Yield all the know facts which have already been enumerated - existing_facts = (f for f in session.target.facts if f.source == self.name) - - if types: - for fact in existing_facts: - for typ in types: - if fnmatch.fnmatch(fact.type, typ): - yield fact - else: - yield from existing_facts - - if self.SCHEDULE != Schedule.ALWAYS: - exists = ( - db.query(pwncat.db.Fact.id) - .filter_by(host_id=session.host, type="marker", source=marker_name) - .scalar() - is not None - ) - if exists: - return - - # Get any new facts + # Get any new facts + try: for item in self.enumerate(session): + + # Allow non-fact status updates if isinstance(item, Status): yield item continue - typ, data = item - # session.target.facts.append(fact) - - # row = pwncat.db.Fact( - # host_id=session.host, type=typ, data=data, source=self.name - # ) - try: - db.add(row) - db.commit() - except sqlalchemy.exc.IntegrityError: - db.rollback() - yield Status(data) - continue + # Only add the item if it doesn't exist + if item not in target.facts: + target.facts.append(item) # Don't yield the actual fact if we didn't ask for this type - if types: - for typ in types: - if fnmatch.fnmatch(row.type, typ): - yield row - else: - yield Status(data) + if not types or any( + any(fnmatch.fnmatch(item_type, req_type) for req_type in types) + for item_type in item.types + ): + yield item else: - yield row + yield Status(item) - # Add the marker if needed - if self.SCHEDULE != Schedule.ALWAYS: - row = pwncat.db.Fact( - host_id=session.host, - type="marker", - source=marker_name, - data=None, + # Update state for restricted modules + if self.SCHEDULE == Schedule.ONCE: + target.enumerate_state[self.name] = True + elif self.SCHEDULE == Schedule.PER_USER: + if not self.name in target.enumerate_state: + target.enumerate_state[self.name] = persistent.list.PersistentList() + target.enumerate_state[self.name].append( + session.platform.current_user().id ) - db.add(row) - # session.db.transaction_manager.commit() + finally: + # Commit database changes + session.db.transaction_manager.commit() def enumerate(self, session): """ @@ -149,4 +136,4 @@ class EnumerateModule(BaseModule): # This makes `run enumerate` initiate a quick scan -from pwncat.modules.enumerate.quick import Module +from pwncat.modules.agnostic.enumerate.quick import Module diff --git a/pwncat/modules/agnostic/enumerate/gather.py b/pwncat/modules/agnostic/enumerate/gather.py index ebffa63..5cc174d 100644 --- a/pwncat/modules/agnostic/enumerate/gather.py +++ b/pwncat/modules/agnostic/enumerate/gather.py @@ -12,8 +12,7 @@ from rich import markup import pwncat.modules from pwncat import util from pwncat.util import console -from pwncat.modules.enumerate import EnumerateModule -from pwncat.db import get_session +from pwncat.modules.agnostic.enumerate import EnumerateModule def strip_markup(styled_text: str) -> str: diff --git a/pwncat/modules/agnostic/escalate/__init__.py b/pwncat/modules/agnostic/escalate/__init__.py index ce2cafb..750b97e 100644 --- a/pwncat/modules/agnostic/escalate/__init__.py +++ b/pwncat/modules/agnostic/escalate/__init__.py @@ -21,7 +21,7 @@ from pwncat.modules import ( ArgumentFormatError, ModuleFailed, ) -from pwncat.modules.persist import PersistType, PersistModule, PersistError +from pwncat.modules.agnostic.persist import PersistType, PersistModule, PersistError from pwncat.gtfobins import Capability from pwncat.file import RemoteBinaryPipe from pwncat.util import CompilationError @@ -34,7 +34,7 @@ class EscalateError(ModuleFailed): def fix_euid_mismatch( escalate: "EscalateModule", exit_cmd: str, target_uid: int, target_gid: int ): - """ Attempt to gain EUID=UID=target_uid. + """Attempt to gain EUID=UID=target_uid. This is intended to fix EUID/UID mismatches after a escalation. """ @@ -161,7 +161,7 @@ def euid_fix(technique_class): @dataclasses.dataclass class Technique: - """ Describes a technique possible through some module. + """Describes a technique possible through some module. Modules should subclass this class in order to implement their techniques. Only the methods corresponding to the @@ -176,7 +176,7 @@ class Technique: """ The module which provides these capabilities """ def write(self, filepath: str, data: bytes): - """ Write the given data to the specified file as another user. + """Write the given data to the specified file as another user. :param filepath: path to the target file :type filepath: str @@ -186,7 +186,7 @@ class Technique: raise NotImplementedError def read(self, filepath: str): - """ Read the given file as the specified user + """Read the given file as the specified user :param filepath: path to the target file :type filepath: str @@ -196,7 +196,7 @@ class Technique: raise NotImplementedError def exec(self, binary: str): - """ Execute a shell as the specified user. + """Execute a shell as the specified user. :param binary: the shell to execute :type binary: str @@ -220,7 +220,7 @@ class Technique: class GTFOTechnique(Technique): - """ A technique which is based on a GTFO binary capability. + """A technique which is based on a GTFO binary capability. This is mainly used for sudo and setuid techniques, but could theoretically be used for other techniques. @@ -314,11 +314,11 @@ class GTFOTechnique(Technique): @dataclasses.dataclass class FileContentsResult(Result): - """ Result which contains the contents of a file. This is the + """Result which contains the contents of a file. This is the result returned from an ``EscalateModule`` when the ``read`` parameter is true. It allows for the file to be used as a stream programmatically, and also nicely formats the file data - if run from the prompt. """ + if run from the prompt.""" filepath: str """ Path to the file which this data came from """ @@ -369,7 +369,7 @@ class FileContentsResult(Result): @dataclasses.dataclass class EscalateChain(Result): - """ Chain of techniques used to escalate. When escalating + """Chain of techniques used to escalate. When escalating through multiple users, this allows ``pwncat`` to easily track the different techniques and users that were traversed. When ``exec`` is used, this object is returned instead of @@ -408,8 +408,8 @@ class EscalateChain(Result): self.chain.append((technique, exit_cmd)) def extend(self, chain: "EscalateChain"): - """ Extend this chain with another chain. The two chains - are concatenated. """ + """Extend this chain with another chain. The two chains + are concatenated.""" self.chain.extend(chain.chain) def pop(self): @@ -420,7 +420,7 @@ class EscalateChain(Result): pwncat.victim.update_user() def unwrap(self): - """ Exit each shell in the chain with the provided exit script. + """Exit each shell in the chain with the provided exit script. This should return the state of the remote shell to prior to escalation.""" @@ -434,7 +434,7 @@ class EscalateChain(Result): class EscalateResult(Result): - """ The result of running an escalate module. This object contains + """The result of running an escalate module. This object contains all the enumerated techniques and provides an abstract way to employ the techniques to attempt privilege escalation. This is the meat and bones of the automatic escalation logic, and shouldn't generally need @@ -458,7 +458,7 @@ class EscalateResult(Result): @property def category(self): - """ EscalateResults are uncategorized + """EscalateResults are uncategorized :meta private: """ @@ -466,7 +466,7 @@ class EscalateResult(Result): @property def title(self): - """ The title of the section when displayed on the terminal + """The title of the section when displayed on the terminal :meta private: """ @@ -474,7 +474,7 @@ class EscalateResult(Result): @property def description(self): - """ Description of these results (list of techniques) + """Description of these results (list of techniques) :meta private: """ @@ -487,9 +487,9 @@ class EscalateResult(Result): return "\n".join(result) def extend(self, result: "EscalateResult"): - """ Extend this result with another escalation enumeration result. + """Extend this result with another escalation enumeration result. This allows you to enumerate multiple modules and utilize all their - techniques together to perform escalation. """ + techniques together to perform escalation.""" for key, value in result.techniques.items(): if key not in self.techniques: @@ -562,7 +562,7 @@ class EscalateResult(Result): return exit_cmd.chain[0][0] def read(self, user: str, filepath: str, progress, no_exec: bool = False): - """ Attempt to use all the techniques enumerated to read a file + """Attempt to use all the techniques enumerated to read a file as the given user. This method returns a file-like object capable of reading the file. @@ -599,7 +599,10 @@ class EscalateResult(Result): # We are now running in a shell as this user, just write the file try: - filp = pwncat.victim.open(filepath, "r",) + filp = pwncat.victim.open( + filepath, + "r", + ) # Our exit command needs to be run as well when the file is # closed original_close = filp.close @@ -701,7 +704,7 @@ class EscalateResult(Result): def _write_authorized_key( self, user: str, pubkey: str, authkeys: List[str], authkeys_path: str, progress ): - """ Attempt to Write the given public key to the user's authorized + """Attempt to Write the given public key to the user's authorized keys file. Return True if successful, otherwise return False. The authorized keys file will be overwritten with the contents of the given @@ -722,7 +725,7 @@ class EscalateResult(Result): return technique def exec(self, user: str, shell: str, progress): - """ Attempt to use all the techniques enumerated to execute a + """Attempt to use all the techniques enumerated to execute a shell as the specified user. :param user: The user to execute a shell as @@ -934,7 +937,7 @@ class EscalateResult(Result): class EscalateModule(BaseModule): - """ The base module for all escalation modules. This module + """The base module for all escalation modules. This module is responsible for enumerating ``Technique`` objects which can be used to attempt various escalation actions. @@ -978,7 +981,7 @@ class EscalateModule(BaseModule): escalation. Lower values execute first. """ def run(self, user, exec, read, write, shell, path, data, **kwargs): - """ This method is not overriden by subclasses. Subclasses should + """This method is not overriden by subclasses. Subclasses should should implement the ``enumerate`` method which yields techniques. Running a module results in an EnumerateResult object which can be @@ -1021,7 +1024,7 @@ class EscalateModule(BaseModule): yield result def enumerate(self, **kwargs) -> "Generator[Technique, None, None]": - """ Enumerate techniques for this module. Each technique must + """Enumerate techniques for this module. Each technique must implement at least one capability, and all techniques will be used together to escalate privileges. Any custom arguments are passed to this method through keyword arguments. None of diff --git a/pwncat/modules/agnostic/escalate/auto.py b/pwncat/modules/agnostic/escalate/auto.py index c058a1d..976fb16 100644 --- a/pwncat/modules/agnostic/escalate/auto.py +++ b/pwncat/modules/agnostic/escalate/auto.py @@ -9,7 +9,7 @@ from pwncat.modules import ( ArgumentFormatError, MissingArgument, ) -from pwncat.modules.escalate import ( +from pwncat.modules.agnostic.escalate import ( EscalateChain, EscalateResult, EscalateModule, diff --git a/pwncat/modules/agnostic/persist/__init__.py b/pwncat/modules/agnostic/persist/__init__.py index 0acd84c..8ca2b9a 100644 --- a/pwncat/modules/agnostic/persist/__init__.py +++ b/pwncat/modules/agnostic/persist/__init__.py @@ -16,41 +16,10 @@ from pwncat.modules import ( PersistType, ArgumentFormatError, ) -from pwncat.db import get_session -def host_type(ident: str) -> pwncat.db.Host: - if isinstance(ident, pwncat.db.Host): - return ident - - if ident is None and pwncat.victim is None: - raise ArgumentFormatError("invalid host") - elif ident is None: - return pwncat.victim.host - - try: - host = get_session().query(pwncat.db.Host).filter_by(id=int(ident)).first() - except ValueError: - host = None - - if host is None: - host = get_session().query(pwncat.db.Host).filter_by(ip=ident).first() - - if host is None: - try: - host = ( - get_session() - .query(pwncat.db.Host) - .filter_by(ip=socket.gethostbyname(ident)) - .first() - ) - except socket.gaierror: - host = None - - if host is None: - raise ArgumentFormatError("invalid host") - - return host +def host_type(ident: str): + return ident class PersistModule(BaseModule): @@ -120,9 +89,9 @@ class PersistModule(BaseModule): ].help = "Ignored for install/remove. Defaults to root for escalate." def run(self, remove, escalate, connect, host, **kwargs): - """ This method should not be overriden by subclasses. It handles all logic + """This method should not be overriden by subclasses. It handles all logic for installation, escalation, connection, and removal. The standard interface - of this method allows abstract interactions across all persistence modules. """ + of this method allows abstract interactions across all persistence modules.""" if "user" not in kwargs: raise RuntimeError(f"{self.__class__} must take a user argument") diff --git a/pwncat/modules/linux/__init__.py b/pwncat/modules/linux/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pwncat/modules/linux/enumerate/creds/pam.py b/pwncat/modules/linux/enumerate/creds/pam.py index 0aa1cbf..19cd00c 100644 --- a/pwncat/modules/linux/enumerate/creds/pam.py +++ b/pwncat/modules/linux/enumerate/creds/pam.py @@ -2,9 +2,9 @@ import pwncat from pwncat.platform.linux import Linux -from pwncat.modules.enumerate import EnumerateModule, Schedule -from pwncat.modules.enumerate.creds import PasswordData -from pwncat.modules.persist.gather import InstalledModule +from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule +from pwncat.modules.linux.enumerate.creds import PasswordData +from pwncat.modules.linux.persist.gather import InstalledModule class Module(EnumerateModule): diff --git a/pwncat/modules/linux/enumerate/creds/password.py b/pwncat/modules/linux/enumerate/creds/password.py index 8be31a7..8ec9f20 100644 --- a/pwncat/modules/linux/enumerate/creds/password.py +++ b/pwncat/modules/linux/enumerate/creds/password.py @@ -4,8 +4,8 @@ import re import pwncat from pwncat.platform.linux import Linux -from pwncat.modules.enumerate import EnumerateModule, Schedule -from pwncat.modules.enumerate.creds import PasswordData +from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule +from pwncat.modules.linux.enumerate.creds import PasswordData class Module(EnumerateModule): diff --git a/pwncat/modules/linux/enumerate/creds/private_key.py b/pwncat/modules/linux/enumerate/creds/private_key.py index af7e9d5..d606672 100644 --- a/pwncat/modules/linux/enumerate/creds/private_key.py +++ b/pwncat/modules/linux/enumerate/creds/private_key.py @@ -5,8 +5,8 @@ import time import pwncat from pwncat.platform.linux import Linux from pwncat.modules import Status -from pwncat.modules.enumerate import EnumerateModule, Schedule -from pwncat.modules.enumerate.creds import PrivateKeyData +from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule +from pwncat.modules.linux.enumerate.creds import PrivateKeyData class Module(EnumerateModule): diff --git a/pwncat/modules/linux/enumerate/file/caps.py b/pwncat/modules/linux/enumerate/file/caps.py index 4e23749..7754b56 100644 --- a/pwncat/modules/linux/enumerate/file/caps.py +++ b/pwncat/modules/linux/enumerate/file/caps.py @@ -5,7 +5,7 @@ import dataclasses import pwncat from pwncat.platform.linux import Linux from pwncat import util -from pwncat.modules.enumerate import EnumerateModule, Schedule +from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule @dataclasses.dataclass diff --git a/pwncat/modules/linux/enumerate/file/suid.py b/pwncat/modules/linux/enumerate/file/suid.py index 9523564..dae1527 100644 --- a/pwncat/modules/linux/enumerate/file/suid.py +++ b/pwncat/modules/linux/enumerate/file/suid.py @@ -6,7 +6,7 @@ import pwncat from pwncat.platform.linux import Linux from pwncat import util from pwncat.modules import Status -from pwncat.modules.enumerate import EnumerateModule, Schedule +from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule @dataclasses.dataclass diff --git a/pwncat/modules/linux/enumerate/misc/writable_path.py b/pwncat/modules/linux/enumerate/misc/writable_path.py index 9d38ec7..20069e4 100644 --- a/pwncat/modules/linux/enumerate/misc/writable_path.py +++ b/pwncat/modules/linux/enumerate/misc/writable_path.py @@ -5,7 +5,7 @@ import stat import pwncat from pwncat.util import Access from pwncat.platform.linux import Linux -from pwncat.modules.enumerate import EnumerateModule, Schedule +from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule class Module(EnumerateModule): diff --git a/pwncat/modules/linux/enumerate/software/cron.py b/pwncat/modules/linux/enumerate/software/cron.py index ba41e44..a0d6536 100644 --- a/pwncat/modules/linux/enumerate/software/cron.py +++ b/pwncat/modules/linux/enumerate/software/cron.py @@ -6,7 +6,7 @@ import re import pwncat from pwncat.platform.linux import Linux from pwncat.modules import Status -from pwncat.modules.enumerate import EnumerateModule, Schedule +from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule @dataclasses.dataclass diff --git a/pwncat/modules/linux/enumerate/software/screen.py b/pwncat/modules/linux/enumerate/software/screen.py index 32fe698..49c432f 100644 --- a/pwncat/modules/linux/enumerate/software/screen.py +++ b/pwncat/modules/linux/enumerate/software/screen.py @@ -5,7 +5,7 @@ import re import shlex import pwncat -from pwncat.modules.enumerate import EnumerateModule, Schedule +from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule from pwncat.platform.linux import Linux diff --git a/pwncat/modules/linux/enumerate/software/sudo/rules.py b/pwncat/modules/linux/enumerate/software/sudo/rules.py index 3784eeb..1297fbc 100644 --- a/pwncat/modules/linux/enumerate/software/sudo/rules.py +++ b/pwncat/modules/linux/enumerate/software/sudo/rules.py @@ -6,7 +6,7 @@ from typing import Generator, Optional, List import pwncat from pwncat.platform.linux import Linux from pwncat import util -from pwncat.modules.enumerate import EnumerateModule, Schedule +from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule per_user = True sudo_pattern = re.compile( @@ -129,14 +129,23 @@ def LineParser(line): commands = re.split(r"""(? Optional[int]: + """Check if the child process has terminated. Set and return + ``returncode`` attribute. Otherwise, returns None.""" + + def wait(self, timeout: float = None) -> int: + """Wait for child process to terminate. Set and return + ``returncode`` attribute. + + If the process does not terminate after ``timeout`` seconds, + raise a ``TimeoutExpired`` exception. It is safe to catch + this exception and retry the wait. + """ + + def communicate(self, input: bytes = None, timeout: float = None): + """Interact with process: Send data to stdin. Read data from stdout + and stderr, until end-of-file is readched. Wait for the process to + terminate and set the ``returncode`` attribute. The optional ``input`` + argument should be data to be sent to the child process, or None, if + no data should be sent to the child. If streams were opened in text mode, + ``input`` must be a string. Otherwise, it must be ``bytes``.""" + + def send_signal(self, signal: int): + """Sends the signal ``signal`` to the child. + + Does nothing if the process completed. + """ + + def terminate(self): + """ Stop the child. """ + + def kill(self): + """ Kills the child """ diff --git a/pwncat/tamper.py b/pwncat/tamper.py index e1cd7ba..50818ac 100644 --- a/pwncat/tamper.py +++ b/pwncat/tamper.py @@ -6,7 +6,8 @@ from colorama import Fore import pwncat from pwncat.util import Access -from pwncat.db import get_session + +# from pwncat.db import get_session class Action(Enum): @@ -47,10 +48,10 @@ class CreatedFile(Tamper): class ModifiedFile(Tamper): - """ File modification tamper. This tamper needs either a specific line which + """File modification tamper. This tamper needs either a specific line which should be removed from a text file, or the original original_content as bytes which will be replaced. If neither is provided, we will track the modification but be unable - to revert it. """ + to revert it.""" def __init__( self, path: str, added_lines: List[str] = None, original_content: bytes = None @@ -113,12 +114,12 @@ class LambdaTamper(Tamper): class TamperManager: - """ TamperManager not only provides some automated ability to tamper with + """TamperManager not only provides some automated ability to tamper with properties of the remote system, but also a tracker for all modifications on the remote system with the ability to remove previous changes. Other modules can register system changes with `PtyHandler.tamper` in order to allow the user to get a wholistic view of all modifications of the remote system, and - attempt revert all modifications automatically. """ + attempt revert all modifications automatically.""" def __init__(self): # List of tampers registered with this manager @@ -173,8 +174,8 @@ class TamperManager: 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. """ + """Pop a tamper from the list of known tampers. This does not revert the tamper. + It removes the tracking for this tamper.""" tracker = ( get_session().query(pwncat.db.Tamper).filter_by(name=str(tamper)).first() diff --git a/pwncat/target.py b/pwncat/target.py index c6d0f59..78fff7c 100644 --- a/pwncat/target.py +++ b/pwncat/target.py @@ -4,7 +4,7 @@ import enum import persistent import persistent.list -from BTrees.OOBTree import TreeSet +from BTrees.OOBTree import TreeSet, OOBTree class NAT(enum.Enum): @@ -75,6 +75,8 @@ class Target(persistent.Persistent): """ Target host operating system """ self.facts: persistent.list.PersistentList = persistent.list.PersistentList() """ List of enumerated facts about the target host """ + self.enumerate_state: OOBTree = OOBTree() + """ The state of all enumeration modules which drives the module schedule """ self.tampers: persistent.list.PersistentList = persistent.list.PersistentList() """ List of files/properties of the target that have been modified and/or created. """ self.users: persistent.list.PersistentList = persistent.list.PersistentList() diff --git a/pwncat/util.py b/pwncat/util.py index 8d2f94f..af28dc7 100644 --- a/pwncat/util.py +++ b/pwncat/util.py @@ -23,7 +23,7 @@ import os from rich.console import Console -console = Console() +console = Console(emoji=False) CTRL_C = b"\x03" diff --git a/pwncatrc b/pwncatrc new file mode 120000 index 0000000..1db0308 --- /dev/null +++ b/pwncatrc @@ -0,0 +1 @@ +./data/pwncatrc \ No newline at end of file