diff --git a/docs/source/installation.rst b/docs/source/installation.rst index bb86847..7c43dda 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -18,7 +18,6 @@ It is recommended to use a virtual environment, however. This can be done easily python -m venv env source env/bin/activate - pip install -r requirements.txt python setup.py install When updating ``pwncat`` is it recommended to setup and update the virtual environment again. diff --git a/pwncat/commands/__init__.py b/pwncat/commands/__init__.py index 49da94f..23d80ca 100644 --- a/pwncat/commands/__init__.py +++ b/pwncat/commands/__init__.py @@ -163,20 +163,6 @@ class CommandParser: complete_while_typing=False, history=history, ) - self.toolbar = PromptSession( - [ - ("fg:ansiyellow bold", "(local) "), - ("fg:ansimagenta bold", "pwncat"), - ("", "$ "), - ], - completer=completer, - lexer=lexer, - style=style, - auto_suggest=auto_suggest, - complete_while_typing=False, - prompt_in_toolbar=True, - history=history, - ) @property def loaded(self): @@ -209,7 +195,7 @@ class CommandParser: def run_single(self): try: - line = self.toolbar.prompt().strip() + line = self.prompt.prompt().strip() except (EOFError, OSError, KeyboardInterrupt): pass else: diff --git a/pwncat/commands/base.py b/pwncat/commands/base.py index 1d02d24..fd55c10 100644 --- a/pwncat/commands/base.py +++ b/pwncat/commands/base.py @@ -61,6 +61,28 @@ def StoreForAction(action: List[str]) -> Callable: return StoreFor +def StoreConstForAction(action: List[str]) -> Callable: + """ Generates a custom argparse Action subclass which verifies that the current + selected "action" option is one of the provided actions in this function. If + not, an error is raised. This stores the constant `const` to the `dest` argument. + This is comparable to `store_const`, but checks that you have selected one of + the specified actions. """ + + class StoreFor(argparse.Action): + """ Store the value if the currently selected action matches the list of + actions passed to this function. """ + + def __call__(self, parser, namespace, values, option_string=None): + if getattr(namespace, "action", None) not in action: + raise argparse.ArgumentError( + self, f"{option_string}: only valid for {action}", + ) + + setattr(namespace, self.dest, self.const) + + return StoreFor + + def RemoteFileType(file_exist=True, directory_exist=False): def _type(command: "CommandDefinition", name: str): """ Ensures that the remote file named exists. This should only be used for diff --git a/pwncat/commands/enumerate.py b/pwncat/commands/enumerate.py index 71f653a..bc6fe5e 100644 --- a/pwncat/commands/enumerate.py +++ b/pwncat/commands/enumerate.py @@ -15,6 +15,8 @@ from pwncat.commands.base import ( Parameter, StoreConstOnce, Group, + StoreForAction, + StoreConstForAction, ) @@ -34,16 +36,14 @@ class Command(CommandDefinition): """ Interface with the underlying enumeration module. This provides methods for enumerating, viewing and clearing cached facts about the victim. - Types of enumeration data include the following options: - - * all - all known enumeration techniques - * common - common useful information - * suid - Set UID binaries on the remote host - * passwords - Known passwords for remote users - * keys - Known private keys found on the remote host - - Other enumeration data may be available which was dynamically registered by - other ``pwncat`` modules. + There are various types of enumeration data which can be collected by + pwncat. Some enumeration data is provided by "enumerator" modules which + will be automatically run if you request a type which they provide. On + the other hand, some enumeration is performed as a side-effect of other + operations (normally a privilege escalation). This data is only stored + when it is found organically. To find out what types are available, you + should use the tab-completion at the local prompt. Some shortcuts are + provided with the "enumeration groups" options below. """ @@ -68,7 +68,14 @@ class Command(CommandDefinition): "action": Group( title="enumeration actions", description="Exactly one action must be chosen from the below list.", - ) + ), + "groups": Group( + title="enumeration groups", + description=( + "common enumeration groups; these put together various " + "groups of enumeration types which may be useful" + ), + ), } ARGS = { "--show,-s": Parameter( @@ -78,12 +85,12 @@ class Command(CommandDefinition): dest="action", const="show", group="action", - help="Find and display all facts of the given type", + help="Find and display all facts of the given type and provider", ), "--long,-l": Parameter( Complete.NONE, action="store_true", - help="Show long description of enumeration results", + help="Show long description of enumeration results (only valid for --show)", ), "--no-enumerate,-n": Parameter( Complete.NONE, @@ -92,9 +99,11 @@ class Command(CommandDefinition): ), "--type,-t": Parameter( Complete.CHOICES, + action=StoreForAction(["show", "flush"]), + nargs=1, choices=get_fact_types, metavar="TYPE", - help="The type of enumeration data to query", + help="The type of enumeration data to query (only valid for --show/--flush)", ), "--flush,-f": Parameter( Complete.NONE, @@ -103,10 +112,17 @@ class Command(CommandDefinition): nargs=0, dest="action", const="flush", - help="Flush the queried enumeration data from the database", + help=( + "Flush the queried enumeration data from the database. " + "This only flushed the data specified by the --type and " + "--provider options. If no type or provider or specified, " + "all data is flushed" + ), ), "--provider,-p": Parameter( Complete.CHOICES, + action=StoreForAction(["show", "flush"]), + nargs=1, choices=get_provider_names, metavar="PROVIDER", help="The enumeration provider to filter by", @@ -116,7 +132,36 @@ class Command(CommandDefinition): group="action", action=ReportAction, nargs=1, - help="Generate an enumeration report containing the specified enumeration data", + help=( + "Generate an enumeration report containing all enumeration " + "data pwncat is capable of generating in a Markdown format." + ), + ), + "--quick,-q": Parameter( + Complete.NONE, + action=StoreConstForAction(["show"]), + dest="type", + const=[ + "system.hostname", + "system.arch", + "system.distro", + "system.kernel.version", + "system.kernel.exploit", + "system.network.hosts", + "system.network", + ], + nargs=0, + help="Activate the set of 'quick' enumeration types", + group="groups", + ), + "--all,-a": Parameter( + Complete.NONE, + action=StoreConstForAction(["show"]), + dest="type", + const=None, + nargs=0, + help="Activate all enumeration types (this is the default)", + group="groups", ), } DEFAULTS = {"action": "help"} @@ -128,22 +173,22 @@ class Command(CommandDefinition): self.parser.print_help() return - # if not args.type: - # args.type = "all" - if args.action == "show": self.show_facts(args.type, args.provider, args.long) elif args.action == "flush": self.flush_facts(args.type, args.provider) elif args.action == "report": - self.generate_report(args.report, args.type, args.provider) + self.generate_report(args.report) - def generate_report(self, report_path: str, typ: str, provider: str): - """ Generate a markdown report of enumeration data for the remote host """ + def generate_report(self, report_path: str): + """ Generate a markdown report of enumeration data for the remote host. This + report is generated from all facts which pwncat is capable of enumerating. + It does not need nor honor the type or provider options. """ + # Dictionary mapping type names to facts. Each type name is mapped + # to a dictionary which maps sources to a list of facts. This makes + # organizing the output report easier. report_data: Dict[str, Dict[str, List[pwncat.db.Fact]]] = {} - hostname = "" - system_details = [] try: @@ -202,7 +247,8 @@ class Command(CommandDefinition): table_writer.value_matrix = system_details table_writer.margin = 1 - # Note enumeration data we don't need anymore + # Note enumeration data we don't need anymore. These are handled above + # in the system_details table which is output with the table_writer. ignore_types = [ "system.hostname", "system.kernel.version", @@ -234,9 +280,7 @@ class Command(CommandDefinition): ] util.progress("enumerating report_data") - for fact in pwncat.victim.enumerate.iter( - typ, filter=lambda f: provider is None or f.source == provider - ): + for fact in pwncat.victim.enumerate.iter(): util.progress(f"enumerating report_data: {fact.data}") if fact.type in ignore_types: continue @@ -308,16 +352,22 @@ class Command(CommandDefinition): facts: Dict[str, Dict[str, List[pwncat.db.Fact]]] = {} + if isinstance(typ, list): + types = typ + else: + types = [typ] + util.progress("enumerating facts") - for fact in pwncat.victim.enumerate.iter( - typ, filter=lambda f: provider is None or f.source == provider - ): - util.progress(f"enumerating facts: {fact.data}") - if fact.type not in facts: - facts[fact.type] = {} - if fact.source not in facts[fact.type]: - facts[fact.type][fact.source] = [] - facts[fact.type][fact.source].append(fact) + for typ in types: + for fact in pwncat.victim.enumerate.iter( + typ, filter=lambda f: provider is None or f.source == provider + ): + util.progress(f"enumerating facts: {fact.data}") + if fact.type not in facts: + facts[fact.type] = {} + if fact.source not in facts[fact.type]: + facts[fact.type][fact.source] = [] + facts[fact.type][fact.source].append(fact) util.erase_progress() diff --git a/pwncat/enumerate/system/init.py b/pwncat/enumerate/system/init.py index cb5e30d..1fa0461 100644 --- a/pwncat/enumerate/system/init.py +++ b/pwncat/enumerate/system/init.py @@ -33,6 +33,9 @@ def enumerate() -> Generator[FactData, None, None]: :return: """ + init = util.Init.UNKNOWN + version = None + # Try to get the command name of the running init process try: with pwncat.victim.open("/proc/1/comm", "r") as filp: @@ -62,16 +65,13 @@ def enumerate() -> Generator[FactData, None, None]: elif "upstart" in comm.lower(): init = util.Init.UPSTART - try: - with pwncat.victim.subprocess(f"{comm} --version", "r") as filp: - version = filp.read().decode("utf-8").strip() - if "systemd" in version.lower(): - init = util.Init.SYSTEMD - elif "sysv" in version.lower(): - init = util.Init.SYSV - elif "upstart" in version.lower(): - init = util.Init.UPSTART - except: - version = "" + with pwncat.victim.subprocess(f"{comm} --version", "r") as filp: + version = filp.read().decode("utf-8").strip() + if "systemd" in version.lower(): + init = util.Init.SYSTEMD + elif "sysv" in version.lower(): + init = util.Init.SYSV + elif "upstart" in version.lower(): + init = util.Init.UPSTART yield InitSystemData(init, version) diff --git a/pwncat/util.py b/pwncat/util.py index 233e884..338056f 100644 --- a/pwncat/util.py +++ b/pwncat/util.py @@ -56,6 +56,7 @@ class Access(Flag): class Init(Enum): + UNKNOWN = auto() SYSTEMD = auto() UPSTART = auto() SYSV = auto() diff --git a/requirements.txt b/requirements.txt index f71f9e7..0b72ebc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ colorama==0.4.3 -git+https://github.com/calebstewart/python-prompt-toolkit +prompt-toolkit git+https://github.com/calebstewart/paramiko wcwidth==0.1.9 netifaces==0.10.9 diff --git a/setup.py b/setup.py index 579bd28..d8df257 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,6 @@ dependencies = [ ] dependency_links = [ - "https://github.com/calebstewart/python-prompt-toolkit/tarball/master#egg=prompt-toolkit", "https://github.com/calebstewart/paramiko/tarball/master#egg=paramiko", "https://github.com/JohnHammond/base64io-python/tarball/master#egg=base64io", ]