Introduction

A Linux component called PolicyKit (also known as Polkit) is a component used in many Unix-based operating systems. Its purpose is to handle communications between non-privileged and privileged processes. Polkit has been included in default configurations for many Linux distributions, including Ubuntu, CentOS and Debian since May 2009.

A function within this component called pkexec allows a user to execute commands with elevated privileges. This function will usually be called by a user that has access to sudo or the root user. pkexec needs root permissions in order to be executed due to the command being given elevated permissions.

A team of security researchers from Qualys discovered that Polkit’s pkexec function has an out-of-bounds read and write vulnerability. This vulnerability was assigned CVE-2021-4034 by the MITRE Corporation. It has a CVSSv3 score of 7.8 with the vector being CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H. This CVE is also known as Pwnkit.

Before going into technical detail about the vulnerability, some concepts about the main function in C need to be understood.

How the “main()” Function in C works

The main() function in C is the first function to be called when a program is executed. It can accept three options: argv, argc, and envp. These examples will be referring to the command ‘sudo apt-get update’ to explain these options.

  • argv is an array of strings, which are the arguments on the command line. In this case, argv {“sudo”,”apt-get”,”update”}.
  • argc is an integer that contains the length of the array argv, so argc = 3 (because there are 3 arguments in the command). It should be noted that the last value of argv is referred to as argv[argc-1]. This is because indexing starts at 0 so the third value has an index of 2. Therefore, argv[argc] (which is argv[3] in this case), will be equal to NULL. This NULL pointer ensures that the list is terminated when it reaches the end.
  • envp is an array of pointers to environment variables. For example, envp = {“file”,”PATH=blah”,”SHELL=/usr/bin/bash”} etc…

Another concept worth knowing is how these arrays are sequenced on the call stack. argv and envp are placed side by side within the call stack, separated by the NULL pointer argv[argc]:

argv[0] | argv[1] | argv[2] | argv[argc] | envp[0] | envp[1] | … | envp[envc] |

Here is what the stack would look like using the example command mentioned earlier:

“sudo” | “apt-get” | “update” | NULL | “SHELL=blah” | “PATH=blah” | … | NULL |

Technical Details of the Vulnerability

For an understanding of the details of the vulnerability, some of pkexec‘s main() function code is shown below:

pkexec's main() function code

On line 534, the code shows that there is a for loop that iterates through the arguments in argv. When the for loop is terminated, the value of is equal to argc-1. Therefore, argv[n] argv[argc-1] = the last argument supplied to the function. This argument is the program that needs to be executed.

Line 612 shows that the value of argv[n] gets copied to the path variable. If the value of argv[n] does not begin with a forward slash, the variable path gets put as an argument to the g_find_program_in_path () function. This function will search for the absolute path of the value of path and will then write the absolute path back to the argv[n].

With this functionality in mind, the vulnerability itself occurs when the argv array has a value of NULL. In this case, the for loop on line 534 will immediately terminate because n is set to 1 and argc would be 0 (due to argv having no length). This means that can never be less than 0, so the loop will be terminated. Due to this termination, the value of stays as 1.

The fact that n is 1 is a problem because the path variable will be assigned as argv[1]argv is NULL and therefore, argv[1] goes outside of the bounds of the array. This causes argv[1] to point to the next frame in the stack, which happens to be envp[0] due to the order of variables in the stack, as mentioned earlier. The value of envp[0] will be saved in the path variable and then passed to the g_find_program_in_path () function.

If the file is found, g_find_program_in_path () will write the absolute path of that file, back to argv[n]. This is an out-of-bounds read and write vulnerability which can be used by an exploit because it allows attackers to write environment variables that pkexec‘s main() function can read.

Using GCONV_PATH to Exploit Pwnkit

Whilst there are many ways this vulnerability can be exploited, focus will be placed on the use of an old vulnerability that uses the GONV_PATH environment variable. GCONV_PATH points to the path of the conversion directory that will be loaded if the CHARSET environment variable does not match the expected encoding of what called it. Due to the security issues of the GCONV_PATH environment variable, it is omitted from the environment variables before pkexec‘s main function is executed.

