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

Merged master

This commit is contained in:
Caleb Stewart 2020-05-14 22:21:20 -04:00
commit 7655b40698
14 changed files with 1250 additions and 689 deletions

View File

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

361
pwncat/commands/__init__.py Normal file
View File

@ -0,0 +1,361 @@
#!/usr/bin/env python3
from prompt_toolkit import PromptSession, ANSI
from prompt_toolkit.shortcuts import ProgressBar
from prompt_toolkit.completion import (
Completer,
PathCompleter,
Completion,
CompleteEvent,
NestedCompleter,
WordCompleter,
merge_completers,
)
from pygments.lexer import RegexLexer, bygroups, include
from pygments.token import *
from pygments.style import Style
from prompt_toolkit.styles.pygments import style_from_pygments_cls
from prompt_toolkit.lexers import PygmentsLexer
from prompt_toolkit.document import Document
from pygments.styles import get_style_by_name
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.history import InMemoryHistory
from typing import Dict, Any, List, Iterable
from enum import Enum, auto
import argparse
import pkgutil
import shlex
import os
import re
from pprint import pprint
from pwncat.commands.base import CommandDefinition, Complete
from pwncat.util import State
from pwncat import util
class CommandParser:
""" Handles dynamically loading command classes, parsing input, and
dispatching commands. """
def __init__(self, pty: "pwncat.pty.PtyHandler"):
""" We need to dynamically load commands from pwncat.commands """
self.commands: List["CommandDefinition"] = []
for loader, module_name, is_pkg in pkgutil.walk_packages(__path__):
if module_name == "base":
continue
self.commands.append(
loader.find_module(module_name)
.load_module(module_name)
.Command(pty, self)
)
history = InMemoryHistory()
completer = CommandCompleter(pty, self.commands)
lexer = PygmentsLexer(CommandLexer.build(self.commands))
style = style_from_pygments_cls(get_style_by_name("monokai"))
auto_suggest = AutoSuggestFromHistory()
self.prompt = PromptSession(
[
("fg:ansiyellow bold", "(local) "),
("fg:ansimagenta bold", "pwncat"),
("", "$ "),
],
completer=completer,
lexer=lexer,
style=style,
auto_suggest=auto_suggest,
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
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):
self.running = True
while self.running:
try:
try:
line = self.prompt.prompt().strip()
except (EOFError, OSError):
self.pty.state = State.RAW
self.running = False
continue
if line == "":
continue
self.dispatch_line(line)
except KeyboardInterrupt:
continue
def dispatch_line(self, line: str):
""" Parse the given line of command input and dispatch a command """
try:
# Spit the line with shell rules
argv = shlex.split(line)
except ValueError as e:
util.error(e.args[0])
return
# Search for a matching command
for command in self.commands:
if command.PROG == argv[0]:
break
else:
util.error(f"{argv[0]}: unknown command")
return
try:
# Parse the arguments
args = command.parser.parse_args(argv[1:])
# Run the command
command.run(args)
except SystemExit:
# The arguments were icncorrect
return
class CommandLexer(RegexLexer):
tokens = {}
@classmethod
def build(cls, commands: List["CommandDefinition"]) -> "CommandLexer":
""" Build the RegexLexer token list from the command definitions """
root = []
for command in commands:
root.append(("^" + re.escape(command.PROG), Name.Function, command.PROG))
mode = []
for args, descr in command.ARGS.items():
for arg in args.split(","):
if not arg.startswith("-"):
continue
if descr[0] != Complete.NONE:
# Enter param state
mode.append((r"\s+" + re.escape(arg), descr[1], "param"))
else:
# Don't enter param state
mode.append((r"\s+" + re.escape(arg), descr[1]))
mode.append((r"\s+(\-\-help|\-h)", Name.Label))
mode.append((r"\"", String, "string"))
mode.append((r".", Text))
cls.tokens[command.PROG] = mode
root.append((r".", Text))
cls.tokens["root"] = root
cls.tokens["param"] = [
(r"\"", String, "string"),
(r"\s", Text, "#pop"),
(r"[^\s]", Text),
]
cls.tokens["string"] = [
(r"[^\"\\]+", String),
(r"\\.", String.Escape),
('"', String, "#pop"),
]
return cls
class RemotePathCompleter(Completer):
""" Complete remote file names/paths """
def __init__(self, pty: "pwncat.pty.PtyHandler"):
self.pty = pty
def get_completions(self, document: Document, complete_event: CompleteEvent):
before = document.text_before_cursor.split()[-1]
path, partial_name = os.path.split(before)
if path == "":
path = "."
pipe = self.pty.subprocess(f"ls -1 -a {shlex.quote(path)}", "r")
for name in pipe:
name = name.decode("utf-8").strip()
if name.startswith(partial_name):
yield Completion(
name,
start_position=-len(partial_name),
display=[("#ff0000", "(remote)"), ("", f" {name}")],
)
class LocalPathCompleter(Completer):
""" Complete local file names/paths """
def __init__(self, pty: "PtyHandler"):
self.pty = pty
def get_completions(self, document: Document, complete_event: CompleteEvent):
before = document.text_before_cursor.split()[-1]
path, partial_name = os.path.split(before)
if path == "":
path = "."
# Ensure the directory exists
if not os.path.isdir(path):
return
for name in os.listdir(path):
if name.startswith(partial_name):
yield Completion(
name,
start_position=-len(partial_name),
display=[("fg:ansiyellow", "(local)"), ("", f" {name}")],
)
class CommandCompleter(Completer):
""" Complete commands from a given list of commands """
def __init__(
self, pty: "pwncat.pty.PtyHandler", commands: List["CommandDefinition"]
):
""" Construct a new command completer """
self.layers = {}
local_file_completer = LocalPathCompleter(pty)
remote_file_completer = RemotePathCompleter(pty)
for command in commands:
self.layers[command.PROG] = [None, [], {}]
option_names = []
positional_completers = []
for name_list, descr in command.ARGS.items():
name_list = name_list.split(",")
if descr[0] == Complete.CHOICES:
completer = WordCompleter(descr[3]["choices"])
elif descr[0] == Complete.LOCAL_FILE:
completer = local_file_completer
elif descr[0] == Complete.REMOTE_FILE:
completer = remote_file_completer
elif descr[0] == Complete.NONE:
completer = None
if len(name_list) == 1 and not name_list[0].startswith("-"):
self.layers[command.PROG][1].append(completer)
else:
for name in name_list:
self.layers[command.PROG][2][name] = completer
option_names.append(name)
self.layers[command.PROG][0] = WordCompleter(
option_names + ["--help", "-h"]
)
self.completer = WordCompleter(list(self.layers))
def get_completions(
self, document: Document, complete_event: CompleteEvent
) -> Iterable[Completion]:
""" Get a list of completions for the given document """
text = document.text_before_cursor.lstrip()
try:
args = shlex.split(text)
except ValueError:
try:
args = shlex.split(text + '"')
except ValueError:
args = shlex.split(text + "'")
# We haven't finished typing the command. Use our word completer for
# commands
if text == "" or (len(args) == 1 and not text.endswith(" ")):
yield from self.completer.get_completions(document, complete_event)
return
# Not in a known command, can't autocomplete
if args[0] not in self.layers:
return
command = self.layers[args[0]]
args = args[1:]
next_completer = command[0]
this_completer = command[0]
positional = 0
# state = "options", completing options next
# state = "arguments", completing arguments to options next
state = "options"
for arg in args:
if state == "options":
# Flag options
if arg.startswith("-"):
# Exact match, with a sub-completer
if arg in command[2] and command[2][arg] is not None:
# Completer for next argument
next_completer = command[2][arg]
state = "arguments"
# Exact match, with no arguments
elif arg in command[2]:
# Command has no argument, next completer is options
# completer
next_completer = command[0]
state = "options"
this_completer = command[0]
# Non-exact match
else:
next_completer = command[0]
this_completer = command[0]
state = "options"
# Appears to be a positional argument, grab next positional
# completer and increment positional count
else:
if positional < len(command[1]):
this_completer = command[1][positional]
next_completer = command[0]
state = "options"
positional += 1
else:
this_completer = command[0]
next_completer = command[0]
state = "options"
else:
# Completing an argument to a option/switch. We can't verify
# it's legitimacy, so we assume it's right, and reset to a
# default state.
state = "options"
this_completer = next_completer
next_completer = command[0]
if text.endswith(" "):
yield from next_completer.get_completions(document, complete_event)
else:
yield from this_completer.get_completions(document, complete_event)

