1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-30 12:24: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
from pwncat.gtfobins import Stream, Capability
from pwncat.platform.linux import LinuxReader, LinuxWriter
from pwncat.modules.agnostic.enumerate.ability import (
ExecuteAbility,
from pwncat.modules.agnostic.enumerate.ability import (ExecuteAbility,
FileReadAbility,
FileWriteAbility,
)
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]"

View File

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

View File

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