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

All old commands ported over

This commit is contained in:
Caleb Stewart 2020-05-14 22:18:21 -04:00
parent b1f3c54087
commit 45810027d0
7 changed files with 162 additions and 510 deletions

View File

@ -106,11 +106,12 @@ def main():
sys.stdout.buffer.write(data) sys.stdout.buffer.write(data)
sys.stdout.flush() sys.stdout.flush()
except ConnectionResetError: except ConnectionResetError:
handler.restore() handler.restore_local_term()
util.warn("connection reset by remote host") util.warn("connection reset by remote host")
finally: finally:
# Restore the shell # Restore the shell
handler.restore() handler.restore_local_term()
util.success("local terminal restored")
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -18,6 +18,7 @@ from prompt_toolkit.lexers import PygmentsLexer
from prompt_toolkit.document import Document from prompt_toolkit.document import Document
from pygments.styles import get_style_by_name from pygments.styles import get_style_by_name
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.history import InMemoryHistory
from typing import Dict, Any, List, Iterable from typing import Dict, Any, List, Iterable
from enum import Enum, auto from enum import Enum, auto
import argparse import argparse
@ -29,6 +30,7 @@ import re
from pprint import pprint from pprint import pprint
from pwncat.commands.base import CommandDefinition, Complete from pwncat.commands.base import CommandDefinition, Complete
from pwncat.util import State
from pwncat import util from pwncat import util
@ -50,6 +52,7 @@ class CommandParser:
.Command(pty, self) .Command(pty, self)
) )
history = InMemoryHistory()
completer = CommandCompleter(pty, self.commands) completer = CommandCompleter(pty, self.commands)
lexer = PygmentsLexer(CommandLexer.build(self.commands)) lexer = PygmentsLexer(CommandLexer.build(self.commands))
style = style_from_pygments_cls(get_style_by_name("monokai")) style = style_from_pygments_cls(get_style_by_name("monokai"))
@ -66,10 +69,35 @@ class CommandParser:
style=style, style=style,
auto_suggest=auto_suggest, auto_suggest=auto_suggest,
complete_while_typing=False, complete_while_typing=False,
history=history,
)
self.toolbar = PromptSession(
[
("fg:ansiyellow bold", "(local) "),
("fg:ansimagenta bold", "pwncat"),
("", "$ "),
],
completer=completer,
lexer=lexer,
style=style,
auto_suggest=auto_suggest,
complete_while_typing=False,
prompt_in_toolbar=True,
history=history,
) )
self.pty = pty self.pty = pty
def run_single(self):
try:
line = self.toolbar.prompt().strip()
except (EOFError, OSError, KeyboardInterrupt):
pass
else:
if line != "":
self.dispatch_line(line)
def run(self): def run(self):
self.running = True self.running = True
@ -79,7 +107,7 @@ class CommandParser:
try: try:
line = self.prompt.prompt().strip() line = self.prompt.prompt().strip()
except (EOFError, OSError): except (EOFError, OSError):
self.pty.enter_raw() self.pty.state = State.RAW
self.running = False self.running = False
continue continue
@ -165,7 +193,7 @@ class CommandLexer(RegexLexer):
class RemotePathCompleter(Completer): class RemotePathCompleter(Completer):
""" Complete remote file names/paths """ """ Complete remote file names/paths """
def __init__(self, pty: "PtyHandler"): def __init__(self, pty: "pwncat.pty.PtyHandler"):
self.pty = pty self.pty = pty
def get_completions(self, document: Document, complete_event: CompleteEvent): def get_completions(self, document: Document, complete_event: CompleteEvent):

View File

@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from pwncat.commands.base import CommandDefinition, Complete, parameter from pwncat.commands.base import CommandDefinition, Complete, parameter
from pwncat import util
class Command(CommandDefinition): class Command(CommandDefinition):
@ -9,4 +10,4 @@ class Command(CommandDefinition):
ARGS = {} ARGS = {}
def run(self, args): def run(self, args):
self.pty.enter_raw() self.pty.state = util.State.RAW

