From 396800261d397a80e509f6c654bfef82bae9a684 Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Tue, 11 May 2021 18:09:05 -0400 Subject: [PATCH] Added initial escalate implementation Also added leave command to unwrap subshells after escalation --- pwncat/commands/escalate.py | 121 ++++++++++++++++++ pwncat/commands/leave.py | 37 ++++++ pwncat/facts/ability.py | 23 ++-- pwncat/manager.py | 33 +++-- .../linux/enumerate/escalate/append_passwd.py | 5 +- 5 files changed, 192 insertions(+), 27 deletions(-) create mode 100644 pwncat/commands/escalate.py create mode 100644 pwncat/commands/leave.py diff --git a/pwncat/commands/escalate.py b/pwncat/commands/escalate.py new file mode 100644 index 0000000..1d342fc --- /dev/null +++ b/pwncat/commands/escalate.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 + +from pwncat.util import console +from pwncat.modules import ModuleFailed +from pwncat.commands.base import Complete, Parameter, CommandDefinition + + +def get_user_choices(command: CommandDefinition): + if command.manager.target is None: + return + + yield from ( + user.name + for user in command.manager.target.run( + "enumerate", progress=False, types=["user"] + ) + ) + + +class Command(CommandDefinition): + """ + Attempt privilege escalation in the current session. This command + may initiate new sessions along the way to attain the privileges of + the requested user. + + The list command is simply a wrapper around enumerating "escalation.*". + This makes the escalation workflow more straightforward, but is not + required.""" + + PROG = "escalate" + ARGS = { + "command": Parameter( + Complete.CHOICES, metavar="COMMAND", choices=["list", "run"] + ), + "--user,-u": Parameter( + Complete.CHOICES, metavar="USERNAME", choices=get_user_choices + ), + } + + def run(self, manager: "pwncat.manager.Manager", args): + + if args.command == "help": + self.parser.print_usage() + return + + if args.user: + args.user = manager.target.find_user(name=args.user) + else: + # NOTE: this should find admin regardless of platform + args.user = manager.target.find_user(name="root") + + if args.command == "list": + self.list_abilities(manager, args) + elif args.command == "run": + with manager.target.task( + f"escalating to [cyan]{args.user.name}[/cyan]" + ) as task: + self.do_escalate(manager, task, args.user) + + def list_abilities(self, manager, args): + """This is just a wrapper for `run enumerate types=escalate.*`, but + it makes the workflow for escalation more apparent.""" + + found = False + + for escalation in manager.target.run("enumerate", types=["escalate.*"]): + if args.user and args.user.id != escalation.uid: + continue + console.print(f"- {escalation.title(manager.target)}") + found = True + + if not found and args.user: + console.log( + f"[yellow]warning[/yellow]: no escalations for {args.user.name}" + ) + elif not found: + console.log("[yellow]warning[/yellow]: no escalations found") + + def do_escalate(self, manager: "pwncat.manager.Manager", task, user, attempted=[]): + """ Execute escalations until we find one that works """ + + # Find escalations for users that weren't attempted already + escalations = [ + e + for e in list(manager.target.run("enumerate", types=["escalate.*"])) + if e.uid not in attempted + ] + + # Attempt escalation directly to the target user if possible + for escalation in (e for e in escalations if e.uid == user.id): + try: + manager.target.update_task( + task, status=f"attempting {escalation.title(manager.target)}" + ) + result = escalation.escalate(manager.target) + manager.target.layers.append(result) + + manager.target.log( + f"escalation to {user.name} [green]successful[/green]!" + ) + return result + except ModuleFailed: + pass + + # Attempt escalation to a different user and recurse + for escalation in (e for e in escalation if e.uid != user.id): + try: + manager.target.update_task( + task, status=f"attempting {escalation.title(manager.target)}" + ) + result = escalation.escalate(manager.target) + manager.target.layers.append(result) + + try: + self.do_escalate(manager, task, user, attempted + [escalation.uid]) + except ModuleFailed: + manager.target.layers.pop()(manager.target) + except ModuleFailed: + pass + + manager.target.log("[yellow]warning[/yellow]: no working escalations found") diff --git a/pwncat/commands/leave.py b/pwncat/commands/leave.py new file mode 100644 index 0000000..f675b29 --- /dev/null +++ b/pwncat/commands/leave.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 + +from pwncat.commands.base import Complete, Parameter, CommandDefinition + + +class Command(CommandDefinition): + """ + Leave a layer of execution from this session. Layers are normally added + as sub-shells from escalation modules. + """ + + PROG = "leave" + ARGS = { + "count": Parameter( + Complete.NONE, + type=int, + default=1, + nargs="?", + help="number of layers to remove (default: 1)", + ), + "--all,-a": Parameter( + Complete.NONE, + action="store_true", + help="leave all active layers", + ), + } + + def run(self, manager: "pwncat.manager.Manager", args): + + try: + if args.all: + args.count = len(manager.target.layers) + + for i in range(args.count): + manager.target.layers.pop()(manager.target) + except IndexError: + manager.target.log("[yellow]warning[/yellow]: no more layers to leave") diff --git a/pwncat/facts/ability.py b/pwncat/facts/ability.py index af2827a..d8ba094 100644 --- a/pwncat/facts/ability.py +++ b/pwncat/facts/ability.py @@ -7,11 +7,9 @@ from io import TextIOWrapper import pwncat.subprocess from pwncat.gtfobins import Stream, Capability from pwncat.platform.linux import LinuxReader, LinuxWriter -from pwncat.modules.agnostic.enumerate.ability import ( - ExecuteAbility, - FileReadAbility, - FileWriteAbility, -) +from pwncat.modules.agnostic.enumerate.ability import (ExecuteAbility, + FileReadAbility, + FileWriteAbility) class GTFOFileRead(FileReadAbility): @@ -74,8 +72,9 @@ class GTFOFileRead(FileReadAbility): return raw_reader - def __str__(self): - return f"file read as UID:{self.uid} via {self.method.binary_path}" + def title(self, session): + user = session.find_user(uid=self.uid) + return f"file read as [blue]{user.name}[/blue] via [cyan]{self.method.binary_path}[/cyan]" class GTFOFileWrite(FileWriteAbility): @@ -138,8 +137,9 @@ class GTFOFileWrite(FileWriteAbility): return raw_writer - def __str__(self): - return f"file write as UID:{self.uid} via {self.method.binary_path}" + def title(self, session): + user = session.find_user(uid=self.uid) + return f"file write as [blue]{user.name}[/blue] via [cyan]{self.method.binary_path}[/cyan]" class GTFOExecute(ExecuteAbility): @@ -201,5 +201,6 @@ class GTFOExecute(ExecuteAbility): self.send_command(session, shell.encode("utf-8") + b"\n") - def __str__(self): - return f"execution as UID:{self.uid} via {self.method.binary_path}" + def title(self, session): + user = session.find_user(uid=self.uid) + return f"shell as [blue]{user.name}[/blue] via [cyan]{self.method.binary_path}[/cyan]" diff --git a/pwncat/manager.py b/pwncat/manager.py index 08664b9..6c77988 100644 --- a/pwncat/manager.py +++ b/pwncat/manager.py @@ -49,16 +49,9 @@ class Session: self.db = manager.db.open() self.module_depth = 0 self.showing_progress = True + self.layers = [] - self._progress = rich.progress.Progress( - "{task.fields[platform]}", - "•", - "{task.description}", - "•", - "{task.fields[status]}", - transient=True, - console=console, - ) + self._progress = None # If necessary, build a new platform object if isinstance(platform, Platform): @@ -207,7 +200,7 @@ class Session: # Ensure the variable exists even if an exception happens # prior to task creation task = None - started = self._progress._started + started = self._progress is not None # ._started if "status" not in kwargs: kwargs["status"] = "..." @@ -217,7 +210,16 @@ class Session: try: # Ensure this bar is started if we are the selected # target. - if self.manager.target == self: + if self.manager.target == self and not started: + self._progress = rich.progress.Progress( + "{task.fields[platform]}", + "•", + "{task.description}", + "•", + "{task.fields[status]}", + transient=True, + console=console, + ) self._progress.start() # Create the new task @@ -232,6 +234,7 @@ class Session: # nested tasks. if not started: self._progress.stop() + self._progress = None def update_task(self, task, *args, **kwargs): """Update an active task""" @@ -251,6 +254,10 @@ class Session: def close(self): """Close the session and remove from manager tracking""" + # Unwrap all layers in the session + while self.layers: + self.layers.pop()(self) + self.platform.channel.close() self.died() @@ -276,8 +283,6 @@ class Manager: self.config = Config() self.sessions: List[Session] = [] self.modules: Dict[str, pwncat.modules.BaseModule] = {} - # self.engine = None - # self.SessionBuilder = None self._target = None self.parser = CommandParser(self) self.interactive_running = False @@ -414,7 +419,7 @@ class Manager: def log(self, *args, **kwargs): """Output a log entry""" - if self.target is not None: + if self.target is not None and self.target._progress is not None: self.target._progress.log(*args, **kwargs) else: console.log(*args, **kwargs) diff --git a/pwncat/modules/linux/enumerate/escalate/append_passwd.py b/pwncat/modules/linux/enumerate/escalate/append_passwd.py index 4e64065..ef111eb 100644 --- a/pwncat/modules/linux/enumerate/escalate/append_passwd.py +++ b/pwncat/modules/linux/enumerate/escalate/append_passwd.py @@ -53,11 +53,12 @@ class AppendPasswd(EscalationReplace): try: session.platform.su(backdoor_user, password=backdoor_pass) + return lambda session: session.platform.channel.send(b"exit\n") except PermissionError: raise ModuleFailed("added user, but switch user failed") - def __str__(self): - return f"""add user via [blue]file write[/blue] as [red]root[/red] (w/ {self.ability})""" + def title(self, session: "pwncat.manager.Session"): + return f"""add user via [blue]file write[/blue] as [red]root[/red] (w/ {self.ability.title(session)})""" class Module(EnumerateModule):