Dissecting pkexec (CVE-2021-4034) vulnerability
Polkit is a crucial component for controlling system-wide privileges in Linux (and other Unix-like systems such as BSD), similar to sudo. Initially it was released in May 2009 under the name PolicyKit and later it was renamed, it contained a critical local privilege escalation vulnerability for 12 years, until it was disclosed on January 25, 2022.
This is fascinating because:
- The critical code was in the main function not buried inside different files and folders.
- How easily code gets overlooked once it has been written.
In this blog post I am going to follow the path of the vulnerability, its exploit and dissect it in order to understand it further, mainly to serve me as a reminder for the my future-self.
Theory
The Qualys finding states that if the number of command-line arguments argc is 0 and argument list (argv) that is passed to execve
is NULL then iterator integer is set to a constant of 1 (see line 534 below) thus enabling us to read and write in out-of-bounds argv[1]. Below is the snippet from Qualys:
435 main (int argc, char *argv[])
436 {
...
534 for (n = 1; n < (guint) argc; n++)
535 {
...
568 }
...
610 path = g_strdup (argv[n]);
...
629 if (path[0] != '/')
630 {
...
632 s = g_find_program_in_path (path);
...
639 argv[n] = path = s;
640 }
Normally (like sudo) pkexec
completely clears its environment, which is quite hard to find which variable will stick around, in order to understand it further is to know GLib (the GNOME library, not the GNU C Library) a little bit more. pkexec
uses g_printerr()
to print error messages which is a part of GLib and g_printerr()
being a GNOME library it normally prints messages in UTF-8 format, but can be configured to print out in other formats as well using iconv_open()
GLib function.
In order to convert error message from one character set (CHARSET) to another, iconv_open()
executes small shared libraries, these are read from a global configuration file normally stored in /usr/lib/gconv/gconv-modules
but at the same time GCONV_PATH
can force iconv_open()
to read another configuration file (which can be stored anywhere) just the name should contain gconv-modules
and normally the GCONV_PATH
is not cleaned because it is reserved to load shared libraries which are important to a system, therefore the CVE-2021-4034 allows us to re-introduce GCONV_PATH
into the pkexec
environment (avoiding the global path of /usr/lib/gconv/gconv-modules
) and execute our own malicious payload library as root. Let's see it in action.
Basically the idea is to trick pkexec
to execute a malicious library as root when it encounters an error and calls the local gconv-modules
instead of the global one.
Technical Part
First we are going to write the function to compile the malicious library, we are naming it compile_library()
.
This is the library that will be loaded once pkexec
encounters an error and calls g_printerr()
GLib function.
This function writes itself a malicious library such that when XAUTHORITY
reports an error it sends it through g_printerr()
GLib function calls the gconv-modules
and the library is called as root, it calls setuid(0), seteuid(0), setgid(0)
and setegid(0)
, for more on Linux permissions refer back to my previous blog post where I use similar exploit in an old apache vulnerability.
Next we will be writing our own main function, this creates all points mentioned in the theoretical part above, we will be creating both argv
and envp
by setting argv
to NULL
and envp
to contain environment variables so we can load our malicious library, we will then create a directory called GCONV_PATH=.
, we will be creating a file on the path called test-milotio
and a directory called test-milotio
containing the gconv-modules
with the malicious library containing setuid(0), seteuid(0), setgid(0)
and setegid(0)
function calls in the following gconv-modules
format:
In gconv-modules
lines starting with module
register the available conversion module, the first word is the source CHARSET
in our case it is UTF-8, the second is the destination CHARSET
which in this case we use 32-bit encoding, we could have used INTERNAL
which represents UCS-4
which actually is UTF-32
so we are setting UTF-32
directly, and last but not least we specify the filename which normally expects a .so
extension, that's what we have compiled. In gconf-modules
format is the fifth element on the format which is used for cost, if it is not specified cost of 1 is assumed, which we are good with it.
Then we execute all of this using pkexec
in order to load the module and set the UID and GID to 0, the main funcion is well commented below:
Below is a video image compiling and executing it in a controlled virtual machine environment running fully updated Kali Linux on VMWare Workstation 16 (do not run it on your host machine or your servers):
Conclusion
Increasingly software is more prevalent in our daily lives and with it software complexity increases and with increased software complexity also there are more ways to break the software and use it for malicious intent, this vulnerability in itself contains an old trick by following simple steps:
- Use a privilege escalation component such as
pkexec
. - Trick the
pkexec
environment to think it is loading a graceful library as a privileged user (root). - In this case it loaded the library when it encountered an error through
g_printerr()
GLib function thus forcing it to load our own localgconv-modules
via theGCONV_PATH=.
created locally. - Inject the malicious library to execute as a privileged user (root).
- And you have root access on the system.
It is highly important to revisit the old code if it is still in use, we are humans after all and we all make mistakes.
Running the Exploit
Source code is available on Dissecting pkexec CVE-2021-4034 repository on my Github account. In order to try it yourself on a controlled virtual environment, just copy and paste the following:
Note that the system requires to have GLib installed and the vulnerable version of pkexec
which at the time of writing the patch is available.