1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-30 20:34:15 +01:00

Added bind and alias commands to fully control configuration through command scripting.

This commit is contained in:
Caleb Stewart 2020-05-15 14:05:51 -04:00
parent ded22f18e4
commit 82ea5799d8
7 changed files with 180 additions and 34 deletions

View File

@ -9,13 +9,27 @@ set backdoor_user "pwncat"
set backdoor_pass "pwncat" set backdoor_pass "pwncat"
# This will fail because we haven't finished loading # This will fail because we haven't finished loading
busybox -i
set on_load { set on_load {
# This will succeed because `on_load` runs after the session is established. # This will succeed because `on_load` runs after the session is established.
busybox -i
} }
# Examples of command bindings
bind s "sync"
bind c "set state command"
bind b {
busybox --install
busybox --status
}
bind p {
privesc --list
}
# Create shortcut aliases for commands
alias up upload
alias down download
alias down
# This would run at start after a successful connection # This would run at start after a successful connection
# privesc -l # privesc -l

View File

@ -21,6 +21,7 @@ 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 prompt_toolkit.history import InMemoryHistory
from typing import Dict, Any, List, Iterable from typing import Dict, Any, List, Iterable
from colorama import Fore
from enum import Enum, auto from enum import Enum, auto
import argparse import argparse
import pkgutil import pkgutil
@ -146,6 +147,7 @@ class CommandParser:
self.pty = pty self.pty = pty
self.loading_complete = False self.loading_complete = False
self.aliases: Dict[str, CommandDefinition] = {}
@property @property
def loaded(self): def loaded(self):
@ -170,7 +172,9 @@ class CommandParser:
try: try:
self.dispatch_line(command) self.dispatch_line(command)
except Exception as exc: except Exception as exc:
util.error(f"{name}: command: {str(exc)}") util.error(
f"{Fore.CYAN}{name}{Fore.RESET}: {Fore.YELLOW}{command}{Fore.RESET}: {str(exc)}"
)
break break
def run_single(self): def run_single(self):
@ -222,6 +226,9 @@ class CommandParser:
for command in self.commands: for command in self.commands:
if command.PROG == argv[0]: if command.PROG == argv[0]:
break break
else:
if argv[0] in self.aliases:
command = self.aliases[argv[0]]
else: else:
util.error(f"{argv[0]}: unknown command") util.error(f"{argv[0]}: unknown command")
return return

40
pwncat/commands/alias.py Normal file
View File

@ -0,0 +1,40 @@
#!/usr/bin/env python3
from pwncat.commands.base import CommandDefinition, Complete, parameter
from colorama import Fore
class Command(CommandDefinition):
""" Alias an existing command with a new name. Specifying no alias or command
will list all aliases. Specifying an alias with no command will remove the
alias if it exists. """
def get_command_names(self):
return [c.PROG for c in self.cmdparser.commands]
PROG = "alias"
ARGS = {
"alias": parameter(Complete.NONE, help="name for the new alias", nargs="?"),
"command": parameter(
Complete.CHOICES,
metavar="COMMAND",
choices=get_command_names,
help="the command the new alias will use",
nargs="?",
),
}
LOCAL = True
def run(self, args):
if args.alias is None:
for name, command in self.cmdparser.aliases.items():
print(
f" {Fore.CYAN}{name}{Fore.RESET} \u2192 "
f"{Fore.YELLOW}{command.PROG}{Fore.RESET}"
)
elif args.command is not None:
# This is safe because of "choices" in the argparser
self.cmdparser.aliases[args.alias] = [
c for c in self.cmdparser.commands if c.PROG == args.command
][0]
else:
del self.cmdparser.aliases[args.alias]

39
pwncat/commands/bind.py Normal file
View File

@ -0,0 +1,39 @@
#!/usr/bin/env python3
from prompt_toolkit.input.ansi_escape_sequences import REVERSE_ANSI_SEQUENCES
from prompt_toolkit.keys import ALL_KEYS, Keys
from pwncat.commands.base import CommandDefinition, Complete, parameter
from pwncat.config import KeyType
from pwncat import util
from colorama import Fore
import string
class Command(CommandDefinition):
PROG = "bind"
ARGS = {
"key": parameter(
Complete.NONE,
metavar="KEY",
type=KeyType,
help="The key to map after your prefix",
nargs="?",
),
"script": parameter(
Complete.NONE, help="The script to run when the key is pressed", nargs="?",
),
}
LOCAL = True
def run(self, args):
if args.key is None:
util.info("currently assigned key-bindings:")
for key, binding in self.pty.config.bindings.items():
print(
f" {Fore.CYAN}{key}{Fore.RESET} = {Fore.YELLOW}{repr(binding)}{Fore.RESET}"
)
elif args.key is not None and args.script is None:
if args.key in self.pty.config.bindings:
del self.pty.config.bindings[args.key]
else:
self.pty.config.bindings[args.key] = args.script

View File

@ -8,7 +8,7 @@ class Command(CommandDefinition):
""" Set variable runtime variable parameters for pwncat """ """ Set variable runtime variable parameters for pwncat """
def get_config_variables(self): def get_config_variables(self):
return list(self.pty.config.values) return ["state"] + list(self.pty.config.values)
PROG = "set" PROG = "set"
ARGS = { ARGS = {
@ -26,7 +26,16 @@ class Command(CommandDefinition):
LOCAL = True LOCAL = True
def run(self, args): def run(self, args):
if args.variable is not None and args.value is not None: if (
args.variable is not None
and args.variable == "state"
and args.value is not None
):
try:
self.pty.state = util.State._member_map_[args.value.upper()]
except KeyError:
util.error(f"{args.value}: invalid state")
elif args.variable is not None and args.value is not None:
try: try:
self.pty.config[args.variable] = args.value self.pty.config[args.variable] = args.value
except ValueError as exc: except ValueError as exc:

View File

@ -1,7 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from prompt_toolkit.input.ansi_escape_sequences import REVERSE_ANSI_SEQUENCES from prompt_toolkit.input.ansi_escape_sequences import (
REVERSE_ANSI_SEQUENCES,
ANSI_SEQUENCES,
)
from prompt_toolkit.keys import ALL_KEYS, Keys from prompt_toolkit.keys import ALL_KEYS, Keys
from typing import Any, Dict, List from typing import Any, Dict, List, Union
import commentjson as json import commentjson as json
import ipaddress import ipaddress
import re import re
@ -50,8 +53,11 @@ class Config:
# Basic key-value store w/ typing # Basic key-value store w/ typing
self.values: Dict[str, Dict[str, Any]] = { self.values: Dict[str, Dict[str, Any]] = {
"lhost": {"value": None, "type": ipaddress.ip_address}, "lhost": {
"prefix": {"value": "C-k", "type": KeyType}, "value": ipaddress.ip_address("127.0.0.1"),
"type": ipaddress.ip_address,
},
"prefix": {"value": KeyType("c-k"), "type": KeyType},
"privkey": {"value": "data/pwncat", "type": local_file_type}, "privkey": {"value": "data/pwncat", "type": local_file_type},
"backdoor_user": {"value": "pwncat", "type": str}, "backdoor_user": {"value": "pwncat", "type": str},
"backdoor_pass": {"value": "pwncat", "type": str}, "backdoor_pass": {"value": "pwncat", "type": str},
@ -60,7 +66,25 @@ class Config:
# Map ascii escape sequences or printable bytes to lists of commands to # Map ascii escape sequences or printable bytes to lists of commands to
# run. # run.
self.bindings: Dict[bytes, str] = {} self.bindings: Dict[KeyType, str] = {
KeyType("c-d"): "pass",
KeyType("s"): "sync",
KeyType("c"): "set state command",
}
def binding(self, name_or_value: Union[str, bytes]) -> str:
""" Get a key binding by it's key name or key value. """
if isinstance(name_or_value, bytes):
binding = [
b for key, b in self.bindings.items() if key.value == name_or_value
]
if not binding:
raise KeyError("no such key binding")
return binding[0]
key = KeyType(name_or_value)
return self.bindings[key]
def __getitem__(self, name: str) -> Any: def __getitem__(self, name: str) -> Any:
""" Get a configuration item """ """ Get a configuration item """

View File

@ -38,7 +38,7 @@ from pwncat.file import RemoteBinaryPipe
from pwncat.lexer import LocalCommandLexer, PwncatStyle from pwncat.lexer import LocalCommandLexer, PwncatStyle
from pwncat.gtfobins import GTFOBins, Capability, Stream from pwncat.gtfobins import GTFOBins, Capability, Stream
from pwncat.commands import CommandParser from pwncat.commands import CommandParser
from pwncat.config import Config from pwncat.config import Config, KeyType
from colorama import Fore from colorama import Fore
@ -216,6 +216,7 @@ 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"
self.has_prefix = False
self.command_parser = CommandParser(self) self.command_parser = CommandParser(self)
# Run the configuration script # Run the configuration script
@ -549,29 +550,41 @@ 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 """
if self.input == b"": if self.has_prefix:
# Enter commmand mode after C-d if data == self.config["prefix"].value:
if data == b"\x04": self.client.send(data)
# Clear line else:
self.client.send(b"\x15") try:
# Enter command mode binding = self.config.binding(data)
# Pass is a special case that can be used at the beginning of a
# command.
if binding.strip().startswith("pass"):
self.client.send(data)
binding = binding.lstrip("pass")
self.restore_local_term()
sys.stdout.write("\n")
# Evaluate the script
self.command_parser.eval(binding, "<binding>")
self.flush_output()
self.client.send(b"\n")
self.saved_term_state = util.enter_raw_mode()
except KeyError:
pass
self.has_prefix = False
elif data == self.config["prefix"].value:
self.has_prefix = True
elif data == KeyType("c-d").value:
# Don't allow exiting the remote prompt with C-d
# you should have a keybinding for "<prefix> C-d" to actually send
# C-d.
self.state = State.COMMAND self.state = State.COMMAND
# C-k is the prefix character
elif data == b"\x0b":
self.input = data
else: else:
self.client.send(data) self.client.send(data)
else:
# "C-k c" to enter command mode
if data == b"c":
self.client.send(b"\x15")
self.state = State.SINGLE
elif data == b":":
self.state = State.SINGLE
# "C-k C-k" or "C-k C-d" sends the second byte
elif data == b"\x0b" or data == b"\x04":
self.client.send(data)
self.input = b""
def recv(self) -> bytes: def recv(self) -> bytes:
""" Recieve data from the client """ """ Recieve data from the client """