Baron Samedit
CVE-2021-3156 Caught my eye this week! It's a heap-based buffer overflow affecting versions prior to 1.9.5p2, allowing local privilege escalation to root via sudoedit -s
and a command that ends with a single backslash character.
I want to take the time to understand this one in depth.
Comprehension
As a starting point, I looked up the CVE Disclosure. And then the currently most linked to article about the issue Qualys Blog. I'm essentially just re-writing that last blog post there so I can put it in my own words and fully understand.
Source
If sudo is executed to run a command in "shell" mode, or through the -i
option,
if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
char **av, *cmnd = NULL;
int ac = 1;
at the beginning of Sudo’s main(), parse_args() rewrites argv...
av[0] = (char *)user_details.shell; /* plugin may override shell */
if (cmnd != NULL) {
av[1] = "-c";
av[2] = cmnd;
}
av[ac] = NULL;
argv = av;
argc = ac;
}
...by concatenating all command-line arguments, escaping all meta-characters with backslashes.
for (av = argv; *av != NULL; av++) {
for (src = *av; *src != '\0'; src++) {
/* quote potential meta characters */
if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
*dst++ = '\\';
*dst++ = *src;
}
*dst++ = ' ';
}
Later, in sudoers_policy_main(), set_cmnd() concatenates the command-line arguments into a heap-based buffer “user_args” and unescapes the meta-characters "for sudoers matching and logging purposes."
for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
from++;
*to++ = *from++;
}
*to++ = ' ';
}
Here's the buffer allocation logic:
for (size = 0, av = NewArgv + 1; *av; av++)
size += strlen(*av) + 1;
if (size == 0 || (user_args = malloc(size)) == NULL) {
The Vulnerability
Let's take a closer look at the line that un-escapes the arguments.
If one of the command-line arguments end's with a backslash, then from[0]
is the backslash character and from[1]
is the argument's null terminator:
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
Then, from
is incremented and now points to the null character:
from++;
On the next line, the null terminator is copied to the user_args
buffer, and from
is incremented again and now points to the first character after the null terminator (from pointer is out of bounds now):
*to++ = *from++;
Finally, the while
loop reads and copies out-of-bounds characters to the user_args
buffer, completing the heap-based overflow vulnerability.
The Exploit Vector
In theory this vulnerable code is not a concern. If MODE_SHELL or MODE_LOGIN_SHELL is set (required for reaching the vulnerable piece of code), parse_args() executes and all command-line arguments should have already been escaped with a single bashslash character:
dst++ = '\\';
In reality, there is a slightly different set of conditions between parse_args
(the shell escape code) and set_cmnd
(the vulnerable code):
/* set_cmnd() */
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
/* Versus parse_args() */
if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
So, we want to set MODE_SHELL and either MODE_EDIT or MODE_CHECK, but not MODE_RUN. This isn't normally possible. If we set MODE_EDIT or MODE_CHECK, then parse_args
removes MODE_SHELL from "valid_flags" and exits with an error.
However, Qualys found a loophole!
If you execute sudo as sudoedit
instead of sudo
, then parse_args
automatically sets MODE_EDIT, but does not reset "valid_flags" which includes "MODE_SHELL" by default:
127 #define DEFAULT_VALID_FLAGS (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL)
...
int valid_flags = DEFAULT_VALID_FLAGS;
...
proglen = strlen(progname);
if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
progname = "sudoedit";
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
}
Therefore, executing sudoedit -s
allows us to escape the escape code and reach the vulnerable code, overflowing the heap-based buffer "user_args":
$ sudoedit -s '\' `perl -e 'print "A" x 65536'`
malloc(): corrupted top size
Aborted (core dumped)