Since the Pwnkit vulnerability allows an attacker to overwrite the environment variables passed to pkexec‘s main function, it is possible to re-introduce GCONV_PATH to the envp array. This will allow it to be used for exploitation.

Pkexec contains a function called g_printerr (). This function prints error messages in UTF-8. Here is the snippet of the code:

snippet of the g_printerr code

Looking at the code, the g_printerr () can be triggered if line 405 does not get a shell listed in the /etc/shells directory. Using this knowledge, passing a SHELL environment variable with an invalid shell path will trigger the g_printerr () function. This becomes useful because if the CHARSET environment variable is not UTF-8 (which is g_printerr‘s default encoding), it will call the function iconv_open ().

iconv_open () will help convert the encoding to whatever the CHARSET environment variable is. It does this by searching in the GCONV_PATH to get the conversion directory path, then loads and executes the conversion file.

Exploitation

The first step to exploiting this vulnerability is by creating a directory with the name ‘GCONV_PATH=.’. This is the directory that will hold a blank file that has the name of the directory that holds the exploit.

The second step to exploiting this vulnerability is to create the blank file whose name will also be used as the CHARSET name. In this case, secquest will be used. This can be done using the command below:

touch 'GCONV_PATH=./secquest'

This is the value that will be written to the envp array using the out-of-bounds write vulnerability. The file must also be made executable.

The third step is to create a directory called secquest. This must match the name of the file created in the ‘GCONV_PATH=.’ directory as this is where the iconv_open () function will search to find the conversion file. Inside the secquest directory, create a file called secquest.c which will contain code that spawns a root shell. Here is an example of a simple root shell in C:

A simple root sheel in C

When iconv_open () finds the conversion file, gconv_init () is the first function to execute. This will spawn the root shell. The system call includes an export PATH command because the PATH environment variable given to pkexec only contains ‘GCONV_PATH=.’ so the original PATH needs to be re-introduced in order for commands to run in the root shell. This can be done by simply running echo $PATH, copying the result and pasting it into the export command above. The gconv () function needs to be included, even though it is never called, otherwise the execution will fail as iconv_open () will error if it is missing.

The next step is to compile secquest.c into secquest.so. the .so is a shared object file that is executed when it is loaded by iconv_open (). This can be done using the following command:

gcc secquest/secquest.c -o secquest/secquest.so -shared -fPIC

The next step is to create the executable file that will call pkexec with the correct arguments. This can be done by using the execve () command in C. An array needs to be declared and needs to contain: the name of the exploit directory (in this case, secquest), the path variable containing the GCONV_PATH environment variable, CHARSET variable that has the same name as the exploit directory, any random SHELL environment variable, and a NULL pointer to indicate the end of the array. Then, execve() needs to be called with 3 arguments: the full path to the pkexec executable, an array with NULL, and the environment variable array. An example is shown below:

example of execve() being called with 3 arguments

This exploit then needs to be compiled with gcc.

The final step is to create a file called gconv-modules file in the secquest directory. This will contain the conversion information for iconv_open (). The conversion file should look like this:

module UTF-8// SECQUEST// secquest 2

Ensure that the names in this string match the name of the CHARSET and exploit directory. The finished file structure should look like this:

Example of the correct file structure for the exploit

All that is left is to execute exploit and a root shell will be spawned.

Impact and Mitigation

The impact of this vulnerability is high. This is because policykit was included in the default packages for many operating systems including Ubuntu, Debian, Fedora, CentOS and possibly others. It could affect systems all the way back to around 2009 when policykit was first introduced as a default package for Unix systems. This vulnerability also allows a low-privileged user to gain access to the root shell on the host system, fully compromising the host machine and possibly even giving the attacker the ability to use this system to further compromise machines in the same network.

The recommendation to mitigate this vulnerability is to ensure that any affected systems update the policykit packages to the latest version. If this is not possible, the temporary solution is to remove the SUID-bit from the pkexec command until updates can be installed.

Resources: