1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-27 10:54:14 +01:00

Added initial escalate implementation

Also added leave command to unwrap subshells after escalation
This commit is contained in:
Caleb Stewart 2021-05-11 18:09:05 -04:00
parent be2fb26765
commit 396800261d
5 changed files with 192 additions and 27 deletions

121
pwncat/commands/escalate.py Normal file
View File

@ -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")

37
pwncat/commands/leave.py Normal file
View File

@ -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")

View File

@ -7,11 +7,9 @@ from io import TextIOWrapper
import pwncat.subprocess import pwncat.subprocess
from pwncat.gtfobins import Stream, Capability from pwncat.gtfobins import Stream, Capability
from pwncat.platform.linux import LinuxReader, LinuxWriter from pwncat.platform.linux import LinuxReader, LinuxWriter
from pwncat.modules.agnostic.enumerate.ability import ( from pwncat.modules.agnostic.enumerate.ability import (ExecuteAbility,
ExecuteAbility, FileReadAbility,
FileReadAbility, FileWriteAbility)
FileWriteAbility,
)
class GTFOFileRead(FileReadAbility): class GTFOFileRead(FileReadAbility):
@ -74,8 +72,9 @@ class GTFOFileRead(FileReadAbility):
return raw_reader return raw_reader
def __str__(self): def title(self, session):
return f"file read as UID:{self.uid} via {self.method.binary_path}" 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): class GTFOFileWrite(FileWriteAbility):
@ -138,8 +137,9 @@ class GTFOFileWrite(FileWriteAbility):
return raw_writer return raw_writer
def __str__(self): def title(self, session):
return f"file write as UID:{self.uid} via {self.method.binary_path}" 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): class GTFOExecute(ExecuteAbility):
@ -201,5 +201,6 @@ class GTFOExecute(ExecuteAbility):
self.send_command(session, shell.encode("utf-8") + b"\n") self.send_command(session, shell.encode("utf-8") + b"\n")
def __str__(self): def title(self, session):
return f"execution as UID:{self.uid} via {self.method.binary_path}" user = session.find_user(uid=self.uid)
return f"shell as [blue]{user.name}[/blue] via [cyan]{self.method.binary_path}[/cyan]"

View File

@ -49,16 +49,9 @@ class Session:
self.db = manager.db.open() self.db = manager.db.open()
self.module_depth = 0 self.module_depth = 0
self.showing_progress = True self.showing_progress = True
self.layers = []
self._progress = rich.progress.Progress( self._progress = None
"{task.fields[platform]}",
"",
"{task.description}",
"",
"{task.fields[status]}",
transient=True,
console=console,
)
# If necessary, build a new platform object # If necessary, build a new platform object
if isinstance(platform, Platform): if isinstance(platform, Platform):
@ -207,7 +200,7 @@ class Session:
# Ensure the variable exists even if an exception happens # Ensure the variable exists even if an exception happens
# prior to task creation # prior to task creation
task = None task = None
started = self._progress._started started = self._progress is not None # ._started
if "status" not in kwargs: if "status" not in kwargs:
kwargs["status"] = "..." kwargs["status"] = "..."
@ -217,7 +210,16 @@ class Session:
try: try:
# Ensure this bar is started if we are the selected # Ensure this bar is started if we are the selected
# target. # 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() self._progress.start()
# Create the new task # Create the new task
@ -232,6 +234,7 @@ class Session:
# nested tasks. # nested tasks.
if not started: if not started:
self._progress.stop() self._progress.stop()
self._progress = None
def update_task(self, task, *args, **kwargs): def update_task(self, task, *args, **kwargs):
"""Update an active task""" """Update an active task"""
@ -251,6 +254,10 @@ class Session:
def close(self): def close(self):
"""Close the session and remove from manager tracking""" """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.platform.channel.close()
self.died() self.died()
@ -276,8 +283,6 @@ class Manager:
self.config = Config() self.config = Config()
self.sessions: List[Session] = [] self.sessions: List[Session] = []
self.modules: Dict[str, pwncat.modules.BaseModule] = {} self.modules: Dict[str, pwncat.modules.BaseModule] = {}
# self.engine = None
# self.SessionBuilder = None
self._target = None self._target = None
self.parser = CommandParser(self) self.parser = CommandParser(self)
self.interactive_running = False self.interactive_running = False
@ -414,7 +419,7 @@ class Manager:
def log(self, *args, **kwargs): def log(self, *args, **kwargs):
"""Output a log entry""" """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) self.target._progress.log(*args, **kwargs)
else: else:
console.log(*args, **kwargs) console.log(*args, **kwargs)

View File

@ -53,11 +53,12 @@ class AppendPasswd(EscalationReplace):
try: try:
session.platform.su(backdoor_user, password=backdoor_pass) session.platform.su(backdoor_user, password=backdoor_pass)
return lambda session: session.platform.channel.send(b"exit\n")
except PermissionError: except PermissionError:
raise ModuleFailed("added user, but switch user failed") raise ModuleFailed("added user, but switch user failed")
def __str__(self): def title(self, session: "pwncat.manager.Session"):
return f"""add user via [blue]file write[/blue] as [red]root[/red] (w/ {self.ability})""" return f"""add user via [blue]file write[/blue] as [red]root[/red] (w/ {self.ability.title(session)})"""
class Module(EnumerateModule): class Module(EnumerateModule):