mirror of
https://github.com/calebstewart/pwncat.git
synced 2024-12-02 21:34:15 +01:00
252 lines
7.2 KiB
Python
252 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
|
import dataclasses
|
|
import re
|
|
from typing import Generator, Optional, List
|
|
|
|
from colorama import Fore
|
|
|
|
import pwncat
|
|
from pwncat.enumerate import FactData
|
|
|
|
name = "sudo"
|
|
provides = "sudo"
|
|
per_user = True
|
|
sudo_pattern = re.compile(
|
|
r"""(%?[a-zA-Z][a-zA-Z0-9_]*)\s+([a-zA-Z_][-a-zA-Z0-9_.]*)\s*="""
|
|
r"""(\([a-zA-Z_][-a-zA-Z0-9_]*(:[a-zA-Z_][a-zA-Z0-9_]*)?\)|[a-zA-Z_]"""
|
|
r"""[a-zA-Z0-9_]*)?\s+((NOPASSWD:\s+)|(SETENV:\s+)|(sha[0-9]{1,3}:"""
|
|
r"""[-a-zA-Z0-9_]+\s+))*(.*)"""
|
|
)
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class SudoSpec(FactData):
|
|
|
|
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 """
|
|
command: str = None
|
|
""" The command specification """
|
|
|
|
def __str__(self):
|
|
display = ""
|
|
|
|
if not self.matched:
|
|
return self.line
|
|
|
|
if self.user is not None:
|
|
display += f"User {Fore.BLUE}{self.user}{Fore.RESET}: "
|
|
else:
|
|
display += f"Group {Fore.CYAN}{self.group}{Fore.RESET}: "
|
|
|
|
display += f"{Fore.YELLOW}{self.command}{Fore.RESET} as "
|
|
|
|
if self.runas_user == "root":
|
|
display += f"{Fore.RED}root{Fore.RESET}"
|
|
elif self.runas_user is not None:
|
|
display += f"{Fore.BLUE}{self.runas_user}{Fore.RESET}"
|
|
|
|
if self.runas_group == "root":
|
|
display += f":{Fore.RED}root{Fore.RESET}"
|
|
elif self.runas_group is not None:
|
|
display += f"{Fore.CYAN}{self.runas_group}{Fore.RESET}"
|
|
|
|
if self.host is not None:
|
|
display += f" on {Fore.MAGENTA}{self.host}{Fore.RESET}"
|
|
|
|
if self.options:
|
|
display += (
|
|
" ("
|
|
+ ",".join(f"{Fore.GREEN}{x}{Fore.RESET}" for x in self.options)
|
|
+ ")"
|
|
)
|
|
|
|
return display
|
|
|
|
@property
|
|
def description(self):
|
|
if self.matched:
|
|
return self.line
|
|
return None
|
|
|
|
|
|
def enumerate() -> Generator[FactData, None, None]:
|
|
"""
|
|
Enumerate sudo privileges for the current user. If able, this will
|
|
parse `/etc/sudoers`. Otherwise, it will attempt to use `sudo -l`
|
|
to enumerate the current user's privileges. In the latter case,
|
|
it will utilize a defined password if available.
|
|
|
|
:return:
|
|
"""
|
|
|
|
directives = ["Defaults", "User_Alias", "Runas_Alias", "Host_Alias", "Cmnd_Alias"]
|
|
|
|
try:
|
|
with pwncat.victim.open("/etc/sudoers", "r") as filp:
|
|
for line in filp:
|
|
line = line.strip()
|
|
# Ignore comments and empty lines
|
|
if line.startswith("#") or line == "":
|
|
continue
|
|
|
|
match = sudo_pattern.search(line)
|
|
if match is None:
|
|
yield SudoSpec(line, matched=False, options=[])
|
|
continue
|
|
|
|
user = match.group(1)
|
|
|
|
if user in directives:
|
|
yield SudoSpec(line, matched=False, options=[])
|
|
continue
|
|
|
|
if user.startswith("%"):
|
|
group = user.lstrip("%")
|
|
user = None
|
|
else:
|
|
group = None
|
|
|
|
host = match.group(2)
|
|
|
|
if match.group(3) is not None:
|
|
runas_user = match.group(3).lstrip("(").rstrip(")")
|
|
if match.group(4) is not None:
|
|
runas_group = match.group(4)
|
|
runas_user = runas_user.split(":")[0]
|
|
else:
|
|
runas_group = None
|
|
if runas_user == "":
|
|
runas_user = "root"
|
|
else:
|
|
runas_user = "root"
|
|
runas_group = None
|
|
|
|
options = []
|
|
hash = None
|
|
|
|
for g in map(match.group, [6, 7, 8]):
|
|
if g is None:
|
|
continue
|
|
|
|
options.append(g.strip().rstrip(":"))
|
|
if g.startswith("sha"):
|
|
hash = g
|
|
|
|
command = match.group(9)
|
|
|
|
yield SudoSpec(
|
|
line,
|
|
True,
|
|
user,
|
|
group,
|
|
host,
|
|
runas_user,
|
|
runas_group,
|
|
options,
|
|
hash,
|
|
command,
|
|
)
|
|
|
|
# No need to parse `sudo -l`, since can read /etc/sudoers
|
|
return
|
|
except (FileNotFoundError, PermissionError):
|
|
pass
|
|
|
|
# Check for our privileges
|
|
try:
|
|
result = pwncat.victim.sudo("-l").decode("utf-8")
|
|
except PermissionError:
|
|
return
|
|
|
|
for line in result.split("\n"):
|
|
line = line.rstrip()
|
|
|
|
# Skipe header lines
|
|
if not line.startswith(" ") and not line.startswith("\t"):
|
|
continue
|
|
|
|
# Strip beginning whitespace
|
|
line = line.strip()
|
|
|
|
# Skip things that aren't user specifications
|
|
if not line.startswith("("):
|
|
continue
|
|
|
|
# Build the beginning part of a normal spec
|
|
line = f"{pwncat.victim.current_user.name} local=" + line.strip()
|
|
|
|
match = sudo_pattern.search(line)
|
|
if match is None:
|
|
yield SudoSpec(line, matched=False, options=[])
|
|
continue
|
|
|
|
user = match.group(1)
|
|
|
|
if user in directives:
|
|
yield SudoSpec(line, matched=False, options=[])
|
|
continue
|
|
|
|
if user.startswith("%"):
|
|
group = user.lstrip("%")
|
|
user = None
|
|
else:
|
|
group = None
|
|
|
|
host = match.group(2)
|
|
|
|
if match.group(3) is not None:
|
|
runas_user = match.group(3).lstrip("(").rstrip(")")
|
|
if match.group(4) is not None:
|
|
runas_group = match.group(4)
|
|
runas_user = runas_user.split(":")[0]
|
|
else:
|
|
runas_group = None
|
|
if runas_user == "":
|
|
runas_user = "root"
|
|
else:
|
|
runas_user = "root"
|
|
runas_group = None
|
|
|
|
options = []
|
|
hash = None
|
|
|
|
for g in map(match.group, [6, 7, 8]):
|
|
if g is None:
|
|
continue
|
|
|
|
options.append(g.strip().rstrip(":"))
|
|
if g.startswith("sha"):
|
|
hash = g
|
|
|
|
command = match.group(9)
|
|
|
|
yield SudoSpec(
|
|
line,
|
|
True,
|
|
user,
|
|
group,
|
|
host,
|
|
runas_user,
|
|
runas_group,
|
|
options,
|
|
hash,
|
|
command,
|
|
)
|