1
0
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:
Caleb Stewart 2021-05-08 00:50:04 -04:00
commit b6f2ae78a5
6 changed files with 174 additions and 86 deletions

View File

@ -3,22 +3,26 @@ 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
self.path: str = path
""" The path to the crontab where this was found """
uid: int
self.uid: int = uid
""" The user ID this entry will run as """
command: str
self.command: str = command
""" The command that will execute """
datetime: str
self.datetime: str = datetime
""" The entire date/time specifier from the crontab entry """
def __str__(self):

View File

@ -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,11 +60,17 @@ 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()
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,
)
# 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)
@ -62,4 +79,37 @@ class Module(EnumerateModule):
if os.path.splitext(path)[1] in [".c", ".o", ".h"]:
continue
yield "software.screen.version", ScreenVersion(path, perms)
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
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)

View File

@ -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,29 +22,43 @@ 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
self.line: str
""" The full, unaltered line from the sudoers file """
matched: bool = False
self.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
self.user: Optional[str] = None
""" The user which this rule applies to. This is None if a group was specified """
group: Optional[str] = None
self.group: Optional[str] = None
""" The group this rule applies to. This is None if a user was specified. """
host: Optional[str] = None
self.host: Optional[str] = None
""" The host this rule applies to """
runas_user: Optional[str] = None
self.runas_user: Optional[str] = None
""" The user we are allowed to run as """
runas_group: Optional[str] = None
self.runas_group: Optional[str] = None
""" The GID we are allowed to run as (may be None)"""
options: List[str] = None
self.options: List[str] = None
""" A list of options specified (e.g. NOPASSWD, SETENV, etc) """
hash: str = None
self.hash: str = None
""" A hash type and value which sudo will obey """
commands: List[str] = None
self.commands: List[str] = None
""" The command specification """
def __str__(self):
@ -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)

View File

@ -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)

View File

@ -35,7 +35,6 @@ class Module(EnumerateModule):
yield group
except Exception as exc:
raise ModuleFailed(f"something fucked {exc}")
# Bad group line
continue

View File

@ -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: