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

Added initial configuration object and config script parsing including on_load hook

This commit is contained in:
Caleb Stewart 2020-05-15 04:24:42 -04:00
parent 9a73d076f0
commit 3fbb4076d1
7 changed files with 286 additions and 5 deletions

52
data/pwncatrc Normal file
View File

@ -0,0 +1,52 @@
# Set your remote hosts file
set lhost 127.0.0.1
# Set your command prefix
set prefix c-k
# Set the default private key to use for privilege escalation
set privkey "~/.pwncat/id_rsa"
# Set the pwncat backdoor user and password
set backdoor_user "pwncat"
set backdoor_pass "pwncat"
# This will fail because we haven't finished loading
busybox -i
set on_load {
# This will succeed because `on_load` runs after the session is established.
busybox -i
}
# This would run at start after a successful connection
# privesc -l
# map takes a key followed by an action. The key is assumed to be followed after
# the prefix, which is set with the "set" command above.
# # pass the a second "C-k" through to the remote host
# map C-k "pass"
#
# # enter local command mode via a different shortcut (vice C-d)
# map c "set state command"
#
# # synchronize remote and local terminals
# map s "sync"
#
# # open a quick local command entry prompt
# map : "set state single"
#
# # send C-d directly to the remote host if prefixed. By default, C-d toggles back
# # and forth between local and remote prompts. This allows you to send C-d to the
# # remote host while at the remote prompt
# map C-d "pass"
#
# # Specify a shortcut to a local shell command
# shortcut ! "local"
#
# # Specify a shortuct to a remote shell command
# shortcut @ "run"
#
# # Alias the upload command with "up"
# alias up "upload"
#
# # Alias the download command with "down"
# alias down "download"

View File

@ -56,6 +56,7 @@ def main():
help="Method to create a pty on the remote host (default: script)",
default="script",
)
parser.add_argument("--config", "-c", help="Configuration script", default=None)
args = parser.parse_args()
if args.type == "reverse":
@ -82,7 +83,7 @@ def main():
util.info(f"connection to {address[0]}:{address[1]} established", overlay=True)
# Create a PTY handler to proctor communications with the remote PTY
handler = PtyHandler(client)
handler = PtyHandler(client, args.config)
# Setup the selector to wait for data asynchronously from both streams
selector = selectors.DefaultSelector()

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3
from typing import TextIO
from prompt_toolkit import PromptSession, ANSI
from prompt_toolkit.shortcuts import ProgressBar
from prompt_toolkit.completion import (
@ -34,6 +35,63 @@ from pwncat.util import State
from pwncat import util
def resolve_blocks(source: str):
""" This is a dumb lexer that turns strings of text with code blocks (squigly
braces) into a single long string separated by semicolons. All code blocks are
converted to strings recursively with correct escaping levels. The resulting
string can be sent to break_commands to iterate over the commands. """
result = []
in_brace = False
inside_quotes = False
i = 0
lineno = 1
while i < len(source):
if not inside_quotes:
if source[i] == '"':
inside_quotes = True
result.append("\\" * int(in_brace) + '"')
elif source[i] == "{" and not in_brace:
result.append('"')
in_brace = True
elif source[i] == "}":
if not in_brace:
raise ValueError(f"line {lineno}: mismatched closing brace")
in_brace = False
result.append('"')
elif source[i] == "\\":
result.append("\\" * (int(in_brace)))
elif source[i] == "\n" and in_brace:
result.append("\\n")
elif source[i] == "#":
# Comment
while i < len(source) and source[i] != "\n":
i += 1
else:
result.append(source[i])
else:
if source[i] == '"':
inside_quotes = False
result.append("\\" * int(in_brace) + '"')
elif source[i] == "\\":
result.append("\\" * (in_brace + 1))
elif source[i] == "\n":
raise ValueError(f"line {lineno}: newlines cannot appear in strings")
else:
result.append(source[i])
if source[i] == "\n":
lineno += 1
i += 1
if in_brace:
raise ValueError(f"mismatched braces")
if inside_quotes:
raise ValueError("missing ending quote")
return "".join(result).split("\n")
class CommandParser:
""" Handles dynamically loading command classes, parsing input, and
dispatching commands. """
@ -87,6 +145,33 @@ class CommandParser:
)
self.pty = pty
self.loading_complete = False
@property
def loaded(self):
return self.loading_complete
@loaded.setter
def loaded(self, value: bool):
assert value == True
self.loading_complete = True
self.eval(self.pty.config["on_load"], "on_load")
def eval(self, source: str, name: str = "<script>"):
""" Evaluate the given source file. This will execute the given string
as a script of commands. Syntax is the same except that commands may
be separated by semicolons, comments are accepted as following a "#" and
multiline strings are supported with '"{' and '}"' as delimeters. """
in_multiline_string = False
lineno = 1
for command in resolve_blocks(source):
try:
self.dispatch_line(command)
except Exception as exc:
util.error(f"{name}: command: {str(exc)}")
break
def run_single(self):
@ -121,6 +206,11 @@ class CommandParser:
def dispatch_line(self, line: str):
""" Parse the given line of command input and dispatch a command """
# Account for blank or whitespace only lines
line = line.strip()
if line == "":
return
try:
# Spit the line with shell rules
argv = shlex.split(line)
@ -136,9 +226,17 @@ class CommandParser:
util.error(f"{argv[0]}: unknown command")
return
if not self.loading_complete and not command.LOCAL:
util.error(
f"{argv[0]}: non-local commands cannot run until after session setup."
)
return
args = [a.encode("utf-8").decode("unicode_escape") for a in argv[1:]]
try:
# Parse the arguments
args = command.parser.parse_args(argv[1:])
args = command.parser.parse_args(args)
# Run the command
command.run(args)

