Qualys Security Advisory

pwnkit: Local Privilege Escalation in polkit's pkexec (CVE-2021-4034)

========================================================================

Contents

========================================================================

Summary

Analysis

Exploitation

Acknowledgments

Timeline

========================================================================

Summary

========================================================================

We discovered a Local Privilege Escalation (from any user to root) in

polkit's pkexec, a SUID-root program that is installed by default on

every major Linux distribution:

"Polkit (formerly PolicyKit) is a component for controlling system-wide

privileges in Unix-like operating systems. It provides an organized way

for non-privileged processes to communicate with privileged ones. [...]

It is also possible to use polkit to execute commands with elevated

privileges using the command pkexec followed by the command intended to

be executed (with root permission)." (Wikipedia)

This vulnerability is an attacker's dream come true:

- pkexec is installed by default on all major Linux distributions (we

exploited Ubuntu, Debian, Fedora, CentOS, and other distributions are

probably also exploitable);

- pkexec is vulnerable since its creation, in May 2009 (commit c8c3d83,

"Add a pkexec(1) command");

- any unprivileged local user can exploit this vulnerability to obtain

full root privileges;

- although this vulnerability is technically a memory corruption, it is

exploitable instantly, reliably, in an architecture-independent way;

- and it is exploitable even if the polkit daemon itself is not running.

We will not publish our exploit immediately; however, please note that

this vulnerability is trivially exploitable, and other researchers might

publish their exploits shortly after the patches are available. If no

patches are available for your operating system, you can remove the

SUID-bit from pkexec as a temporary mitigation; for example:

chmod 0755 /usr/bin/pkexec

This vulnerability is one of our most beautiful discoveries; to honor

its memory, we recommend listening to DJ Pone's "Falken's Maze" (double

pun intended) while reading this advisory. Thank you very much!

========================================================================

Analysis

========================================================================

pkexec is a sudo-like, SUID-root program, described as follows by its

man page:

------------------------------------------------------------------------

NAME

pkexec - Execute a command as another user

SYNOPSIS

pkexec [--version] [--disable-internal-agent] [--help]

pkexec [--user username] PROGRAM [ARGUMENTS...]

DESCRIPTION

pkexec allows an authorized user to execute PROGRAM as another

user. If PROGRAM is not specified, the default shell will be run.

If username is not specified, then the program will be executed

as the administrative super user, root.

------------------------------------------------------------------------

The beginning of pkexec's main() function processes the command-line

arguments (lines 534-568), and searches for the program to be executed

(if its path is not absolute) in the directories of the PATH environment

variable (lines 610-640):

------------------------------------------------------------------------

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 }

------------------------------------------------------------------------

Unfortunately, if the number of command-line arguments argc is 0 (if the

argument list argv that we pass to execve() is empty, i.e. {NULL}), then

argv[0] is NULL (the argument list's terminator) and:

- at line 534, the integer n is permanently set to 1;

- at line 610, the pointer path is rea out-of-bounds from argv[1];

- at line 639, the pointer s is written out-of-bounds to argv[1].

But what exactly is read from and written to this out-of-bounds argv[1]?

To answer this question, we must digress briefly. When we execve() a new

program, the kernel copies our argument and environment strings and

pointers (argv and envp) to the end of the new program's stack; for

example:

|---------+---------+-----+------------|---------+---------+-----+------------|

| argv[0] | argv[1] | ... | argv[argc] | envp[0] | envp[1] | ... | envp[envc] |

|----|----+----|----+-----+-----|------|----|----+----|----+-----+-----|------|

V V V V V V

"program" "-option" NULL "value" "PATH=name" NULL

Clearly (because the argv and envp pointers are contiguous in memory),

if argc is 0, then the out-of-bounds argv[1] is actually envp[0], the

pointer to our first environment variable, "value". Consequently:

- at line 610, the path of the program to be executed is read

out-of-bounds from argv[1] (i.e. envp[0]), and points to "value";

- at line 632, this path "value" is passed to g_find_program_in_path()

(because "value" does not start with a slash, at line 629);

- g_find_program_in_path() searches for an executable file named "value"

in the directories of our PATH environment variable;

- if such an executable file is found, its full path is returned to

pkexec's main() function (at line 632);

- and at line 639, this full path is written out-of-bounds to argv[1]

(i.e. envp[0]), thus overwriting our first environment variable.

More precisely:

- if our PATH environment variable is "PATH=name", and if the directory

"name" exists (in the current working directory) and contains an

executable file named "value", then a pointer to the string

"name/value" is written out-of-bounds to envp[0];

- or, if our PATH is "PATH=name=.", and if the directory "name=." exists

and contains an executable file named "value", then a pointer to the

string "name=./value" is written out-of-bounds to envp[0].

In other words, this out-of-bounds write allows us to re-introduce an

"unsecure" environment variable (for example, LD_PRELOAD) into pkexec's

environment; these "unsecure" variables are normally removed (by ld.so)

from the environment of SUID programs before the main() function is

called. We will exploit this powerful primitive in the following

section.

Last-minute note: polkit also supports non-Linux operating systems such

as Solaris and *BSD, but we have not investigated their exploitability;

however, we note that OpenBSD is not exploitable, because its kernel

refuses to execve() a program if argc is 0.

========================================================================

Exploitation

========================================================================

Our question is: to successfully exploit this vulnerability, which

"unsecure" variable should we re-introduce into pkexec's environment?

Our options are limited, because shortly after the out-of-bounds write

(at line 639), pkexec completely clears its environment (at line 702):

------------------------------------------------------------------------

639 argv[n] = path = s;

...

657 for (n = 0; environment_variables_to_save[n] != NULL; n++)

658 {

659 const gchar *key = environment_variables_to_save[n];

...

662 value = g_getenv (key);

...

670 if (!validate_environment_variable (key, value))

...

675 }

...

702 if (clearenv () != 0)

------------------------------------------------------------------------

The answer to our question comes from pkexec's complexity: to print an

error message to stderr, pkexec calls the GLib's function g_printerr()

(note: the GLib is a GNOME library, not the GNU C Library, aka glibc);

for example, the functions validate_environment_variable() and

log_message() call g_printerr() (at lines 126 and 408-409):

------------------------------------------------------------------------

88 log_message (gint level,

89 gboolean print_to_stderr,

90 const gchar *format,

91 ...)

92 {

...

125 if (print_to_stderr)

126 g_printerr ("%s\n", s);

------------------------------------------------------------------------

383 validate_environment_variable (const gchar *key,

384 const gchar *value)

385 {

...

406 log_message (LOG_CRIT, TRUE,

407 "The value for the SHELL variable was not found the /etc/shells file");

408 g_printerr ("\n"

409 "This incident has been reported.\n");

------------------------------------------------------------------------

g_printerr() normally prints UTF-8 error messages, but it can print

messages in another charset if the environment variable CHARSET is not

UTF-8 (note: CHARSET is not security sensitive, it is not an "unsecure"

environment variable). To convert messages from UTF-8 to another

charset, g_printerr() calls the glibc's function iconv_open().

To convert messages from one charset to another, iconv_open() executes

small shared libraries; normally, these triplets ("from" charset, "to"

charset, and library name) are read from a default configuration file,

/usr/lib/gconv/gconv-modules. Alternatively, the environment variable

GCONV_PATH can force iconv_open() to read another configuration file;

naturally, GCONV_PATH is one of the "unsecure" environment variables

(because it leads to the execution of arbitrary libraries), and is

therefore removed by ld.so from the environment of SUID programs.

Unfortunately, CVE-2021-4034 allows us to re-introduce GCONV_PATH into

pkexec's environment, and to execute our own shared library, as root.

Important: this exploitation technique leaves traces in the logs (either

"The value for the SHELL variable was not found the /etc/shells file" or

"The value for environment variable [...] contains suscipious content").

However, please note that this vulnerability is also exploitable without

leaving any traces in the logs, but this is left as an exercise for the

interested reader.

For further discussions about pkexec, GLib, and GCONV_PATH, please refer

to the following posts by Tavis Ormandy, Jakub Wilk, and Yuki Koike:

https://www.openwall.com/lists/oss-security/2014/07/14/1

https://www.openwall.com/lists/oss-security/2017/06/23/8

https://hugeh0ge.github.io/2019/11/04/Getting-Arbitrary-Code-Execution-from-fopen-s-2nd-Argument/

========================================================================

Acknowledgments

========================================================================

We thank polkit's authors, Red Hat Product Security, and the members of

distros@openwall for their invaluable help with the disclosure of this

vulnerability. We also thank Birdy Nam Nam for their inspiring work.

========================================================================

Timeline

========================================================================

2021-11-18: Advisory sent to secalert@redhat.

2022-01-11: Advisory and patch sent to distros@openwall.

2022-01-25: Coordinated Release Date (5:00 PM UTC).d