13
pwncat/commands/back.py Normal file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env python3
from pwncat.commands.base import CommandDefinition, Complete, parameter
from pwncat import util
class Command(CommandDefinition):
""" Return to the remote terminal """
PROG = "back"
ARGS = {}
def run(self, args):
self.pty.state = util.State.RAW

155
pwncat/commands/base.py Normal file
View File

@ -0,0 +1,155 @@
#!/usr/bin/env python3
from typing import Dict, Any, List, Callable
from pygments.token import *
from enum import Enum, auto
from functools import partial
import argparse
import shlex
import os
class Complete(Enum):
# Complete from the choices array in kwargs
CHOICES = auto()
# Complete from a local file
LOCAL_FILE = auto()
# Complete from a remote file
REMOTE_FILE = auto()
# This argument has no parameter
NONE = auto()
class StoreConstOnce(argparse.Action):
""" Only allow the user to store a value in the destination once. This prevents
users from selection multiple actions in the privesc parser. """
def __call__(self, parser, namespace, values, option_string=None):
if hasattr(self, "__" + self.dest + "_seen"):
raise argparse.ArgumentError(self, "only one action may be specified")
setattr(self, "__" + self.dest + "_seen", True)
setattr(namespace, self.dest, self.const)
def StoreForAction(action: List[str]) -> Callable:
""" Generates a custom argparse Action subclass which verifies that the current
selected "action" option is one of the provided actions in this function. If
not, an error is raised. """
class StoreFor(argparse.Action):
""" Store the value if the currently selected action matches the list of
actions passed to this function. """
def __call__(self, parser, namespace, values, option_string=None):
if getattr(namespace, "action", None) not in action:
raise argparse.ArgumentError(
self, f"{option_string}: only valid for {action}",
)
setattr(namespace, self.dest, values)
return StoreFor
def RemoteFileType(file_exist=True, directory_exist=False):
def _type(command: "CommandDefinition", name: str):
""" Ensures that the remote file named exists. This should only be used for
arguments which represent files on the remote system which should be viewable
by the running user (e.g. not helpful for privesc methods). """
# Attempt to find the "test" command
test = command.pty.which("test")
if test is None:
test = command.pty.which("[")
# No test command, this is a nicety, not a necessity.
if test is None:
return name
# Check if the file exists
if file_exist:
result = command.pty.run(f"{test} -f {shlex.quote(name)} && echo exists")
if b"exists" not in result:
raise argparse.ArgumentTypeError(f"{name}: no such file or directory")
elif directory_exist:
dirpath = os.path.dirname(name)
result = command.pty.run(f"{test} -d {shlex.quote(dirpath)} && echo exists")
if b"exists" not in result:
raise argparse.ArgumentTypeError(
f"{dirpath}: no such file or directory"
)
# it exists
return name
return _type
def parameter(complete, token=Name.Label, *args, **kwargs):
""" Build a parameter tuple from argparse arguments """
return (complete, token, args, kwargs)
class CommandDefinition:
""" Default help/description goes here """
PROG = "unimplemented"
ARGS = {}
DEFAULTS = {}
# An example definition of arguments
# PROG = "command"
# ARGS = {
# "--all,-a": parameter(
# Complete.NONE, action="store_true", help="A switch/option"
# ),
# "--file,-f": parameter(Complete.LOCAL_FILE, help="A local file"),
# "--rfile": parameter(Complete.REMOTE_FILE, help="A remote file"),
# "positional": parameter(
# Complete.CHOICES, choices=["a", "b", "c"], help="Choose one!"
# ),
# }
def __init__(self, pty: "pwncat.pty.PtyHandler", cmdparser: "CommandParser"):
""" Initialize a new command instance. Parse the local arguments array
into an argparse object. """
self.pty = pty
self.cmdparser = cmdparser
# Create the parser object
self.parser = argparse.ArgumentParser(prog=self.PROG, description=self.__doc__)
self.build_parser(self.parser, self.ARGS)
def run(self, args):
""" Perform whatever your command is. `args` has already been parsed with
your argparse definitions. """
raise NotImplementedError
def build_parser(self, parser: argparse.ArgumentParser, args: Dict[str, Any]):
""" Fill the given parser with arguments based on the dict """
for arg, descr in args.items():
names = arg.split(",")
# Patch choice to work with a callable
if "choices" in descr[3] and callable(descr[3]["choices"]):
method = descr[3]["choices"]
class wrapper:
def __iter__(wself):
yield from method(self)
descr[3]["choices"] = wrapper()
# Patch "type" so we can see "self"
if (
"type" in descr[3]
and isinstance(descr[3]["type"], tuple)
and descr[3]["type"][0] == "method"
):
descr[3]["type"] = partial(descr[3]["type"][1], self)
parser.add_argument(*names, *descr[2], **descr[3])
parser.set_defaults(**self.DEFAULTS)

