CVE-2021-3156: Heap-Based Buffer Overflow in Sudo (Baron Samedit)

The Qualys Research Team has discovered a heap overflow vulnerability in sudo, a near-ubiquitous utility available on major Unix-like operating systems. Any unprivileged user can gain root privileges on a vulnerable host using a default sudo configuration by exploiting this vulnerability.

Sudo is a powerful utility that’s included in most if not all Unix- and Linux-based OSes. It allows users to run programs with the security privileges of another user. The vulnerability itself has been hiding in plain sight for nearly 10 years. It 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.

Successful exploitation of this vulnerability allows any unprivileged user to gain root privileges on the vulnerable host. Qualys security researchers have been able to independently verify the vulnerability and develop multiple variants of exploit and obtain 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 also likely to be exploitable.

As soon as the Qualys research team confirmed the vulnerability, Qualys engaged in responsible vulnerability disclosure and coordinated with sudo’s author and open source distributions to announce the vulnerability.

Disclosure Timeline

  • 2021-01-13: Advisory sent to Todd.Miller@sudo
  • 2021-01-19: Advisory and patches sent to distros@openwall
  • 2021-01-26: Coordinated Release Date (6:00 PM UTC)

Proof of Concept Video 

Technical Details

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):
-------------------------------------------------------------------- 571 if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { 572 char **av, *cmnd = NULL; 573 int ac = 1; ... 581 cmnd = dst = reallocarray(NULL, cmnd_size, 2); ... 587 for (av = argv; *av != NULL; av++) { 588 for (src = *av; *src != '\0'; src++) { 589 /* quote potential meta characters */ 590 if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$') 591 *dst++ = '\\'; 592 *dst++ = *src; 593 } 594 *dst++ = ' '; 595 } ... 600 ac += 2; /* -c cmnd */ ... 603 av = reallocarray(NULL, ac + 1, sizeof(char *)); ... 609 av[0] = (char *)user_details.shell; /* plugin may override shell */ 610 if (cmnd != NULL) { 611 av[1] = "-c"; 612 av[2] = cmnd; 613 } 614 av[ac] = NULL; 615 616 argv = av; 617 argc = ac; 618 } --------------------------------------------------------------------- 

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”:

-------------------------------------------------------------- 819 if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { ... 852 for (size = 0, av = NewArgv + 1; *av; av++) 853 size += strlen(*av) + 1; 854 if (size == 0 || (user_args = malloc(size)) == NULL) { ... 857 } 858 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { ... 864 for (to = user_args, av = NewArgv + 1; (from = *av); av++) { 865 while (*from) { 866 if (from[0] == '\\' && !isspace((unsigned char)from[1])) 867 from++; 868 *to++ = *from++; 869 } 870 *to++ = ' '; 871 } ... 884 } ... 886 } --------------------------------------------------------------------- 

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 lines852-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:

--------------------------------------------------------------------- 819 if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { ... 858 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { --------------------------------------------------------------------- 

versus:

--------------------------------------------------------------------- 571 if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { --------------------------------------------------------------------- 

Our question 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):

--------------------------------------------------------------------- 358 case 'e': ... 361 mode = MODE_EDIT; 362 sudo_settings[ARG_SUDOEDIT].value = "true"; 363 valid_flags = MODE_NONINTERACTIVE; 364 break; ... 416 case 'l': ... 423 mode = MODE_LIST; 424 valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST; 425 break; ... 518 if (argc > 0 && mode == MODE_LIST) 519 mode = MODE_CHECK; ... 532 if ((flags & valid_flags) != flags) 533 usage(1); --------------------------------------------------------------------- 

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):

--------------------------------------------------------------------- 127 #define DEFAULT_VALID_FLAGS (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL) ... 249 int valid_flags = DEFAULT_VALID_FLAGS; ... 267 proglen = strlen(progname); 268 if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) { 269 progname = "sudoedit"; 270 mode = MODE_EDIT; 271 sudo_settings[ARG_SUDOEDIT].value = "true"; 272 } ------------------------------------------------------------------------ 

Consequently, if we execute “sudoedit -s”, then we set both MODE_EDIT and MODE_SHELL (but not MODE_RUN), we avoid the escape code, reach the vulnerable code, and overflow the heap-based buffer “user_args” through a command-line argument that ends with a single backslash character:

--------------------------------------------------------------------- sudoedit -s '\' `perl -e 'print "A" x 65536'` malloc(): corrupted top size Aborted (core dumped) ---------------------------------------------------------------------

From an attacker’s point of view, this buffer overflow is ideal due to following reasons:

1) The attacker controls the size of the “user_args” buffer that can be overflowed (the size of our concatenated command-line arguments, at lines 852-854);

2) The attacker independently controls the size and contents of the overflow itself (our last command-line argument is conveniently followed by our first environment variables, which are not included in the size calculation at lines 852-853);

3) The attacker can even write null bytes to the buffer that was overflowed (every command-line argument or environment variable that ends with a single backslash writes a null byte to “user_args”, at lines 866-868).

For example, on an amd64 Linux, the following command allocates a 24-byte “user_args” buffer (a 32-byte heap chunk) and overwrites the next chunk’s size field with “A=a\0B=b\0” (0x00623d4200613d41), its fd field with “C=c\0D=d\0” (0x00643d4400633d43), and its bk field with “E=e\0F=f\0” (0x00663d4600653d45):

--------------------------------------------------------------------- env -i 'AA=a\' 'B=b\' 'C=c\' 'D=d\' 'E=e\' 'F=f' sudoedit -s '1234567890123456789012\' --------------------------------------------------------------------- --|--------+--------+--------+--------|--------+--------+--------+--------+-- | | |12345678|90123456|789012.A|A=a.B=b.|C=c.D=d.|E=e.F=f.| --|--------+--------+--------+--------|--------+--------+--------+--------+-- size <---- user_args buffer ----> size fd bk 

Solution

Given the breadth of the attack surface for this vulnerability, Qualys recommends users apply patches for this vulnerability immediately.

Qualys customers can search the vulnerability knowledgebase for CVE-2021-3156 to identify all the QIDs and assets vulnerable for this vulnerability.

If you are not a customer, start your free Qualys VMDR trial to get full access to the QIDs (detections) for CVE-2021-3156, so you can identify your vulnerable assets.

Qualys Coverage

QID 374891: Sudo Heap-Based Buffer Overflow Vulnerability

The QID is available in vulnsigs version VULNSIGS-2.5.90-4 and in Linux Cloud Agent manifest version lx_manifest-2.5.90.4-3.

Dashboard

With VMDR Dashboard, you can track this vulnerability, their impacted hosts, their status and overall management in real time. With trending enabled for dashboard widgets, you can keep track of these vulnerabilities trends in your environment using the “Baron Samedit | Heap-based buffer overflow Sudo” Dashboard.

View and download the Baron Samedit dashboard.

Baron Samedit dashboard

Vendor References

Frequently Asked Questions (FAQs)

What versions are vulnerable?

The following versions of sudo are vulnerable:

  • All legacy versions from 1.8.2 to 1.8.31p2
  • All stable versions from 1.9.0 to 1.9.5p1
How can I test if I have vulnerable version?

To test if a system is vulnerable or not, login to the system as a non-root user.

Run command “sudoedit -s /”

If the system is vulnerable, it will respond with an error that starts with “sudoedit:”

If the system is patched, it will respond with an error that starts with “usage:”

Are versions before 1.8.2 vulnerable?

No. See explanation above.

Is a local user required to exploit the vulnerability?

Yes, however this user does not need to be a privileged user or be a part of sudoers list. For example, even account ‘nobody’ can exploit the issue.

Why name the vulnerability “Baron Samedit”?

It’s a play on Baron Samedi and sudoedit.

Will Qualys Research Team publish exploit code for this vulnerability? 

No.