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,22 +3,26 @@ import dataclasses
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
import rich.markup
|
||||||
|
|
||||||
import pwncat
|
import pwncat
|
||||||
|
from pwncat.db import Fact
|
||||||
from pwncat.platform.linux import Linux
|
from pwncat.platform.linux import Linux
|
||||||
from pwncat.modules import Status
|
from pwncat.modules import Status
|
||||||
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
|
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
class CronEntry(Fact):
|
||||||
class CronEntry:
|
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 """
|
""" The path to the crontab where this was found """
|
||||||
uid: int
|
self.uid: int = uid
|
||||||
""" The user ID this entry will run as """
|
""" The user ID this entry will run as """
|
||||||
command: str
|
self.command: str = command
|
||||||
""" The command that will execute """
|
""" The command that will execute """
|
||||||
datetime: str
|
self.datetime: str = datetime
|
||||||
""" The entire date/time specifier from the crontab entry """
|
""" The entire date/time specifier from the crontab entry """
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -4,20 +4,31 @@ import os
|
|||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
|
import rich.markup
|
||||||
|
|
||||||
import pwncat
|
import pwncat
|
||||||
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
|
from pwncat.db import Fact
|
||||||
from pwncat.platform.linux import Linux
|
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
|
class ScreenVersion(Fact):
|
||||||
vulnerable: bool = True
|
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):
|
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):
|
class Module(EnumerateModule):
|
||||||
@ -31,14 +42,14 @@ class Module(EnumerateModule):
|
|||||||
PLATFORM = [Linux]
|
PLATFORM = [Linux]
|
||||||
SCHEDULE = Schedule.ONCE
|
SCHEDULE = Schedule.ONCE
|
||||||
|
|
||||||
def enumerate(self):
|
def enumerate(self, session):
|
||||||
"""
|
"""
|
||||||
Enumerate kernel/OS version information
|
Enumerate locations of vulnerable screen versions
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Grab current path plus other interesting paths
|
# Grab current path plus other interesting paths
|
||||||
paths = set(pwncat.victim.getenv("PATH").split(":"))
|
paths = set(session.platform.getenv("PATH").split(":"))
|
||||||
paths = paths | {
|
paths = paths | {
|
||||||
"/bin",
|
"/bin",
|
||||||
"/sbin",
|
"/sbin",
|
||||||
@ -49,11 +60,17 @@ class Module(EnumerateModule):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Look for matching binaries
|
# Look for matching binaries
|
||||||
with pwncat.victim.subprocess(
|
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"
|
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:
|
shell=True,
|
||||||
for line in pipe:
|
text=True,
|
||||||
line = line.decode("utf-8").strip()
|
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(" ")
|
perms, *path = line.split(" ")
|
||||||
path = " ".join(path)
|
path = " ".join(path)
|
||||||
perms = int(perms, 8)
|
perms = int(perms, 8)
|
||||||
@ -62,4 +79,37 @@ class Module(EnumerateModule):
|
|||||||
if os.path.splitext(path)[1] in [".c", ".o", ".h"]:
|
if os.path.splitext(path)[1] in [".c", ".o", ".h"]:
|
||||||
continue
|
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)
|
||||||
|
@ -3,9 +3,12 @@ import dataclasses
|
|||||||
import re
|
import re
|
||||||
from typing import Generator, Optional, List
|
from typing import Generator, Optional, List
|
||||||
|
|
||||||
|
import rich.markup
|
||||||
|
|
||||||
import pwncat
|
import pwncat
|
||||||
from pwncat.platform.linux import Linux
|
|
||||||
from pwncat import util
|
from pwncat import util
|
||||||
|
from pwncat.db import Fact
|
||||||
|
from pwncat.platform.linux import Linux
|
||||||
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
|
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
|
||||||
|
|
||||||
per_user = True
|
per_user = True
|
||||||
@ -19,29 +22,43 @@ sudo_pattern = re.compile(
|
|||||||
directives = ["Defaults", "User_Alias", "Runas_Alias", "Host_Alias", "Cmnd_Alias"]
|
directives = ["Defaults", "User_Alias", "Runas_Alias", "Host_Alias", "Cmnd_Alias"]
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
class SudoSpec(Fact):
|
||||||
class SudoSpec:
|
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 """
|
""" 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
|
""" The regular expression match data. If this is None, all following fields
|
||||||
are invalid and should not be used. """
|
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 """
|
""" 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. """
|
""" 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 """
|
""" 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 """
|
""" 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)"""
|
""" 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) """
|
""" 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 """
|
""" A hash type and value which sudo will obey """
|
||||||
commands: List[str] = None
|
self.commands: List[str] = None
|
||||||
""" The command specification """
|
""" The command specification """
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -51,28 +68,32 @@ class SudoSpec:
|
|||||||
return self.line
|
return self.line
|
||||||
|
|
||||||
if self.user is not None:
|
if self.user is not None:
|
||||||
display += f"User [blue]{self.user}[/blue]: "
|
display += f"User [blue]{rich.markup.escape(self.user)}[/blue]: "
|
||||||
else:
|
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":
|
if self.runas_user == "root":
|
||||||
display += f"[red]root[/red]"
|
display += f"[red]root[/red]"
|
||||||
elif self.runas_user is not None:
|
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":
|
if self.runas_group == "root":
|
||||||
display += f":[red]root[/red]"
|
display += f":[red]root[/red]"
|
||||||
elif self.runas_group is not None:
|
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:
|
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:
|
if self.options:
|
||||||
display += (
|
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
|
return display
|
||||||
@ -151,17 +172,17 @@ class Module(EnumerateModule):
|
|||||||
PLATFORM = [Linux]
|
PLATFORM = [Linux]
|
||||||
SCHEDULE = Schedule.PER_USER
|
SCHEDULE = Schedule.PER_USER
|
||||||
|
|
||||||
def enumerate(self):
|
def enumerate(self, session):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with pwncat.victim.open("/etc/sudoers", "r") as filp:
|
with session.platform.open("/etc/sudoers", "r") as filp:
|
||||||
for line in filp:
|
for line in filp:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
# Ignore comments and empty lines
|
# Ignore comments and empty lines
|
||||||
if line.startswith("#") or line == "":
|
if line.startswith("#") or line == "":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
yield "sudo", LineParser(line)
|
yield LineParser(line)
|
||||||
|
|
||||||
# No need to parse `sudo -l`, since can read /etc/sudoers
|
# No need to parse `sudo -l`, since can read /etc/sudoers
|
||||||
return
|
return
|
||||||
@ -170,10 +191,13 @@ class Module(EnumerateModule):
|
|||||||
|
|
||||||
# Check for our privileges
|
# Check for our privileges
|
||||||
try:
|
try:
|
||||||
result = pwncat.victim.sudo("-nl", send_password=False).decode("utf-8")
|
|
||||||
if result.strip() == "sudo: a password is required":
|
proc = session.platform.sudo(["sudo", "-nl"], as_is=True)
|
||||||
result = pwncat.victim.sudo("-l").decode("utf-8")
|
result = proc.stdout.read()
|
||||||
|
proc.wait() # ensure this closes properly
|
||||||
|
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
|
# if this asks for a password and we don't have one, bail
|
||||||
return
|
return
|
||||||
|
|
||||||
for line in result.split("\n"):
|
for line in result.split("\n"):
|
||||||
@ -191,6 +215,6 @@ class Module(EnumerateModule):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Build the beginning part of a normal spec
|
# 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 dataclasses
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
|
import rich.markup
|
||||||
|
|
||||||
import pwncat
|
import pwncat
|
||||||
|
from pwncat.db import Fact
|
||||||
from pwncat.platform.linux import Linux
|
from pwncat.platform.linux import Linux
|
||||||
|
from pwncat.subprocess import CalledProcessError
|
||||||
|
from pwncat.modules.agnostic.enumerate import EnumerateModule, Schedule
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
class SudoVersion(Fact):
|
||||||
class SudoVersion:
|
|
||||||
"""
|
"""
|
||||||
Version of the installed sudo binary may be useful for exploitation
|
Version of the installed sudo binary may be useful for exploitation
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
version: str
|
def __init__(self, source, version, output, vulnerable):
|
||||||
output: str
|
super().__init__(source=source, types=["software.sudo.version"])
|
||||||
vulnerable: bool
|
|
||||||
|
self.version: str = version
|
||||||
|
self.output: str = output
|
||||||
|
self.vulnerable: bool = vulnerable
|
||||||
|
|
||||||
def __str__(self):
|
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:
|
if self.vulnerable:
|
||||||
result += f" (may be [red]vulnerable[/red])"
|
result += f" (may be [red]vulnerable[/red])"
|
||||||
return result
|
return result
|
||||||
@ -44,18 +50,23 @@ class Module(EnumerateModule):
|
|||||||
PLATFORM = [Linux]
|
PLATFORM = [Linux]
|
||||||
SCHEDULE = Schedule.ONCE
|
SCHEDULE = Schedule.ONCE
|
||||||
|
|
||||||
def enumerate(self):
|
def enumerate(self, session):
|
||||||
"""
|
"""
|
||||||
Enumerate kernel/OS version information
|
Enumerate the currently running version of sudo
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check the sudo version number
|
# Check the sudo version number
|
||||||
result = pwncat.victim.env(["sudo", "--version"]).decode("utf-8").strip()
|
result = session.platform.run(
|
||||||
except FileNotFoundError:
|
["sudo", "--version"], capture_output=True, check=True
|
||||||
|
)
|
||||||
|
except CalledProcessError:
|
||||||
|
# Something went wrong with the sudo version
|
||||||
return
|
return
|
||||||
|
|
||||||
|
version = result.stdout.decode("utf-8")
|
||||||
|
|
||||||
# Taken from here:
|
# Taken from here:
|
||||||
# https://book.hacktricks.xyz/linux-unix/privilege-escalation#sudo-version
|
# https://book.hacktricks.xyz/linux-unix/privilege-escalation#sudo-version
|
||||||
known_vulnerable = [
|
known_vulnerable = [
|
||||||
@ -76,7 +87,7 @@ class Module(EnumerateModule):
|
|||||||
|
|
||||||
# Can we match this output to a specific sudo version?
|
# Can we match this output to a specific sudo version?
|
||||||
match = re.search(
|
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:
|
if match is not None and match.group(1) is not None:
|
||||||
vulnerable = False
|
vulnerable = False
|
||||||
@ -87,9 +98,9 @@ class Module(EnumerateModule):
|
|||||||
vulnerable = True
|
vulnerable = True
|
||||||
break
|
break
|
||||||
|
|
||||||
yield "sudo.version", SudoVersion(match.group(1), result, vulnerable)
|
yield SudoVersion(self.name, match.group(1), version, vulnerable)
|
||||||
return
|
return
|
||||||
|
|
||||||
# We couldn't parse the version out, but at least give the full version
|
# We couldn't parse the version out, but at least give the full version
|
||||||
# output in the long form/report of enumeration.
|
# 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
|
yield group
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise ModuleFailed(f"something fucked {exc}")
|
|
||||||
# Bad group line
|
# Bad group line
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -1217,7 +1217,7 @@ class Linux(Platform):
|
|||||||
popen_kwargs["env"] = None
|
popen_kwargs["env"] = None
|
||||||
|
|
||||||
if password is None:
|
if password is None:
|
||||||
password = self.current_user.password
|
password = self.session.current_user().password
|
||||||
|
|
||||||
# At this point, the command is a string
|
# At this point, the command is a string
|
||||||
if not as_is:
|
if not as_is:
|
||||||
|
Loading…
Reference in New Issue
Block a user