View File

@ -0,0 +1,78 @@
#!/usr/bin/env python3
from colorama import Fore
from pwncat.commands.base import (
CommandDefinition,
Complete,
parameter,
StoreConstOnce,
StoreForAction,
)
from pwncat import util
class Command(CommandDefinition):
""" Manage installation of a known-good busybox binary on the remote system.
After installing busybox, pwncat will be able to utilize it's functionality
to augment or stabilize local binaries. This command can download a remote
busybox binary appropriate for the remote architecture and then upload it
to the remote system. """
PROG = "busybox"
ARGS = {
"--list,-l": parameter(
Complete.NONE,
action=StoreConstOnce,
nargs=0,
const="list",
dest="action",
help="List applets which the remote busybox provides",
),
"--install,-i": parameter(
Complete.NONE,
action=StoreConstOnce,
nargs=0,
const="install",
dest="action",
help="Install busybox on the remote host for use with pwncat",
),
"--status,-s": parameter(
Complete.NONE,
action=StoreConstOnce,
nargs=0,
const="status",
dest="action",
help="List the current busybox installation status",
),
"--url,-u": parameter(
Complete.NONE,
action=StoreForAction(["install"]),
nargs=1,
help="The base URL to download busybox binaries from (default: 1.31.0-defconfig-multiarch-musl)",
default=(
"https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/"
),
),
}
DEFAULTS = {"action": "status"}
def run(self, args):
if args.action == "list":
if not self.pty.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.pty.busybox_provides:
print(f" * {name}")
elif args.action == "status":
if not self.pty.has_busybox:
util.error("busybox hasn't been installed yet")
return
util.info(
f"busybox is installed to: {Fore.BLUE}{self.pty.busybox_path}{Fore.RESET}"
)
util.info(
f"busybox provides {Fore.GREEN}{len(self.pty.busybox_provides)}{Fore.RESET} applets"
)
elif args.action == "install":
self.pty.bootstrap_busybox(args.url)

