mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-27 19:04:15 +01:00
Added subprocess option to get file-like access to command output, and a downloader that reuses the open socket connection for fast downloads
This commit is contained in:
parent
69346b9395
commit
df336d1081
@ -7,15 +7,17 @@ from pwncat.downloader.nc import NetcatDownloader
|
|||||||
from pwncat.downloader.curl import CurlDownloader
|
from pwncat.downloader.curl import CurlDownloader
|
||||||
from pwncat.downloader.shell import ShellDownloader
|
from pwncat.downloader.shell import ShellDownloader
|
||||||
from pwncat.downloader.bashtcp import BashTCPDownloader
|
from pwncat.downloader.bashtcp import BashTCPDownloader
|
||||||
|
from pwncat.downloader.raw import RawShellDownloader
|
||||||
|
|
||||||
all_downloaders = [
|
all_downloaders = [
|
||||||
NetcatDownloader,
|
NetcatDownloader,
|
||||||
CurlDownloader,
|
CurlDownloader,
|
||||||
ShellDownloader,
|
ShellDownloader,
|
||||||
BashTCPDownloader,
|
BashTCPDownloader,
|
||||||
|
RawShellDownloader,
|
||||||
]
|
]
|
||||||
downloaders = [NetcatDownloader, CurlDownloader]
|
downloaders = [NetcatDownloader, CurlDownloader]
|
||||||
fallback = ShellDownloader
|
fallback = RawShellDownloader
|
||||||
|
|
||||||
|
|
||||||
def get_names() -> List[str]:
|
def get_names() -> List[str]:
|
||||||
|
@ -23,8 +23,13 @@ class Downloader:
|
|||||||
def check(cls, pty: "pwncat.pty.PtyHandler") -> bool:
|
def check(cls, pty: "pwncat.pty.PtyHandler") -> bool:
|
||||||
""" Check if the given PTY connection can support this downloader """
|
""" Check if the given PTY connection can support this downloader """
|
||||||
for binary in cls.BINARIES:
|
for binary in cls.BINARIES:
|
||||||
if pty.which(binary) is None:
|
if isinstance(binary, list) or isinstance(binary, tuple):
|
||||||
raise DownloadError(f"required remote binary not found: {binary}")
|
for equivalent in binary:
|
||||||
|
if pty.which(equivalent):
|
||||||
|
return
|
||||||
|
elif pty.which(binary) is not None:
|
||||||
|
return
|
||||||
|
raise DownloadError(f"required remote binary not found: {binary}")
|
||||||
|
|
||||||
def __init__(self, pty: "pwncat.pty.PtyHandler", remote_path: str, local_path: str):
|
def __init__(self, pty: "pwncat.pty.PtyHandler", remote_path: str, local_path: str):
|
||||||
self.pty = pty
|
self.pty = pty
|
||||||
|
44
pwncat/downloader/raw.py
Normal file
44
pwncat/downloader/raw.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from typing import Generator, Callable
|
||||||
|
from io import BufferedReader
|
||||||
|
import base64
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
from pwncat.downloader.base import Downloader, DownloadError
|
||||||
|
from pwncat import util
|
||||||
|
|
||||||
|
|
||||||
|
class RawShellDownloader(Downloader):
|
||||||
|
|
||||||
|
NAME = "raw"
|
||||||
|
BINARIES = [("dd", "cat")]
|
||||||
|
BLOCKSZ = 8192
|
||||||
|
|
||||||
|
def command(self) -> Generator[str, None, None]:
|
||||||
|
""" Yield list of commands to transfer the file """
|
||||||
|
|
||||||
|
remote_path = shlex.quote(self.remote_path)
|
||||||
|
blocksz = 1024 * 1024
|
||||||
|
binary = self.pty.which("dd")
|
||||||
|
|
||||||
|
if binary is None:
|
||||||
|
binary = self.pty.which("cat")
|
||||||
|
|
||||||
|
if "dd" in binary:
|
||||||
|
pipe = self.pty.subprocess(f"dd if={remote_path} bs={blocksz} 2>/dev/null")
|
||||||
|
else:
|
||||||
|
pipe = self.pty.subprocess(f"cat {remote_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.local_path, "wb") as filp:
|
||||||
|
util.copyfileobj(pipe, filp, self.on_progress)
|
||||||
|
finally:
|
||||||
|
self.on_progress(0, -1)
|
||||||
|
pipe.close()
|
||||||
|
|
||||||
|
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
|
114
pwncat/file.py
Normal file
114
pwncat/file.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from io import RawIOBase
|
||||||
|
import socket
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteBinaryPipe(RawIOBase):
|
||||||
|
""" Encapsulate a piped interaction with a remote process. The remote PTY
|
||||||
|
should have been placed in raw mode prior to this object being created, and
|
||||||
|
the appropriate flags in pty already modified. If EOF is found or the object
|
||||||
|
is closed, it will restore the state of the terminal (w/ `reset`). No further
|
||||||
|
reading or writing will be allowed. """
|
||||||
|
|
||||||
|
def __init__(self, pty: "pwncat.pty.PtyHandler", delim: bytes, binary: bool):
|
||||||
|
self.pty = pty
|
||||||
|
self.delim = delim
|
||||||
|
self.eof = 0
|
||||||
|
self.next_eof = False
|
||||||
|
self.binary = binary
|
||||||
|
self.split_eof = b""
|
||||||
|
|
||||||
|
def on_eof(self):
|
||||||
|
if self.eof:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Set eof flag
|
||||||
|
self.eof = 1
|
||||||
|
|
||||||
|
if self.binary:
|
||||||
|
# Reset the terminal
|
||||||
|
self.pty.reset()
|
||||||
|
# Send a bare echo, and read all data to ensure we don't clobber the
|
||||||
|
# output of the user's terminal
|
||||||
|
self.pty.run("echo")
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.eof:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Kill the last job. This should be us.
|
||||||
|
self.pty.run("kill -9 %%", wait=False)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
self.on_eof()
|
||||||
|
|
||||||
|
# def read(self, size: int = -1):
|
||||||
|
# if self.eof == -1:
|
||||||
|
# self.on_eof()
|
||||||
|
|
||||||
|
# if self.eof:
|
||||||
|
# return b""
|
||||||
|
|
||||||
|
# if size == -1:
|
||||||
|
# data = b""
|
||||||
|
# while self.delim not in data:
|
||||||
|
# data += self.pty.client.recv(1024 * 1024)
|
||||||
|
# data = data.split(self.delim)[0]
|
||||||
|
# self.eof = -1
|
||||||
|
# else:
|
||||||
|
# data = self.pty.client.recv(size)
|
||||||
|
# if self.delim in data:
|
||||||
|
# self.eof = -1
|
||||||
|
# data = data.split(self.delim)[0]
|
||||||
|
|
||||||
|
# return data
|
||||||
|
|
||||||
|
def readinto(self, b: bytearray):
|
||||||
|
if self.eof:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if isinstance(b, memoryview):
|
||||||
|
obj = b.obj
|
||||||
|
else:
|
||||||
|
obj = b
|
||||||
|
|
||||||
|
# Receive the data
|
||||||
|
n = self.pty.client.recv_into(b)
|
||||||
|
|
||||||
|
# Check for EOF
|
||||||
|
if self.delim in obj:
|
||||||
|
self.on_eof()
|
||||||
|
n = obj.find(self.delim)
|
||||||
|
return n
|
||||||
|
else:
|
||||||
|
# Check for EOF split across blocks
|
||||||
|
for i in range(1, len(self.delim) - 1):
|
||||||
|
# See if a piece of the delimeter is at the end of this block
|
||||||
|
piece = self.delim[:-i]
|
||||||
|
if bytes(b[-len(piece) :]) == piece:
|
||||||
|
try:
|
||||||
|
# Peak the next bytes, to see if this is actually the
|
||||||
|
# delimeter
|
||||||
|
rest = self.pty.client.recv(
|
||||||
|
i, socket.MSG_PEEK | socket.MSG_DONTWAIT
|
||||||
|
)
|
||||||
|
except (socket.error, BlockingIOError):
|
||||||
|
rest = b""
|
||||||
|
# It is!
|
||||||
|
if (piece + rest) == self.delim:
|
||||||
|
# Receive the delimeter
|
||||||
|
self.pty.client.recv(i)
|
||||||
|
# Adjust result
|
||||||
|
n -= len(piece)
|
||||||
|
# Set EOF for next read
|
||||||
|
self.on_eof()
|
||||||
|
|
||||||
|
return n
|
||||||
|
|
||||||
|
def flush_read(self):
|
||||||
|
""" read all until eof and ignore it """
|
||||||
|
for block in iter(lambda: self.read(1024 * 1024), b""):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def write(self, data: bytes):
|
||||||
|
return self.pty.client.send(data)
|
108
pwncat/pty.py
108
pwncat/pty.py
@ -28,6 +28,7 @@ import os
|
|||||||
from pwncat import util
|
from pwncat import util
|
||||||
from pwncat import downloader, uploader, privesc
|
from pwncat import downloader, uploader, privesc
|
||||||
from pwncat.lexer import LocalCommandLexer
|
from pwncat.lexer import LocalCommandLexer
|
||||||
|
from pwncat.file import RemoteBinaryPipe
|
||||||
from colorama import Fore
|
from colorama import Fore
|
||||||
|
|
||||||
|
|
||||||
@ -260,7 +261,9 @@ class PtyHandler:
|
|||||||
self.run("unset HISTFILE; export HISTCONTROL=ignorespace")
|
self.run("unset HISTFILE; export HISTCONTROL=ignorespace")
|
||||||
|
|
||||||
util.info("setting terminal prompt", overlay=True)
|
util.info("setting terminal prompt", overlay=True)
|
||||||
self.run(f'export PS1="{self.remote_prefix} $PS1"')
|
self.run(
|
||||||
|
f'export SAVED_PS1="$PS1"; export PS1="{self.remote_prefix} $SAVED_PS1"'
|
||||||
|
)
|
||||||
|
|
||||||
# Locate interesting binaries
|
# Locate interesting binaries
|
||||||
# The auto-resolving doesn't work correctly until we have a pty
|
# The auto-resolving doesn't work correctly until we have a pty
|
||||||
@ -303,7 +306,9 @@ class PtyHandler:
|
|||||||
self.has_prompt = True
|
self.has_prompt = True
|
||||||
|
|
||||||
util.info("setting terminal prompt", overlay=True)
|
util.info("setting terminal prompt", overlay=True)
|
||||||
self.run(f'export PS1="{self.remote_prefix} $PS1"')
|
self.run(
|
||||||
|
f'export SAVED_PS1="$PS1"; export PS1="{self.remote_prefix} $SAVED_PS1"'
|
||||||
|
)
|
||||||
|
|
||||||
# Make sure HISTFILE is unset in this PTY (it resets when a pty is
|
# Make sure HISTFILE is unset in this PTY (it resets when a pty is
|
||||||
# opened)
|
# opened)
|
||||||
@ -452,6 +457,9 @@ class PtyHandler:
|
|||||||
result = self.run(line[1:])
|
result = self.run(line[1:])
|
||||||
sys.stdout.buffer.write(result)
|
sys.stdout.buffer.write(result)
|
||||||
continue
|
continue
|
||||||
|
elif line[0] == "-":
|
||||||
|
self.run(line[1:], wait=False)
|
||||||
|
continue
|
||||||
|
|
||||||
argv = shlex.split(line)
|
argv = shlex.split(line)
|
||||||
|
|
||||||
@ -539,6 +547,12 @@ class PtyHandler:
|
|||||||
|
|
||||||
def on_progress(copied, blocksz):
|
def on_progress(copied, blocksz):
|
||||||
""" Update the progress bar """
|
""" Update the progress bar """
|
||||||
|
if blocksz == -1:
|
||||||
|
counter.stopped = True
|
||||||
|
counter.done = True
|
||||||
|
pb.invalidate()
|
||||||
|
return
|
||||||
|
|
||||||
counter.items_completed += blocksz
|
counter.items_completed += blocksz
|
||||||
if counter.items_completed >= counter.total:
|
if counter.items_completed >= counter.total:
|
||||||
counter.done = True
|
counter.done = True
|
||||||
@ -546,15 +560,11 @@ class PtyHandler:
|
|||||||
if (time.time() - last_update) > 0.1:
|
if (time.time() - last_update) > 0.1:
|
||||||
pb.invalidate()
|
pb.invalidate()
|
||||||
|
|
||||||
download.serve(on_progress)
|
|
||||||
|
|
||||||
download.command()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while not counter.done:
|
download.serve(on_progress)
|
||||||
time.sleep(0.1)
|
if download.command():
|
||||||
except KeyboardInterrupt:
|
while not counter.done:
|
||||||
pass
|
time.sleep(0.2)
|
||||||
finally:
|
finally:
|
||||||
download.shutdown()
|
download.shutdown()
|
||||||
|
|
||||||
@ -653,6 +663,11 @@ class PtyHandler:
|
|||||||
help_msg = getattr(self, c).__doc__
|
help_msg = getattr(self, c).__doc__
|
||||||
print(f"{c[3:]:15s}{help_msg}")
|
print(f"{c[3:]:15s}{help_msg}")
|
||||||
|
|
||||||
|
def do_reset(self, argv):
|
||||||
|
""" Reset the remote terminal (calls sync, reset, and sets PS1) """
|
||||||
|
self.reset()
|
||||||
|
self.do_sync([])
|
||||||
|
|
||||||
def run(self, cmd, wait=True) -> bytes:
|
def run(self, cmd, wait=True) -> bytes:
|
||||||
""" Run a command in the context of the remote host and return the
|
""" Run a command in the context of the remote host and return the
|
||||||
output. This is run synchrounously.
|
output. This is run synchrounously.
|
||||||
@ -689,7 +704,7 @@ class PtyHandler:
|
|||||||
if delim:
|
if delim:
|
||||||
command = f" echo _PWNCAT_STARTDELIM_; {cmd}; echo _PWNCAT_ENDDELIM_"
|
command = f" echo _PWNCAT_STARTDELIM_; {cmd}; echo _PWNCAT_ENDDELIM_"
|
||||||
else:
|
else:
|
||||||
command = cmd
|
command = f" {cmd}"
|
||||||
|
|
||||||
response = b""
|
response = b""
|
||||||
|
|
||||||
@ -707,9 +722,80 @@ class PtyHandler:
|
|||||||
|
|
||||||
return b"_PWNCAT_ENDDELIM_"
|
return b"_PWNCAT_ENDDELIM_"
|
||||||
|
|
||||||
|
def subprocess(self, cmd) -> RemoteBinaryPipe:
|
||||||
|
""" Create an asynchronous child on the remote end and return a
|
||||||
|
file-like object which can communicate with it's standard output. The
|
||||||
|
remote terminal is placed in raw mode with no-echo first, and the
|
||||||
|
command is run on a separate background shell w/ no standard input. The
|
||||||
|
output of the command can be retrieved through the returned file-like
|
||||||
|
object. You **must** either call `close()` of the pipe, or read until
|
||||||
|
eof, or the PTY will not be restored to a normal state.
|
||||||
|
|
||||||
|
If `close()` is called prior to EOF, the remote process will be killed,
|
||||||
|
and any remaining output will be flushed prior to resetting the terminal.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(cmd, list):
|
||||||
|
cmd = shlex.join(cmd)
|
||||||
|
|
||||||
|
sdelim = "_PWNCAT_STARTDELIM_"
|
||||||
|
edelim = "_PWNCAT_ENDDELIM_"
|
||||||
|
|
||||||
|
# List of ";" separated commands that will be run
|
||||||
|
command = []
|
||||||
|
# Clear the prompt, or it will get displayed in our output due to the
|
||||||
|
# background task
|
||||||
|
command.append("export PS1=")
|
||||||
|
# Needed to disable job control messages in bash
|
||||||
|
command.append("set +m")
|
||||||
|
# This is gross, but it allows us to recieve stderr and stdout, while
|
||||||
|
# ignoring the job control start message.
|
||||||
|
command.append(
|
||||||
|
f"{{ echo {sdelim}; {cmd} && echo {edelim} || echo {edelim} 2>&1 & }} 2>/dev/null"
|
||||||
|
)
|
||||||
|
# Re-enable normal job control in bash
|
||||||
|
command.append("set -m")
|
||||||
|
|
||||||
|
# Join them all into one command
|
||||||
|
command = ";".join(command).encode("utf-8")
|
||||||
|
|
||||||
|
# Enter raw mode w/ no echo on the remote terminal
|
||||||
|
# DANGER
|
||||||
|
self.raw(echo=False)
|
||||||
|
|
||||||
|
self.client.sendall(command + b"\n")
|
||||||
|
self.recvuntil(sdelim)
|
||||||
|
self.recvuntil("\n")
|
||||||
|
|
||||||
|
# Bash sends some bullshit that messes up terminals. Check to see if it
|
||||||
|
# is there, and ignore it if it is
|
||||||
|
try:
|
||||||
|
data = self.client.recv(2, socket.MSG_PEEK | socket.MSG_DONTWAIT)
|
||||||
|
except (BlockingIOError, socket.error):
|
||||||
|
data = b""
|
||||||
|
|
||||||
|
if data == b"\x1b_":
|
||||||
|
self.recvuntil(b"\x1b\\")
|
||||||
|
|
||||||
|
return RemoteBinaryPipe(self, edelim.encode("utf-8"), True)
|
||||||
|
|
||||||
|
def raw(self, echo: bool = False):
|
||||||
|
self.run("stty raw -echo", wait=False)
|
||||||
|
self.has_cr = False
|
||||||
|
self.has_echo = False
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.run("reset", wait=False)
|
||||||
|
self.has_cr = True
|
||||||
|
self.has_echo = True
|
||||||
|
self.run(f'export PS1="{self.remote_prefix} $SAVED_PS1"')
|
||||||
|
|
||||||
def recvuntil(self, needle: bytes, flags=0):
|
def recvuntil(self, needle: bytes, flags=0):
|
||||||
""" Recieve data from the client until the specified string appears """
|
""" Recieve data from the client until the specified string appears """
|
||||||
|
|
||||||
|
if isinstance(needle, str):
|
||||||
|
needle = needle.encode("utf-8")
|
||||||
|
|
||||||
result = b""
|
result = b""
|
||||||
while not result.endswith(needle):
|
while not result.endswith(needle):
|
||||||
try:
|
try:
|
||||||
|
Loading…
Reference in New Issue
Block a user