View File

@ -8,6 +8,7 @@ from pwncat.commands.base import (
StoreForAction, StoreForAction,
) )
from pwncat import util, privesc from pwncat import util, privesc
from pwncat.util import State
from colorama import Fore from colorama import Fore
import argparse import argparse
import shutil import shutil
@ -174,6 +175,6 @@ class Command(CommandDefinition):
) )
self.pty.reset() self.pty.reset()
self.pty.do_back([]) self.pty.state = State.RAW
except privesc.PrivescError as exc: except privesc.PrivescError as exc:
util.error(f"escalation failed: {exc}") util.error(f"escalation failed: {exc}")

33
pwncat/commands/sync.py Normal file
View File

@ -0,0 +1,33 @@
#!/usr/bin/env python3
from pwncat.commands.base import CommandDefinition, Complete, parameter
from pwncat import util
import os
class Command(CommandDefinition):
""" Synchronize the remote terminal with the local terminal. This will
attempt to set the remote prompt, terminal width, terminal height, and TERM
environment variables to enable to cleanest interface to the remote system
possible. """
PROG = "sync"
ARGS = {}
DEFAULTS = {}
def run(self, args):
# Get the terminal type
TERM = os.environ.get("TERM", None)
if TERM is None:
util.warn("no local TERM set. falling back to 'xterm'")
TERM = "xterm"
# Get the width and height
columns, rows = os.get_terminal_size(0)
# Update the state
self.pty.run(
f"stty rows {rows};" f"stty columns {columns};" f"export TERM='{TERM}'"
)
util.success("terminal state synchronized")

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from typing import Dict, Optional, Iterable, IO, Callable from typing import Dict, Optional, Iterable, IO, Callable, Any
from prompt_toolkit import PromptSession, ANSI from prompt_toolkit import PromptSession, ANSI
from prompt_toolkit.shortcuts import ProgressBar from prompt_toolkit.shortcuts import ProgressBar
from prompt_toolkit.completion import ( from prompt_toolkit.completion import (
@ -31,6 +31,7 @@ import os
import re import re
import io import io
from pwncat.util import State
from pwncat import util from pwncat import util
from pwncat import downloader, uploader, privesc from pwncat import downloader, uploader, privesc
from pwncat.file import RemoteBinaryPipe from pwncat.file import RemoteBinaryPipe
@ -41,14 +42,6 @@ from pwncat.commands import CommandParser
from colorama import Fore from colorama import Fore
class State(enum.Enum):
""" The current PtyHandler state """
NORMAL = enum.auto()
RAW = enum.auto()
COMMAND = enum.auto()
def with_parser(f): def with_parser(f):
@wraps(f) @wraps(f)
def _decorator(self, argv): def _decorator(self, argv):
@ -123,6 +116,7 @@ class CommandCompleter(Completer):
# Split document. # Split document.
text = document.text_before_cursor.lstrip() text = document.text_before_cursor.lstrip()
stripped_len = len(document.text_before_cursor) - len(text) stripped_len = len(document.text_before_cursor) - len(text)
prev_term: Optional[str]
# If there is a space, check for the first term, and use a # If there is a space, check for the first term, and use a
# subcompleter. # subcompleter.
@ -191,12 +185,12 @@ class PtyHandler:
local terminal if requested and exit raw mode. """ local terminal if requested and exit raw mode. """
self.client = client self.client = client
self.state = "normal" self._state = State.COMMAND
self.saved_term_state = None self.saved_term_state = None
self.input = b"" self.input = b""
self.lhost = None self.lhost = None
self.known_binaries = {} self.known_binaries: Dict[str, Optional[str]] = {}
self.known_users = {} self.known_users: Dict[str, Any] = {}
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 = ( self.remote_prompt = (
@ -205,7 +199,7 @@ class PtyHandler:
) )
self.prompt = self.build_prompt_session() self.prompt = self.build_prompt_session()
self.has_busybox = False self.has_busybox = False
self.busybox_path = None self.busybox_path: Optional[str] = None
self.binary_aliases = { self.binary_aliases = {
"python": [ "python": [
"python2", "python2",
@ -222,9 +216,6 @@ class PtyHandler:
self.gtfo: GTFOBins = GTFOBins("data/gtfobins.json", self.which) self.gtfo: GTFOBins = GTFOBins("data/gtfobins.json", self.which)
self.default_privkey = "./data/pwncat" self.default_privkey = "./data/pwncat"
# Setup the argument parsers for local the local prompt
self.setup_command_parsers()
# We should always get a response within 3 seconds... # We should always get a response within 3 seconds...
self.client.settimeout(1) self.client.settimeout(1)
@ -280,11 +271,6 @@ class PtyHandler:
if self.arch == "amd64": if self.arch == "amd64":
self.arch = "x86_64" self.arch = "x86_64"
# Check if we are on BSD
response = self.run("uname -a").decode("utf-8").strip()
if "bsd" in response.lower():
self.bootstrap_bsd()
# Ensure history is disabled # Ensure history is disabled
util.info("disabling remote command history", overlay=True) util.info("disabling remote command history", overlay=True)
self.run("unset HISTFILE; export HISTCONTROL=ignorespace") self.run("unset HISTFILE; export HISTCONTROL=ignorespace")
@ -357,10 +343,6 @@ class PtyHandler:
# Disable automatic margins, which fuck up the prompt # Disable automatic margins, which fuck up the prompt
self.run("tput rmam") self.run("tput rmam")
# Synchronize the terminals
util.info("synchronizing terminal state", overlay=True)
self.do_sync([])
self.privesc = privesc.Finder(self) self.privesc = privesc.Finder(self)
# Save our terminal state # Save our terminal state
@ -368,64 +350,11 @@ class PtyHandler:
self.command_parser = CommandParser(self) self.command_parser = CommandParser(self)
# Synchronize the terminals
self.command_parser.dispatch_line("sync")
# Force the local TTY to enter raw mode # Force the local TTY to enter raw mode
self.enter_raw() self.state = State.RAW
def bootstrap_bsd(self):
""" BSD acts differently than linux (since it isn't). While pwncat isn't
specifically dependent on linux, it does depend on the interfaces provided
by standard commands in linux. BSD has diverged. The easiest solution is to
download a BSD version of busybox, which provides all the normal interfaces
we expect. We can't use the normal busybox bootstrap, since it depends on
some of these interfaces. We have to do it manually for BSD. """
if self.arch != "x86_64" and self.arch != "i386" or self.which("dd") is None:
util.error(f"unable to support freebsd on {self.arch}.")
util.error(
"upload your own busybox and tell pwncat where to find it w/ the busybox command to enable full functionality"
)
return
dd = self.which("dd")
length = os.path.getsize("data/busybox-bsd")
command = f"{dd} of=/tmp/busybox bs=1 count={length}"
with ProgressBar("uploading busydbox via dd") as pb:
counter = pb(length)
last_update = time.time()
def on_progress(count, blocksz):
counter.items_completed += blocksz
if (time.time() - last_update) > 0.1:
pb.invalidate()
last_update = time.time()
with open("data/busybox-bsd", "rb") as filp:
util.copyfileobj(filp, pipe, on_progress)
counter.done = True
counter.stopped = True
pb.invalidate()
time.sleep(0.1)
# We now have busybox!
self.run("chmod +x /tmp/busybox")
util.success("we now have busybox on bsd!")
# Notify everyone else about busybox
self.has_busybox = True
self.busybox_path = "/tmp/busybox"
self.busybox_provides = (
open("data/busybox-default-provides", "r").read().strip().split("\n")
)
# Start the busybox shell
util.info("starting busybox shell")
self.process("exec /tmp/busybox ash", delim=False)
self.shell = "/tmp/busybox ash"
self.flush_output()
self.reset()
def bootstrap_busybox(self, url): def bootstrap_busybox(self, url):
""" Utilize the architecture we grabbed from `uname -m` to grab a """ Utilize the architecture we grabbed from `uname -m` to grab a
@ -572,7 +501,7 @@ class PtyHandler:
complete_while_typing=False, complete_while_typing=False,
) )
def which(self, name: str, request=True, quote=False) -> str: def which(self, name: str, request=True, quote=False) -> Optional[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
@ -580,7 +509,7 @@ class PtyHandler:
if self.has_busybox: if self.has_busybox:
if name in self.busybox_provides: if name in self.busybox_provides:
if quote: if quote:
return f"{shlex.quote(self.busybox_path)} {name}" return f"{shlex.quote(str(self.busybox_path))} {name}"
else: else:
return f"{self.busybox_path} {name}" return f"{self.busybox_path} {name}"
@ -612,339 +541,81 @@ class PtyHandler:
r""" Process a new byte of input from stdin. This is to catch "\r~C" and open r""" Process a new byte of input from stdin. This is to catch "\r~C" and open
a local prompt """ a local prompt """
# Send the new data to the client if self.input == b"":
self.client.send(data) # Enter commmand mode after C-d
if data == b"\x04":
# Only process data following a new line # Clear line
if data == b"\r": self.client.send(b"\x15")
self.input = data # Enter command mode
elif len(data) == 0: self.state = State.COMMAND
return # C-k is the prefix character
elif data == b"\x0b":
self.input = data
else:
self.client.send(data)
else: else:
self.input += data # "C-k c" to enter command mode
if data == b"c":
if self.input == b"\r~C": self.client.send(b"\x15")
# Erase the current line on the remote host ("~C") self.state = State.SINGLE
# This is 2 backspace characters elif data == b":":
self.client.send(b"\x08" * 2 + b"\r") self.state = State.SINGLE
# Start processing local commands # "C-k C-k" or "C-k C-d" sends the second byte
self.enter_command() elif data == b"\x0b" or data == b"\x04":
elif len(self.input) >= 3: self.client.send(data)
# Our only escapes are 3 characters (include the newline)
self.input = b"" self.input = b""
def recv(self) -> bytes: def recv(self) -> bytes:
""" Recieve data from the client """ """ Recieve data from the client """
return self.client.recv(4096) return self.client.recv(4096)
def enter_raw(self, save: bool = True): @property
""" Enter raw mode on the local terminal """ def state(self) -> State:
return self._state
# Give the user a nice terminal @state.setter
self.flush_output() def state(self, value: State):
self.client.send(b"\n") if value == self._state:
old_term_state = util.enter_raw_mode()
self.state = State.RAW
# Tell the command parser to stop
self.command_parser.running = False
# Save the state if requested
if save:
self.saved_term_state = old_term_state
def enter_command(self):
""" Enter commmand mode. This sets normal mode and uses prompt toolkit
process commands from the user for the local machine """
# Go back to normal mode
self.restore()
self.state = State.COMMAND
# Hopefully this fixes weird cursor position issues
sys.stdout.write("\n")
self.command_parser.run()
return
# Process commands
while self.state is State.COMMAND:
try:
try:
line = self.prompt.prompt()
except (EOFError, OSError):
# The user pressed ctrl-d, go back
self.enter_raw()
continue
if len(line) > 0:
if line[0] == "!":
# Allow running shell commands
subprocess.run(line[1:], shell=True)
continue
elif line[0] == "@":
result = self.run(line[1:])
sys.stdout.buffer.write(result)
continue
elif line[0] == "-":
self.run(line[1:], wait=False)
continue
try:
argv = shlex.split(line)
except ValueError as e:
util.error(e.args[0])
continue
# Empty command
if len(argv) == 0:
continue
try:
method = getattr(self, f"do_{argv[0]}")
except AttributeError:
util.warn(f"{argv[0]}: command does not exist")
continue
# Call the method
method(argv[1:])
except KeyboardInterrupt as exc:
traceback.print_exc()
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)
def with_progress(
self, title: str, target: Callable[[Callable], None], length: int = None
):
""" A shortcut to displaying a progress bar for various things. It will
start a prompt_toolkit progress bar with the given title and a counter
with the given length. Then, it will call `target` with an `on_progress`
parameter. This parameter should be called for all progress updates. See
the `do_upload` and `do_download` for examples w/ copyfileobj """
with ProgressBar(title) as pb:
counter = pb(range(length))
last_update = time.time()
def on_progress(copied, blocksz):
""" Update the progress bar """
if blocksz == -1:
counter.stopped = True
counter.done = True
pb.invalidate()
return
counter.items_completed += blocksz
if counter.items_completed >= counter.total:
counter.done = True
counter.stopped = True
if (time.time() - last_update) > 0.1:
pb.invalidate()
target(on_progress)
# https://github.com/prompt-toolkit/python-prompt-toolkit/issues/964
time.sleep(0.1)
@with_parser
def do_download(self, args):
""" Download a file from the remote host """
try:
# Locate an appropriate downloader class
DownloaderClass = downloader.find(self, args.method)
except downloader.DownloadError as exc:
util.error(f"{exc}")
return return
# Grab the arguments if value == State.RAW:
path = args.path self.flush_output()
basename = os.path.basename(args.path) self.client.send(b"\n")
outfile = args.output.format(basename=basename) util.success("pwncat is ready 🐈")
self.saved_term_state = util.enter_raw_mode()
download = DownloaderClass(self, remote_path=path, local_path=outfile) self.command_parser.running = False
self._state = value
# Get the remote file size
size = self.run(f'stat -c "%s" {shlex.quote(path)} 2>/dev/null || echo "none"')
if b"none" in size:
util.error(f"{path}: no such file or directory")
return return
size = int(size) if value == State.COMMAND:
# Go back to normal mode
self.restore_local_term()
self._state = State.COMMAND
# Hopefully this fixes weird cursor position issues
util.success("local terminal restored")
# Setting the state to local command mode does not return until
# command processing is complete.
self.command_parser.run()
return
if value == State.SINGLE:
# Go back to normal mode
self.restore_local_term()
self._state = State.SINGLE
# Hopefully this fixes weird cursor position issues
sys.stdout.write("\n")
# Setting the state to local command mode does not return until
# command processing is complete.
self.command_parser.run_single()
with ProgressBar( # Go back to raw mode
[("#888888", "downloading with "), ("fg:ansiyellow", f"{download.NAME}")] self.flush_output()
) as pb: self.client.send(b"\n")
counter = pb(range(size)) self.saved_term_state = util.enter_raw_mode()
last_update = time.time() self._state = State.RAW
def on_progress(copied, blocksz):
""" Update the progress bar """
if blocksz == -1:
counter.stopped = True
counter.done = True
pb.invalidate()
return
counter.items_completed += blocksz
if counter.items_completed >= counter.total:
counter.done = True
counter.stopped = True
if (time.time() - last_update) > 0.1:
pb.invalidate()
try:
download.serve(on_progress)
if download.command():
while not counter.done:
time.sleep(0.2)
finally:
download.shutdown()
# https://github.com/prompt-toolkit/python-prompt-toolkit/issues/964
time.sleep(0.1)
@with_parser
def do_upload(self, args):
""" Upload a file to the remote host """
if not os.path.isfile(args.path):
util.error(f"{args.path}: no such file or directory")
return return
try: def restore_local_term(self):
# Locate an appropriate downloader class """ Save the local terminal state """
UploaderClass = uploader.find(self, args.method) util.restore_terminal(self.saved_term_state)
except uploader.UploadError as exc:
util.error(f"{exc}")
return
path = args.path
basename = os.path.basename(args.path)
name = basename
outfile = args.output.format(basename=basename)
upload = UploaderClass(self, remote_path=outfile, local_path=path)
with ProgressBar(
[("#888888", "uploading via "), ("fg:ansiyellow", f"{upload.NAME}")]
) as pb:
counter = pb(range(os.path.getsize(path)))
last_update = time.time()
def on_progress(copied, blocksz):
""" Update the progress bar """
counter.items_completed += blocksz
if counter.items_completed >= counter.total:
counter.done = True
counter.stopped = True
if (time.time() - last_update) > 0.1:
pb.invalidate()
upload.serve(on_progress)
upload.command()
try:
while not counter.done:
time.sleep(0.1)
except KeyboardInterrupt:
pass
finally:
upload.shutdown()
# https://github.com/prompt-toolkit/python-prompt-toolkit/issues/964
time.sleep(0.1)
def do_sync(self, argv):
""" Synchronize the remote PTY with the local terminal settings """
TERM = os.environ.get("TERM", "xterm")
columns, rows = os.get_terminal_size(0)
self.run(f"stty rows {rows}; stty columns {columns}; export TERM='{TERM}'")
def do_set(self, argv):
""" Set or view the currently assigned variables """
if len(argv) == 0:
util.info("local variables:")
for k, v in self.vars.items():
print(f" {k} = {shlex.quote(v)}")
util.info("user passwords:")
for user, data in self.users.items():
if data["password"] is not None:
print(
f" {Fore.GREEN}{user}{Fore.RESET} -> {Fore.CYAN}{shlex.quote(data['password'])}{Fore.RESET}"
)
return
parser = argparse.ArgumentParser(prog="set")
parser.add_argument(
"--password",
"-p",
action="store_true",
help="set the password for the given user",
)
parser.add_argument("variable", help="the variable name or user")
parser.add_argument("value", help="the new variable/user password value")
try:
args = parser.parse_args(argv)
except SystemExit:
# The arguments were parsed incorrectly, return.
return
if args.password is not None and args.variable not in self.users:
util.error(f"{args.variable}: no such user")
elif args.password is not None:
self.users[args.variable]["password"] = args.value
else:
self.vars[args.variable] = args.value
def do_help(self, argv):
""" View help for local commands """
if len(argv) == 0:
commands = [x for x in dir(self) if x.startswith("do_")]
else:
commands = [x for x in dir(self) if x.startswith("do_") and x[3:] in argv]
for c in commands:
help_msg = getattr(self, c).__doc__
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, input: bytes = b"") -> bytes: def run(self, cmd, wait=True, input: bytes = b"") -> 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
@ -1040,27 +711,27 @@ class PtyHandler:
edelim = util.random_string(10) # "_PWNCAT_ENDDELIM_" edelim = util.random_string(10) # "_PWNCAT_ENDDELIM_"
# List of ";" separated commands that will be run # List of ";" separated commands that will be run
command = [] commands: List[str] = []
# Clear the prompt, or it will get displayed in our output due to the # Clear the prompt, or it will get displayed in our output due to the
# background task # background task
command.append(" export PS1=") commands.append(" export PS1=")
# Needed to disable job control messages in bash # Needed to disable job control messages in bash
command.append("set +m") commands.append("set +m")
# This is gross, but it allows us to recieve stderr and stdout, while # This is gross, but it allows us to recieve stderr and stdout, while
# ignoring the job control start message. # ignoring the job control start message.
if "w" not in mode and not no_job: if "w" not in mode and not no_job:
command.append( commands.append(
f"{{ echo; echo {sdelim}; {cmd} && echo {edelim} || echo {edelim} & }} 2>/dev/null" f"{{ echo; echo {sdelim}; {cmd} && echo {edelim} || echo {edelim} & }} 2>/dev/null"
) )
else: else:
# This is dangerous. We are in raw mode, and if the process never # This is dangerous. We are in raw mode, and if the process never
# ends and doesn't provide a way to exit, then we are stuck. # ends and doesn't provide a way to exit, then we are stuck.
command.append(f"echo; echo {sdelim}; {cmd}; echo {edelim}") commands.append(f"echo; echo {sdelim}; {cmd}; echo {edelim}")
# Re-enable normal job control in bash # Re-enable normal job control in bash
command.append("set -m") commands.append("set -m")
# Join them all into one command # Join them all into one command
command = ";".join(command).encode("utf-8") command = ";".join(commands).encode("utf-8")
# Enter raw mode w/ no echo on the remote terminal # Enter raw mode w/ no echo on the remote terminal
# DANGER # DANGER
@ -1069,7 +740,7 @@ class PtyHandler:
self.client.sendall(command + b"\n") self.client.sendall(command + b"\n")
while not self.recvuntil("\n").startswith(sdelim.encode("utf-8")): while not self.recvuntil(b"\n").startswith(sdelim.encode("utf-8")):
continue continue
# Send the data if requested # Send the data if requested
@ -1380,95 +1051,6 @@ class PtyHandler:
return result return result
def restore(self):
""" Restore the terminal state """
util.restore_terminal(self.saved_term_state)
self.state = State.NORMAL
def setup_command_parsers(self):
""" Setup the argparsers for the different local commands """
self.upload_parser = argparse.ArgumentParser(prog="upload")
self.upload_parser.add_argument(
"--method",
"-m",
choices=["", *uploader.get_names()],
default=None,
help="set the download method (default: auto)",
)
self.upload_parser.add_argument(
"--output",
"-o",
default="./{basename}",
help="path to the output file (default: basename of input)",
)
self.upload_parser.add_argument("path", help="path to the file to upload")
self.download_parser = argparse.ArgumentParser(prog="download")
self.download_parser.add_argument(
"--method",
"-m",
choices=downloader.get_names(),
default=None,
help="set the download method (default: auto)",
)
self.download_parser.add_argument(
"--output",
"-o",
default="./{basename}",
help="path to the output file (default: basename of input)",
)
self.download_parser.add_argument("path", help="path to the file to download")
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

@ -6,6 +6,7 @@ from prompt_toolkit.shortcuts import ProgressBar
from functools import partial from functools import partial
from colorama import Fore, Style from colorama import Fore, Style
from io import TextIOWrapper from io import TextIOWrapper
from enum import Enum, auto
import netifaces import netifaces
import socket import socket
import string import string
@ -24,6 +25,15 @@ CTRL_C = b"\x03"
ALPHANUMERIC = string.ascii_letters + string.digits ALPHANUMERIC = string.ascii_letters + string.digits
class State(Enum):
""" The current PtyHandler state """
NORMAL = auto()
RAW = auto()
COMMAND = auto()
SINGLE = auto()
def human_readable_size(size, decimal_places=2): def human_readable_size(size, decimal_places=2):
for unit in ["B", "KiB", "MiB", "GiB", "TiB"]: for unit in ["B", "KiB", "MiB", "GiB", "TiB"]:
if size < 1024.0: if size < 1024.0:
@ -131,9 +141,6 @@ def enter_raw_mode():
returns: the old state of the terminal returns: the old state of the terminal
""" """
info("setting terminal to raw mode and disabling echo", overlay=True)
success("pwncat is ready 🐈\n", overlay=True)
# Ensure we don't have any weird buffering issues # Ensure we don't have any weird buffering issues
sys.stdout.flush() sys.stdout.flush()
@ -176,7 +183,6 @@ def restore_terminal(state):
# tty.setcbreak(sys.stdin) # tty.setcbreak(sys.stdin)
fcntl.fcntl(sys.stdin, fcntl.F_SETFL, state[1]) fcntl.fcntl(sys.stdin, fcntl.F_SETFL, state[1])
sys.stdout.write("\n") sys.stdout.write("\n")
info("local terminal restored")
def get_ip_addr() -> str: def get_ip_addr() -> str: