mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-30 12:24:14 +01:00
Added bind and alias commands to fully control configuration through command scripting.
This commit is contained in:
parent
ded22f18e4
commit
82ea5799d8
@ -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
|
||||||
|
|
||||||
|
@ -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):
|
||||||
@ -223,8 +227,11 @@ class CommandParser:
|
|||||||
if command.PROG == argv[0]:
|
if command.PROG == argv[0]:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
util.error(f"{argv[0]}: unknown command")
|
if argv[0] in self.aliases:
|
||||||
return
|
command = self.aliases[argv[0]]
|
||||||
|
else:
|
||||||
|
util.error(f"{argv[0]}: unknown command")
|
||||||
|
return
|
||||||
|
|
||||||
if not self.loading_complete and not command.LOCAL:
|
if not self.loading_complete and not command.LOCAL:
|
||||||
util.error(
|
util.error(
|
||||||
|
40
pwncat/commands/alias.py
Normal file
40
pwncat/commands/alias.py
Normal 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
39
pwncat/commands/bind.py
Normal 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
|
@ -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:
|
||||||
|
@ -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 """
|
||||||
|
@ -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
|
|
||||||
self.client.send(b"\x15")
|
|
||||||
# Enter command mode
|
|
||||||
self.state = State.COMMAND
|
|
||||||
# C-k is the prefix character
|
|
||||||
elif data == b"\x0b":
|
|
||||||
self.input = data
|
|
||||||
else:
|
else:
|
||||||
self.client.send(data)
|
try:
|
||||||
|
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
|
||||||
else:
|
else:
|
||||||
# "C-k c" to enter command mode
|
self.client.send(data)
|
||||||
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 """
|
||||||
|
Loading…
Reference in New Issue
Block a user