View File

@ -0,0 +1,51 @@
#!/usr/bin/env python3
from pwncat.commands.base import (
CommandDefinition,
Complete,
parameter,
StoreConstOnce,
StoreForAction,
RemoteFileType,
)
from functools import partial
from colorama import Fore
from pwncat import util
import argparse
import datetime
import time
import os
class Command(CommandDefinition):
""" Download a file from the remote host to the local host"""
PROG = "download"
ARGS = {
"source": parameter(Complete.REMOTE_FILE),
"destination": parameter(Complete.LOCAL_FILE),
}
def run(self, args):
try:
length = self.pty.get_file_size(args.source)
started = time.time()
with open(args.destination, "wb") as destination:
with self.pty.open(args.source, "rb", length=length) as source:
util.with_progress(
[
("", "downloading "),
("fg:ansigreen", args.source),
("", " to "),
("fg:ansired", args.destination),
],
partial(util.copyfileobj, source, destination),
length=length,
)
elapsed = time.time() - started
util.success(
f"downloaded {Fore.CYAN}{util.human_readable_size(length)}{Fore.RESET} "
f"in {Fore.GREEN}{util.human_readable_delta(elapsed)}{Fore.RESET}"
)
except (FileNotFoundError, PermissionError, IsADirectoryError) as exc:
self.parser.error(str(exc))

