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:
parent
be2fb26765
commit
396800261d
121
pwncat/commands/escalate.py
Normal file
121
pwncat/commands/escalate.py
Normal 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
37
pwncat/commands/leave.py
Normal 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")
|
@ -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]"
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user