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:
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
|
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]"
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
Loading…
Reference in New Issue
Block a user