
Technical analysis and exploit development for CVE-2021-3156, a heap-based buffer overflow in Sudo, including three working exploits for local privilege escalation on major Linux distributions.
Qualys Security Advisory
Baron Samedit: Heap-based buffer overflow in Sudo (CVE-2021-3156)
Summary Analysis Exploitation Acknowledgments Timeline
We discovered a heap-based buffer overflow in Sudo (https://www.sudo.ws/). This vulnerability:
is exploitable by any local user (normal users and system users, sudoers and non-sudoers), without authentication (i.e., the attacker does not need to know the user's password);
was introduced in July 2011 (commit 8255ed69), and affects all legacy versions from 1.8.2 to 1.8.31p2 and all stable versions from 1.9.0 to 1.9.5p1, in their default configuration.
We developed three different exploits for this vulnerability, and obtained full root privileges on Ubuntu 20.04 (Sudo 1.8.31), Debian 10 (Sudo 1.8.27), and Fedora 33 (Sudo 1.9.2). Other operating systems and distributions are probably also exploitable.
If Sudo is executed to run a command in "shell" mode (shell -c command):
either through the -s option, which sets Sudo's MODE_SHELL flag;
or through the -i option, which sets Sudo's MODE_SHELL and MODE_LOGIN_SHELL flags;
then, at the beginning of Sudo's main(), parse_args() rewrites argv (lines 609-617), by concatenating all command-line arguments (lines 587-595) and by escaping all meta-characters with backslashes (lines 590-591):
Later, in sudoers_policy_main(), set_cmnd() concatenates the command-line arguments into a heap-based buffer "user_args" (lines 864-871) and unescapes the meta-characters (lines 866-867), "for sudoers matching and logging purposes":
Unfortunately, if a command-line argument ends with a single backslash character, then:
at line 866, "from[0]" is the backslash character, and "from[1]" is the argument's null terminator (i.e., not a space character);
at line 867, "from" is incremented and points to the null terminator;
at line 868, the null terminator is copied to the "user_args" buffer, and "from" is incremented again and points to the first character after the null terminator (i.e., out of the argument's bounds);
the "while" loop at lines 865-869 reads and copies out-of-bounds characters to the "user_args" buffer.
In other words, set_cmnd() is vulnerable to a heap-based buffer overflow, because the out-of-bounds characters that are copied to the "user_args" buffer were not included in its size (calculated at lines 852-853).
In theory, however, no command-line argument can end with a single backslash character: if MODE_SHELL or MODE_LOGIN_SHELL is set (line 858, a necessary condition for reaching the vulnerable code), then MODE_SHELL is set (line 571) and parse_args() already escaped all meta-characters, including backslashes (i.e., it escaped every single backslash with a second backslash).
In practice, however, the vulnerable code in set_cmnd() and the escape code in parse_args() are surrounded by slightly different conditions:
versus:
Our question, then, is: can we set MODE_SHELL and either MODE_EDIT or MODE_CHECK (to reach the vulnerable code) but not the default MODE_RUN (to avoid the escape code)?
The answer, it seems, is no: if we set MODE_EDIT (-e option, line 361) or MODE_CHECK (-l option, lines 423 and 519), then parse_args() removes MODE_SHELL from the "valid_flags" (lines 363 and 424) and exits with an error if we specify an invalid flag such as MODE_SHELL (lines 532-533):
But we found a loophole: if we execute Sudo as "sudoedit" instead of "sudo", then parse_args() automatically sets MODE_EDIT (line 270) but does not reset "valid_flags", and the "valid_flags" include MODE_SHELL by default (lines 127 and 249):