
Detailed write-up and proof-of-concept exploit for CVE-2021-4034 (PolKit pkexec local privilege escalation), including a Docker lab environment for hands-on analysis and debugging.
[toc]
Vulnerability ID: CVE-2021-4034
Vulnerability Score:
Affected Product: linux PolKit (pkexec)
Affected Versions: Versions from 2009 to present (current 0.105) Reference http://its.dlut.edu.cn/info/1054/78309.htm
Exploit Conditions: linux local; pkexec is a suid file with execute permission
Source Code: apt source policykit-1
or https://launchpad.net/ubuntu/bionic/+package/policykit-1
Docker Environment: chenaotian/cve-2021-4034
I built a docker that provides:
pkexec compiled by myselfEverything is in /root/ directory:

su test to switch to test user and run it.Start the docker:
docker run -d -ti --rm -h cvedebug --name cvedebug --cap-add=SYS_PTRACE chenaotian/cve-2021-4034:latest /bin/bash
Test the exp:
cd ~/exp/CVE-2021-4034/
./run.sh
su test
./exp
whoami
The affected product is the pkexec command under polkit. pkexec is similar to sudo in that it allows us to execute commands as another user (usually root). Use dpkg to check which package pkexec belongs to:
dpkg -S /usr/bin/pkexec

Then get the source package (also available in my docker), and compile a debuggable version from the source for debugging.
The trigger principle is very simple
/polkit-0.105/src/programs/pkexec.c : 386 main
int
main (int argc, char *argv[])
{
··· ···
··· ···
/* This loop iterates over user input parameters and sets values based on different inputs.
* But the problem is that the loop starts at index 1, without considering the case where the user provides no parameters.
*/
for (n = 1; n < (guint) argc; n++)
{
if (strcmp (argv[n], "--help") == 0)
{
opt_show_help = TRUE;
}
··· ···
else //If it's an unrecognized parameter, break out; this means the parameter is the command to execute
{
break;
}
}
··· ···
g_assert (argv[argc] == NULL);
path = g_strdup (argv[n]); //Get the specific command string
if (path == NULL)
{
···
}
if (path[0] != '/')
{
/* g_find_program_in_path() is not susceptible to attacks via the environment */
//This function searches for the absolute path of the command based on the PATH environment variable
s = g_find_program_in_path (path);
if (s == NULL)
{
···
}
g_free (path);
argv[n] = path = s;//Write back the absolute path to the command line argument
}
··· ···
··· ···
Based on my comments in the code:
pkexec to execute).--, it considers that argument as the command to execute with pkexec, and breaks out of the loop to continue with subsequent logic.g_find_program_in_path to search for the absolute path of the command. g_find_program_in_path searches for the absolute path of the passed argument (command) based on the PATH environment variable. For example, passing cat returns /bin/cat.Although it's easy to understand, the problem is:
When a Linux binary runs, the command line arguments argv[] and environment variables environ[] are placed at the bottom of the stack, and argv[] and environ[] are contiguous. The last item in argv[] is null.

If pkexec is started from the command line without any other arguments, then argv[0] is "pkexec", and argv[1] is \x00 – no problem. But if pkexec is started with execve without any other arguments, then argv[0] is \x00, and argv[1] points to the environment variable! When reading argv[1], it will out-of-bounds read environ[0].
Starting pkexec directly from command line: argc is 1, argv[0] is the path to pkexec:

Starting pkexec with execve: argc is 0:

What impact does this have? When started with execve without any other arguments, argv[] length is 0, so argv[1] equals environ[0]. Then the logic analyzed above becomes: get the value of the first environment variable, and search for its absolute path in the PATH environment variable. If found, write it back to the first environment variable. The exploitation method is as follows:
First, it must be clear that pkexec is a privileged (suid) file:

How can we use environment variables in a privileged file to cause trouble? First, understand a small detail:
The Linux dynamic linker ld-linux-x86-64.so.2 clears sensitive environment variables when executing a privileged program:
Function _dl_non_dynamic_init: glibc-2.27/elf/dl-support.c : 307
void
_dl_non_dynamic_init (void)
{
··· ···
··· ···
if (__libc_enable_secure) // In privileged mode
{
static const char unsecure_envvars[] =
UNSECURE_ENVVARS
#ifdef EXTRA_UNSECURE_ENVVARS
EXTRA_UNSECURE_ENVVARS
#endif
;
const char *cp = unsecure_envvars;
// Loop to clear (unset) all dangerous environment variables in the list
while (cp < unsecure_envvars + sizeof (unsecure_envvars))
{
__unsetenv (cp);
cp = (const char *) __rawmemchr (cp, '\0') + 1;
}
#if !HAVE_TUNABLES
if (__access ("/etc/suid-debug", F_OK) != 0)
__unsetenv ("MALLOC_CHECK_");
#endif
}
··· ···
··· ···
}
The dangerous environment variable list UNSECURE_ENVVARS is defined as:
glibc-2.27/sysdeps/generic/unsecvars.h : 10