View File

@ -95,6 +95,7 @@ class CommandDefinition:
PROG = "unimplemented"
ARGS = {}
DEFAULTS = {}
LOCAL = False
# An example definition of arguments
# PROG = "command"

46
pwncat/commands/set.py Normal file
View File

@ -0,0 +1,46 @@
#!/usr/bin/env python3
from colorama import Fore
from pwncat.commands.base import CommandDefinition, Complete, parameter
from pwncat import util
class Command(CommandDefinition):
""" Set variable runtime variable parameters for pwncat """
def get_config_variables(self):
return list(self.pty.config.values)
PROG = "set"
ARGS = {
"variable": parameter(
Complete.CHOICES,
nargs="?",
choices=get_config_variables,
metavar="VARIABLE",
help="the variable name to modify",
),
"value": parameter(
Complete.LOCAL_FILE, nargs="?", help="the value for the given variable"
),
}
LOCAL = True
def run(self, args):
if args.variable is not None and args.value is not None:
try:
self.pty.config[args.variable] = args.value
except ValueError as exc:
util.error(str(exc))
elif args.variable is not None:
value = self.pty.config[args.variable]
print(
f" {Fore.CYAN}{args.variable}{Fore.RESET} = "
f"{Fore.YELLOW}{repr(value)}{Fore.RESET}"
)
else:
for name in self.pty.config:
value = self.pty.config[name]
print(
f" {Fore.CYAN}{name}{Fore.RESET} = "
f"{Fore.YELLOW}{repr(value)}{Fore.RESET}"
)

75
pwncat/config.py Normal file
View File

@ -0,0 +1,75 @@
#!/usr/bin/env python3
from prompt_toolkit.input.ansi_escape_sequences import REVERSE_ANSI_SEQUENCES
from prompt_toolkit.keys import ALL_KEYS, Keys
from typing import Any, Dict, List
import commentjson as json
import ipaddress
import re
import os
def key_type(value: str) -> bytes:
""" Converts a key name to a ansi keycode. The value can either be a single
printable character or a named key from prompt_toolkit Keys """
if len(value) == 1:
return value.encode("utf-8")
if value not in ALL_KEYS:
raise ValueError(f"invalid key: {value}")
key = [key for key in Keys if key.value == value][0]
return REVERSE_ANSI_SEQUENCES[key].encode("utf-8")
class KeyType:
def __init__(self, name: str):
if len(name) == 1:
self.name = name
self.value = name.encode("utf-8")
else:
if name not in ALL_KEYS:
raise ValueError(f"{name}: invalid key")
key = [key for key in Keys if key.value == name][0]
self.name = name
self.value = REVERSE_ANSI_SEQUENCES[key].encode("utf-8")
def __repr__(self):
return f"Key(name={repr(self.name)})"
def __bytes__(self):
return self.value
def local_file_type(value: str) -> str:
""" Ensure the local file exists """
if not os.path.isfile(value):
raise ValueError(f"{value}: no such file or directory")
return value
class Config:
def __init__(self, pty: "pwncat.pty.PtyHandler"):
# Basic key-value store w/ typing
self.values: Dict[str, Dict[str, Any]] = {
"lhost": {"value": None, "type": ipaddress.ip_address},
"prefix": {"value": "C-k", "type": KeyType},
"privkey": {"value": "data/pwncat", "type": local_file_type},
"backdoor_user": {"value": "pwncat", "type": str},
"backdoor_pass": {"value": "pwncat", "type": str},
"on_load": {"value": "", "type": str},
}
# Map ascii escape sequences or printable bytes to lists of commands to
# run.
self.bindings: Dict[bytes, str] = {}
def __getitem__(self, name: str) -> Any:
""" Get a configuration item """
return self.values[name]["value"]
def __setitem__(self, name: str, value: Any):
""" Set a configuration item """
item = self.values[name]
item["value"] = item["type"](value)
def __iter__(self):
yield from self.values

View File

@ -38,6 +38,7 @@ from pwncat.file import RemoteBinaryPipe
from pwncat.lexer import LocalCommandLexer, PwncatStyle
from pwncat.gtfobins import GTFOBins, Capability, Stream
from pwncat.commands import CommandParser
from pwncat.config import Config
from colorama import Fore
@ -179,11 +180,12 @@ class PtyHandler:
"script",
]
def __init__(self, client: socket.SocketType, has_pty: bool = False):
def __init__(self, client: socket.SocketType, config_path: str):
""" Initialize a new Pty Handler. This will handle creating the PTY and
setting the local terminal to raw. It also maintains the state to open a
local terminal if requested and exit raw mode. """
self.config = Config(self)
self.client = client
self._state = State.COMMAND
self.saved_term_state = None
@ -212,9 +214,14 @@ class PtyHandler:
"sh": ["bash", "zsh", "dash"],
"nc": ["netcat", "ncat"],
}
self.has_pty = has_pty
self.gtfo: GTFOBins = GTFOBins("data/gtfobins.json", self.which)
self.default_privkey = "./data/pwncat"
self.command_parser = CommandParser(self)
# Run the configuration script
with open(config_path, "r") as filp:
config_script = filp.read()
self.command_parser.eval(config_script, config_path)
# We should always get a response within 3 seconds...
self.client.settimeout(1)
@ -348,7 +355,8 @@ class PtyHandler:
# Save our terminal state
self.stty_saved = self.run("stty -g").decode("utf-8").strip()
self.command_parser = CommandParser(self)
# The session is fully setup now
self.command_parser.loaded = True
# Synchronize the terminals
self.command_parser.dispatch_line("sync")