mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-23 17:15:38 +01:00
Merge branch 'platforms' of github.com:calebstewart/pwncat into platforms
This commit is contained in:
commit
b6f2ae78a5
@ -3,23 +3,27 @@ import dataclasses
|
||||
import os
|
||||
import re
|
||||
|
||||
import rich.markup
|
||||
|
||||
import pwncat
|
||||
from pwncat.db import Fact
|
||||
from pwncat.platform.linux import Linux
|
||||
from pwncat.modules import Status
|
||||
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class CronEntry:
|
||||
class CronEntry(Fact):
|
||||
def __init__(self, source, path, uid, command, datetime):
|
||||
super().__init__(source=source, types=["software.cron.entry"])
|
||||
|
||||
path: str
|
||||
""" The path to the crontab where this was found """
|
||||
uid: int
|
||||
""" The user ID this entry will run as """
|
||||
command: str
|
||||
""" The command that will execute """
|
||||
datetime: str
|
||||
""" The entire date/time specifier from the crontab entry """
|
||||
self.path: str = path
|
||||
""" The path to the crontab where this was found """
|
||||
self.uid: int = uid
|
||||
""" The user ID this entry will run as """
|
||||
self.command: str = command
|
||||
""" The command that will execute """
|
||||
self.datetime: str = datetime
|
||||
""" The entire date/time specifier from the crontab entry """
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
|
@ -4,20 +4,31 @@ import os
|
||||
import re
|
||||
import shlex
|
||||
|
||||
import rich.markup
|
||||
|
||||
import pwncat
|
||||
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
|
||||
from pwncat.db import Fact
|
||||
from pwncat.platform.linux import Linux
|
||||
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
|
||||
from pwncat.subprocess import CalledProcessError
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ScreenVersion:
|
||||
"""
|
||||
TODO: This should realistically yield an ability (which can be used for
|
||||
privilege escalation)... but we can implement that later.
|
||||
"""
|
||||
|
||||
path: str
|
||||
perms: int
|
||||
vulnerable: bool = True
|
||||
|
||||
class ScreenVersion(Fact):
|
||||
def __init__(self, source, path, perms, vulnerable):
|
||||
super().__init__(source=source, types=["software.screen.version"])
|
||||
|
||||
self.path: str = path
|
||||
self.perms: int = perms
|
||||
self.vulnerable: bool = vulnerable
|
||||
|
||||
def __str__(self):
|
||||
return f"[cyan]{self.path}[/cyan] (perms: [blue]{oct(self.perms)[2:]}[/blue])"
|
||||
return f"[cyan]{rich.markup.escape(self.path)}[/cyan] (perms: [blue]{oct(self.perms)[2:]}[/blue]) [bold red]is vulnerable[/bold red]"
|
||||
|
||||
|
||||
class Module(EnumerateModule):
|
||||
@ -31,14 +42,14 @@ class Module(EnumerateModule):
|
||||
PLATFORM = [Linux]
|
||||
SCHEDULE = Schedule.ONCE
|
||||
|
||||
def enumerate(self):
|
||||
def enumerate(self, session):
|
||||
"""
|
||||
Enumerate kernel/OS version information
|
||||
Enumerate locations of vulnerable screen versions
|
||||
:return:
|
||||
"""
|
||||
|
||||
# Grab current path plus other interesting paths
|
||||
paths = set(pwncat.victim.getenv("PATH").split(":"))
|
||||
paths = set(session.platform.getenv("PATH").split(":"))
|
||||
paths = paths | {
|
||||
"/bin",
|
||||
"/sbin",
|
||||
@ -49,17 +60,56 @@ class Module(EnumerateModule):
|
||||
}
|
||||
|
||||
# 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)
|
||||
proc = session.platform.Popen(
|
||||
f"find {shlex.join(paths)} \\( -type f -or -type l \\) -executable \\( -name 'screen' -or -name 'screen-*' \\) -printf '%#m %p\\n' 2>/dev/null",
|
||||
shell=True,
|
||||
text=True,
|
||||
stdout=pwncat.subprocess.PIPE,
|
||||
)
|
||||
|
||||
# When the screen source code is on disk and marked as executable, this happens...
|
||||
if os.path.splitext(path)[1] in [".c", ".o", ".h"]:
|
||||
# First, collect all the paths to a `screen` binary we can find
|
||||
screen_paths = []
|
||||
for line in proc.stdout:
|
||||
line = line.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
|
||||
|
||||
if perms & 0o4000:
|
||||
# if this is executable
|
||||
screen_paths.append(path)
|
||||
|
||||
# Now, check each screen version to determine if it is vulnerable
|
||||
for screen_path in screen_paths:
|
||||
version_output = session.platform.Popen(
|
||||
f"{screen_path} --version",
|
||||
shell=True,
|
||||
text=True,
|
||||
stdout=pwncat.subprocess.PIPE,
|
||||
)
|
||||
for line in version_output.stdout:
|
||||
# This process checks if it is a vulnerable version of screen
|
||||
match = re.search(r"(\d+\.\d+\.\d+)", line)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
yield "software.screen.version", ScreenVersion(path, perms)
|
||||
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 ScreenVersion(self.name, path, perms, vulnerable=True)
|
||||
|
@ -3,9 +3,12 @@ import dataclasses
|
||||
import re
|
||||
from typing import Generator, Optional, List
|
||||
|
||||
import rich.markup
|
||||
|
||||
import pwncat
|
||||
from pwncat.platform.linux import Linux
|
||||
from pwncat import util
|
||||
from pwncat.db import Fact
|
||||
from pwncat.platform.linux import Linux
|
||||
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
|
||||
|
||||
per_user = True
|
||||
@ -19,30 +22,44 @@ sudo_pattern = re.compile(
|
||||
directives = ["Defaults", "User_Alias", "Runas_Alias", "Host_Alias", "Cmnd_Alias"]
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SudoSpec:
|
||||
class SudoSpec(Fact):
|
||||
def __init__(
|
||||
self,
|
||||
source,
|
||||
line: str,
|
||||
matched: bool = False,
|
||||
user: Optional[str] = None,
|
||||
group: Optional[str] = None,
|
||||
host: Optional[str] = None,
|
||||
runas_user: Optional[str] = None,
|
||||
runas_group: Optional[str] = None,
|
||||
options: List[str] = None,
|
||||
hash: str = None,
|
||||
commands: List[str] = None,
|
||||
):
|
||||
super().__init__(source=source, types=["software.sudo.rule"])
|
||||
|
||||
line: str
|
||||
""" The full, unaltered line from the sudoers file """
|
||||
matched: bool = False
|
||||
""" The regular expression match data. If this is None, all following fields
|
||||
are invalid and should not be used. """
|
||||
user: Optional[str] = None
|
||||
""" The user which this rule applies to. This is None if a group was specified """
|
||||
group: Optional[str] = None
|
||||
""" The group this rule applies to. This is None if a user was specified. """
|
||||
host: Optional[str] = None
|
||||
""" The host this rule applies to """
|
||||
runas_user: Optional[str] = None
|
||||
""" The user we are allowed to run as """
|
||||
runas_group: Optional[str] = None
|
||||
""" The GID we are allowed to run as (may be None)"""
|
||||
options: List[str] = None
|
||||
""" A list of options specified (e.g. NOPASSWD, SETENV, etc) """
|
||||
hash: str = None
|
||||
""" A hash type and value which sudo will obey """
|
||||
commands: List[str] = None
|
||||
""" The command specification """
|
||||
self.line: str
|
||||
""" The full, unaltered line from the sudoers file """
|
||||
self.matched: bool = False
|
||||
""" The regular expression match data. If this is None, all following fields
|
||||
are invalid and should not be used. """
|
||||
self.user: Optional[str] = None
|
||||
""" The user which this rule applies to. This is None if a group was specified """
|
||||
self.group: Optional[str] = None
|
||||
""" The group this rule applies to. This is None if a user was specified. """
|
||||
self.host: Optional[str] = None
|
||||
""" The host this rule applies to """
|
||||
self.runas_user: Optional[str] = None
|
||||
""" The user we are allowed to run as """
|
||||
self.runas_group: Optional[str] = None
|
||||
""" The GID we are allowed to run as (may be None)"""
|
||||
self.options: List[str] = None
|
||||
""" A list of options specified (e.g. NOPASSWD, SETENV, etc) """
|
||||
self.hash: str = None
|
||||
""" A hash type and value which sudo will obey """
|
||||
self.commands: List[str] = None
|
||||
""" The command specification """
|
||||
|
||||
def __str__(self):
|
||||
display = ""
|
||||
@ -51,28 +68,32 @@ class SudoSpec:
|
||||
return self.line
|
||||
|
||||
if self.user is not None:
|
||||
display += f"User [blue]{self.user}[/blue]: "
|
||||
display += f"User [blue]{rich.markup.escape(self.user)}[/blue]: "
|
||||
else:
|
||||
display += f"Group [cyan]{self.group}[/cyan]: "
|
||||
display += f"Group [cyan]{rich.markup.escape(self.group)}[/cyan]: "
|
||||
|
||||
display += f"[yellow]{'[/yellow], [yellow]'.join(self.commands)}[/yellow] as "
|
||||
display += f"[yellow]{'[/yellow], [yellow]'.join((rich.markup.escape(x) for c in self.commands))}[/yellow] as "
|
||||
|
||||
if self.runas_user == "root":
|
||||
display += f"[red]root[/red]"
|
||||
elif self.runas_user is not None:
|
||||
display += f"[blue]{self.runas_user}[/blue]"
|
||||
display += f"[blue]{rich.markup.escape(self.runas_user)}[/blue]"
|
||||
|
||||
if self.runas_group == "root":
|
||||
display += f":[red]root[/red]"
|
||||
elif self.runas_group is not None:
|
||||
display += f"[cyan]{self.runas_group}[/cyan]"
|
||||
display += f"[cyan]{rich.markup.escape(self.runas_group)}[/cyan]"
|
||||
|
||||
if self.host is not None:
|
||||
display += f" on [magenta]{self.host}[/magenta]"
|
||||
display += f" on [magenta]{rich.markup.escape(self.host)}[/magenta]"
|
||||
|
||||
if self.options:
|
||||
display += (
|
||||
" (" + ",".join(f"[green]{x}[/green]" for x in self.options) + ")"
|
||||
" ("
|
||||
+ ",".join(
|
||||
f"[green]{rich.markup.escape(x)}[/green]" for x in self.options
|
||||
)
|
||||
+ ")"
|
||||
)
|
||||
|
||||
return display
|
||||
@ -151,17 +172,17 @@ class Module(EnumerateModule):
|
||||
PLATFORM = [Linux]
|
||||
SCHEDULE = Schedule.PER_USER
|
||||
|
||||
def enumerate(self):
|
||||
def enumerate(self, session):
|
||||
|
||||
try:
|
||||
with pwncat.victim.open("/etc/sudoers", "r") as filp:
|
||||
with session.platform.open("/etc/sudoers", "r") as filp:
|
||||
for line in filp:
|
||||
line = line.strip()
|
||||
# Ignore comments and empty lines
|
||||
if line.startswith("#") or line == "":
|
||||
continue
|
||||
|
||||
yield "sudo", LineParser(line)
|
||||
yield LineParser(line)
|
||||
|
||||
# No need to parse `sudo -l`, since can read /etc/sudoers
|
||||
return
|
||||
@ -170,10 +191,13 @@ class Module(EnumerateModule):
|
||||
|
||||
# Check for our privileges
|
||||
try:
|
||||
result = pwncat.victim.sudo("-nl", send_password=False).decode("utf-8")
|
||||
if result.strip() == "sudo: a password is required":
|
||||
result = pwncat.victim.sudo("-l").decode("utf-8")
|
||||
|
||||
proc = session.platform.sudo(["sudo", "-nl"], as_is=True)
|
||||
result = proc.stdout.read()
|
||||
proc.wait() # ensure this closes properly
|
||||
|
||||
except PermissionError:
|
||||
# if this asks for a password and we don't have one, bail
|
||||
return
|
||||
|
||||
for line in result.split("\n"):
|
||||
@ -191,6 +215,6 @@ class Module(EnumerateModule):
|
||||
continue
|
||||
|
||||
# Build the beginning part of a normal spec
|
||||
line = f"{pwncat.victim.current_user.name} local=" + line.strip()
|
||||
line = f"{session.current_user()} local=" + line.strip()
|
||||
|
||||
yield "software.sudo.rule", LineParser(line)
|
||||
yield LineParser(line)
|
||||
|
@ -2,24 +2,30 @@
|
||||
import dataclasses
|
||||
import re
|
||||
|
||||
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
|
||||
import rich.markup
|
||||
|
||||
import pwncat
|
||||
from pwncat.db import Fact
|
||||
from pwncat.platform.linux import Linux
|
||||
from pwncat.subprocess import CalledProcessError
|
||||
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SudoVersion:
|
||||
class SudoVersion(Fact):
|
||||
"""
|
||||
Version of the installed sudo binary may be useful for exploitation
|
||||
|
||||
"""
|
||||
|
||||
version: str
|
||||
output: str
|
||||
vulnerable: bool
|
||||
def __init__(self, source, version, output, vulnerable):
|
||||
super().__init__(source=source, types=["software.sudo.version"])
|
||||
|
||||
self.version: str = version
|
||||
self.output: str = output
|
||||
self.vulnerable: bool = vulnerable
|
||||
|
||||
def __str__(self):
|
||||
result = f"[yellow]sudo[/yellow] version [cyan]{self.version}[/cyan]"
|
||||
result = f"[yellow]sudo[/yellow] version [cyan]{rich.markup.escape(self.version)}[/cyan]"
|
||||
if self.vulnerable:
|
||||
result += f" (may be [red]vulnerable[/red])"
|
||||
return result
|
||||
@ -44,18 +50,23 @@ class Module(EnumerateModule):
|
||||
PLATFORM = [Linux]
|
||||
SCHEDULE = Schedule.ONCE
|
||||
|
||||
def enumerate(self):
|
||||
def enumerate(self, session):
|
||||
"""
|
||||
Enumerate kernel/OS version information
|
||||
Enumerate the currently running version of sudo
|
||||
:return:
|
||||
"""
|
||||
|
||||
try:
|
||||
# Check the sudo version number
|
||||
result = pwncat.victim.env(["sudo", "--version"]).decode("utf-8").strip()
|
||||
except FileNotFoundError:
|
||||
result = session.platform.run(
|
||||
["sudo", "--version"], capture_output=True, check=True
|
||||
)
|
||||
except CalledProcessError:
|
||||
# Something went wrong with the sudo version
|
||||
return
|
||||
|
||||
version = result.stdout.decode("utf-8")
|
||||
|
||||
# Taken from here:
|
||||
# https://book.hacktricks.xyz/linux-unix/privilege-escalation#sudo-version
|
||||
known_vulnerable = [
|
||||
@ -76,7 +87,7 @@ class Module(EnumerateModule):
|
||||
|
||||
# Can we match this output to a specific sudo version?
|
||||
match = re.search(
|
||||
r"sudo version ([0-9]+\.[0-9]+\.[^\s]*)", result, re.IGNORECASE
|
||||
r"sudo version ([0-9]+\.[0-9]+\.[^\s]*)", version, re.IGNORECASE
|
||||
)
|
||||
if match is not None and match.group(1) is not None:
|
||||
vulnerable = False
|
||||
@ -87,9 +98,9 @@ class Module(EnumerateModule):
|
||||
vulnerable = True
|
||||
break
|
||||
|
||||
yield "sudo.version", SudoVersion(match.group(1), result, vulnerable)
|
||||
yield SudoVersion(self.name, match.group(1), version, vulnerable)
|
||||
return
|
||||
|
||||
# We couldn't parse the version out, but at least give the full version
|
||||
# output in the long form/report of enumeration.
|
||||
yield "software.sudo.version", SudoVersion("unknown", result, False)
|
||||
yield SudoVersion(self.name, "unknown", version, False)
|
||||
|
@ -35,7 +35,6 @@ class Module(EnumerateModule):
|
||||
yield group
|
||||
|
||||
except Exception as exc:
|
||||
raise ModuleFailed(f"something fucked {exc}")
|
||||
# Bad group line
|
||||
continue
|
||||
|
||||
|
@ -1217,7 +1217,7 @@ class Linux(Platform):
|
||||
popen_kwargs["env"] = None
|
||||
|
||||
if password is None:
|
||||
password = self.current_user.password
|
||||
password = self.session.current_user().password
|
||||
|
||||
# At this point, the command is a string
|
||||
if not as_is:
|
||||
|
Loading…
Reference in New Issue
Block a user