mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-11-27 10:54:14 +01:00
Go started on software
This commit is contained in:
parent
eb068ac493
commit
fb7cff5d80
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user