25
pwncat/commands/help.py Normal file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
from pwncat.commands.base import CommandDefinition, Complete, parameter
from pwncat import util
class Command(CommandDefinition):
""" List known commands and print their associated help documentation. """
def get_command_names(self):
""" Get the list of known commands """
return [c.PROG for c in self.cmdparser.commands]
PROG = "help"
ARGS = {"topic": parameter(Complete.CHOICES, choices=get_command_names, nargs="?")}
def run(self, args):
if args.topic:
for command in self.cmdparser.commands:
if command.PROG == args.topic:
command.parser.print_help()
break
else:
util.info("the following commands are available:")
for command in self.cmdparser.commands:
print(f" * {command.PROG}")

180
pwncat/commands/privesc.py Normal file
View File

@ -0,0 +1,180 @@
#!/usr/bin/env python3
from typing import List, Callable
from pwncat.commands.base import (
CommandDefinition,
Complete,
parameter,
StoreConstOnce,
StoreForAction,
)
from pwncat import util, privesc
from pwncat.util import State
from colorama import Fore
import argparse
import shutil
import sys
class Command(CommandDefinition):
""" Attempt various privilege escalation methods. This command will attempt
search for privilege escalation across all known modules. Privilege escalation
routes can grant file read, file write or shell capabilities. The "escalate"
mode will attempt to abuse any of these to gain a shell.
Further, escalation and file read/write actions will attempt to escalate multiple
times to reach the target user if possible, attempting all known escalation paths
until one arrives at the target user. """
def get_user_choices(self):
""" Get a list of all users on the remote machine. This is used for
parameter checking and tab completion of the "users" parameter below. """
return list(self.pty.users)
PROG = "privesc"
ARGS = {
"--list,-l": parameter(
Complete.NONE,
action=StoreConstOnce,
nargs=0,
const="list",
dest="action",
help="Enumerate and list available privesc techniques",
),
"--all,-a": parameter(
Complete.NONE,
action="store_const",
dest="user",
const=None,
help="list escalations for all users",
),
"--user,-u": parameter(
Complete.CHOICES,
default="root",
choices=get_user_choices,
metavar="USER",
help="the user to gain privileges as",
),
"--max-depth,-m": parameter(
Complete.NONE,
default=None,
type=int,
help="Maximum depth for the privesc search (default: no maximum)",
),
"--read,-r": parameter(
Complete.NONE,
action=StoreConstOnce,
nargs=0,
const="read",
dest="action",
help="Attempt to read a remote file as the specified user",
),
"--write,-w": parameter(
Complete.NONE,
action=StoreConstOnce,
nargs=0,
const="write",
dest="action",
help="Attempt to write a remote file as the specified user",
),
"--path,-p": parameter(
Complete.REMOTE_FILE,
action=StoreForAction(["write", "read"]),
help="Remote path for read or write actions",
),
"--escalate,-e": parameter(
Complete.NONE,
action=StoreConstOnce,
nargs=0,
const="escalate",
dest="action",
help="Attempt to escalate to gain a full shell as the target user",
),
"--data,-d": parameter(
Complete.LOCAL_FILE,
action=StoreForAction(["write"]),
default=None,
help="The local file to write to the remote file",
),
}
DEFAULTS = {"action": "list"}
def run(self, args):
if args.action == "list":
techniques = self.pty.privesc.search(args.user)
if len(techniques) == 0:
util.warn("no techniques found")
else:
for tech in techniques:
util.info(f" - {tech}")
elif args.action == "read":
if not args.path:
self.parser.error("missing required argument: --path")
try:
read_pipe, chain = self.pty.privesc.read_file(
args.path, args.user, args.max_depth
)
util.success("file successfully opened!")
# Read the data from the pipe
shutil.copyfileobj(read_pipe, sys.stdout.buffer)
read_pipe.close()
# Unwrap in case we had to privesc to get here
self.pty.privesc.unwrap(chain)
except privesc.PrivescError as exc:
util.error(f"read file failed: {exc}")
elif args.action == "write":
# Make sure the correct arguments are present
if not args.path:
self.parser.error("missing required argument: --path")
if not args.data:
self.parser.error("missing required argument: --data")
# Read in the data file
with open(args.data, "rb") as f:
data = f.read()
try:
# Attempt to write the data to the remote file
chain = self.pty.privesc.write_file(
args.path, data, target_user=args.user, depth=args.max_depth,
)
self.pty.privesc.unwrap(chain)
util.success("file written successfully!")
except privesc.PrivescError as exc:
util.error(f"file write failed: {exc}")
elif args.action == "escalate":
try:
chain = self.pty.privesc.escalate(args.user, args.max_depth)
ident = self.pty.id
backdoor = False
if ident["euid"]["id"] == 0 and ident["uid"]["id"] != 0:
util.progress(
"EUID != UID. installing backdoor to complete privesc"
)
try:
self.pty.privesc.add_backdoor()
backdoor = True
except privesc.PrivescError as exc:
util.warn(f"backdoor installation failed: {exc}")
util.success("privilege escalation succeeded using:")
for i, (technique, _) in enumerate(chain):
arrow = f"{Fore.YELLOW}\u2ba1{Fore.RESET} "
print(f"{(i+1)*' '}{arrow}{technique}")
if backdoor:
print(
(
f"{(len(chain)+1)*' '}{arrow}"
f"{Fore.YELLOW}pwncat{Fore.RESET} backdoor"
)
)
self.pty.reset()
self.pty.state = State.RAW
except privesc.PrivescError as 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")

26
pwncat/commands/test.py Normal file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
from pygments.token import *
from pwncat.commands.base import CommandDefinition, Complete, parameter
class Command(CommandDefinition):
""" Command Description """
PROG = "test"
ARGS = {
"--remote,-r": parameter(
Complete.REMOTE_FILE, Name.LABEL, help="Argument Help"
),
"--local,-l": parameter(Complete.LOCAL_FILE, Name.LABEL, help="Argument Help"),
"--choice,-c": parameter(
Complete.CHOICES,
Name.LABEL,
choices=["one", "two", "three"],
help="Select one!",
),
}
def run(self, args):
print(args.arg)

56
pwncat/commands/upload.py Normal file
View File

@ -0,0 +1,56 @@
#!/usr/bin/env python3
from pwncat.commands.base import (
CommandDefinition,
Complete,
parameter,
StoreConstOnce,
StoreForAction,
RemoteFileType,
)
from functools import partial
from colorama import Fore
from pwncat import util
import argparse
import datetime
import time
import os
class Command(CommandDefinition):
""" Upload a file from the local host to the remote host"""
PROG = "upload"
ARGS = {
"source": parameter(Complete.LOCAL_FILE),
"destination": parameter(
Complete.REMOTE_FILE,
type=("method", RemoteFileType(file_exist=False, directory_exist=True)),
),
}
def run(self, args):
try:
length = os.path.getsize(args.source)
started = time.time()
with open(args.source, "rb") as source:
with self.pty.open(
args.destination, "wb", length=length
) as destination:
util.with_progress(
[
("", "uploading "),
("fg:ansigreen", args.source),
("", " to "),
("fg:ansired", args.destination),
],
partial(util.copyfileobj, source, destination),
length=length,
)
elapsed = time.time() - started
util.success(
f"uploaded {Fore.CYAN}{util.human_readable_size(length)}{Fore.RESET} "
f"in {Fore.GREEN}{util.human_readable_delta(elapsed)}{Fore.RESET}"
)
except (FileNotFoundError, PermissionError, IsADirectoryError) as exc:
self.parser.error(str(exc))

View File

@ -22,7 +22,6 @@ from pwncat import util
# privesc_methods = [SetuidMethod, SuMethod]
# privesc_methods = [SuMethod, SudoMethod, SetuidMethod, DirtycowMethod, ScreenMethod]
privesc_methods = [SuMethod, SudoMethod, SetuidMethod]
# privesc_methods = [SuMethod, SetuidMethod]
class Finder:

File diff suppressed because it is too large Load Diff

View File

@ -2,9 +2,11 @@
from typing import Tuple, BinaryIO, Callable
from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import TCPServer, BaseRequestHandler
from prompt_toolkit.shortcuts import ProgressBar
from functools import partial
from colorama import Fore, Style
from io import TextIOWrapper
from enum import Enum, auto
import netifaces
import socket
import string
@ -13,6 +15,7 @@ import threading
import logging
import termios
import fcntl
import time
import tty
import sys
import os
@ -22,6 +25,46 @@ CTRL_C = b"\x03"
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):
for unit in ["B", "KiB", "MiB", "GiB", "TiB"]:
if size < 1024.0:
break
size /= 1024.0
return f"{size:.{decimal_places}f}{unit}"
def human_readable_delta(seconds):
""" This produces a human-readable time-delta output suitable for output to
the terminal. It assumes that "seconds" is less than 1 day. I.e. it will only
display at most, hours minutes and seconds. """
if seconds < 60:
return f"{seconds:.2f} seconds"
output = []
output.append(f"{int(seconds%60)} seconds")
minutes = seconds // 60
output.append(f"{minutes % 60} minutes")
if minutes < 60:
return f"{output[1]} and {output[0]}"
hours = minutes // 60
output.append(f"{hours} hours")
return f"{output[2]}, {output[1]} and {output[0]}"
def copyfileobj(src, dst, callback, nomv=False):
""" Copy a file object to another file object with a callback.
This method assumes that both files are binary and support readinto
@ -39,7 +82,7 @@ def copyfileobj(src, dst, callback, nomv=False):
for chunk in iter(lambda: src.read(length), b""):
dst.write(chunk)
copied += len(chunk)
callback(copied, len(chunk))
callback(len(chunk))
else:
with memoryview(bytearray(length)) as mv:
while True:
@ -52,7 +95,39 @@ def copyfileobj(src, dst, callback, nomv=False):
else:
dst.write(mv)
copied += n
callback(copied, n)
callback(n)
def with_progress(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(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)
def random_string(length: int = 8):
@ -66,9 +141,6 @@ def enter_raw_mode():
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
sys.stdout.flush()
@ -111,7 +183,6 @@ def restore_terminal(state):
# tty.setcbreak(sys.stdin)
fcntl.fcntl(sys.stdin, fcntl.F_SETFL, state[1])
sys.stdout.write("\n")
info("local terminal restored")
def get_ip_addr() -> str: