Investigadores de Qualys han descubierto un heap overflow en sudo que, en su configuración por defecto, puede permitir a cualquier usuario local acceder como root en un sistema vulnerable.
La vulnerabilidad ha sido etiquetada como CVE-2021-3156, referencia también como Baron Samedi, y estuvo ahí durante casi 10 años, desde el commit 8255ed69 hecho en junio de 2011. Concretamente afecta a las versiones de la 1.8.2 a la 1.8.31p2 y de la 1.9.0 a la 1.9.5p1 de sudo. Qualys desarrolló exploits para varias distribuciones de Linux, incluyendo Ubuntu 20.04 (Sudo 1.8.31), Debian 10 (Sudo 1.8.27) y Fedora 33 (Sudo 1.9.2), y se cree que otras distribuciones también son vulnerables.
Detalle técnico explicado *brillantemente* por Qualys
Para ejecutar sudo en modo "shell" podemos usar los parámetros:
-s : flag MODE_SHELL
-i : flags MODE_SHELL y MODE_LOGIN_SHELL
y después el comando deseado. Como veis a continuación, al comienzo del main() de Sudo parse_args() reescribe argv (líneas 609-617), concatenando todos los argumentos de la línea de comandos (líneas 587-595) y escapando todos los metacaracteres con "\" (líneas 590-591):
El problema viene si el argumento que le pasamos termina con un solo carácter "\", entonces:
En teoría ningún argumento pasado en la línea de comandos puede terminar con un solo carácter "\": si MODE_SHELL o MODE_LOGIN_SHELL están configurados (línea 858, una condición necesaria para llegar al código vulnerable), entonces MODE_SHELL está configurado (línea 571) y parse_args() ya se escapó de todos los metacaracteres, incluidos los "\" (es decir, se escapó de cada "\" con un segundo "\").
En la práctica, sin embargo, el código vulnerable en set_cmnd() y el código de escape en parse_args() tienen condiciones ligeramente diferentes:
La pregunta es: ¿podemos configurar MODE_SHELL y MODE_EDIT o MODE_CHECK (para llegar al código vulnerable) pero no el MODE_RUN predeterminado (para evitar el código de escape)?
La respuesta, al parecer, es no: si configuramos MODE_EDIT (opción -e, línea 361) o MODE_CHECK (opción -l, líneas 423 y 519), entonces parse_args() elimina MODE_SHELL de "valid_flags" (líneas 363 y 424) ) y sale con un error si especificamos una flag no válida como MODE_SHELL (líneas 532-533):
Pero la gente de Qualys encontró una pequeña laguna: si ejecutamos Sudo como "sudoedit" en lugar de "sudo", entonces parse_args() establece automáticamente MODE_EDIT (línea 270) pero no restablece "valid_flags", y los "valid_flags" incluyen MODE_SHELL por defecto (líneas 127 y 249):
Por lo tanto, si ejecutamos "sudoedit -s", entonces seteamos MODE_EDIT y MODE_SHELL (pero no MODE_RUN), evitamos el código de escape, llegamos al código vulnerable y desbordamos el heap "user_args" a través de nuestro argumento que termina con un solo carácter "\":
Desde el punto de vista de un atacante, este desbordamiento de búfer es ideal debido a las siguientes razones:
1) El atacante controla el tamaño del búfer “user_args” que puede desbordarse (el tamaño de nuestros argumentos de línea de comandos concatenados, en las líneas 852-854)
2) El atacante controla de forma independiente el tamaño y el contenido del desbordamiento en sí (nuestro último argumento de línea de comandos es seguido convenientemente por nuestras primeras variables de entorno, que no se incluyen en el cálculo de tamaño en las líneas 852-853);
3) El atacante puede incluso escribir bytes nulos en el búfer que se desbordó (cada argumento de línea de comando o variable de entorno que termine con una sola barra invertida escribe un byte nulo en "user_args", en las líneas 866-868).
Por ejemplo, en un amd64 Linux, el siguiente comando asigna un búfer "user_args" de 24 bytes (un fragmento de pila de 32 bytes) y sobrescribe el campo de tamaño del fragmento siguiente con "A=a\0B=b\0" (0x00623d4200613d41), su campo fd con "C=c\0D=d\0" (0x00643d4400633d43), y su campo bk con "E=e\0F=f\0" (0x00663d4600653d45):
La vulnerabilidad ha sido etiquetada como CVE-2021-3156, referencia también como Baron Samedi, y estuvo ahí durante casi 10 años, desde el commit 8255ed69 hecho en junio de 2011. Concretamente afecta a las versiones de la 1.8.2 a la 1.8.31p2 y de la 1.9.0 a la 1.9.5p1 de sudo. Qualys desarrolló exploits para varias distribuciones de Linux, incluyendo Ubuntu 20.04 (Sudo 1.8.31), Debian 10 (Sudo 1.8.27) y Fedora 33 (Sudo 1.9.2), y se cree que otras distribuciones también son vulnerables.
Detalle técnico explicado *brillantemente* por Qualys
Para ejecutar sudo en modo "shell" podemos usar los parámetros:
-s : flag MODE_SHELL
-i : flags MODE_SHELL y MODE_LOGIN_SHELL
y después el comando deseado. Como veis a continuación, al comienzo del main() de Sudo parse_args() reescribe argv (líneas 609-617), concatenando todos los argumentos de la línea de comandos (líneas 587-595) y escapando todos los metacaracteres con "\" (líneas 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 }
---------------------------------------------------------------------
Más tarde, en sudoers_policy_main(), set_cmnd() concatena los argumentos de la línea de comandos en un búfer del heap "user_args" (líneas 864-871) y elimina el escape de los metacaracteres (líneas 866-867), para que case con el sudoers y loggear correctamente:
--------------------------------------------------------------
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 }
---------------------------------------------------------------------
El problema viene si el argumento que le pasamos termina con un solo carácter "\", entonces:
- en la línea 866, "from[0]" es el carácter "\" y "from[1]" es un null (es decir, no un carácter de espacio)
- en la línea 867, "from" se incrementa y apunta a null
- en la línea 868, null se copia al búfer "user_args", y "from" se incrementa nuevamente y apunta al primer carácter después del null (es decir, fuera de los límites del argumento);
- el loop "while" en las líneas 865-869 lee y copia los caracteres fuera de los límites en el búfer "user_args".
En teoría ningún argumento pasado en la línea de comandos puede terminar con un solo carácter "\": si MODE_SHELL o MODE_LOGIN_SHELL están configurados (línea 858, una condición necesaria para llegar al código vulnerable), entonces MODE_SHELL está configurado (línea 571) y parse_args() ya se escapó de todos los metacaracteres, incluidos los "\" (es decir, se escapó de cada "\" con un segundo "\").
En la práctica, sin embargo, el código vulnerable en set_cmnd() y el código de escape en parse_args() tienen condiciones ligeramente diferentes:
---------------------------------------------------------------------
819 if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
858 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
---------------------------------------------------------------------
contra:---------------------------------------------------------------------
571 if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
---------------------------------------------------------------------
La pregunta es: ¿podemos configurar MODE_SHELL y MODE_EDIT o MODE_CHECK (para llegar al código vulnerable) pero no el MODE_RUN predeterminado (para evitar el código de escape)?
La respuesta, al parecer, es no: si configuramos MODE_EDIT (opción -e, línea 361) o MODE_CHECK (opción -l, líneas 423 y 519), entonces parse_args() elimina MODE_SHELL de "valid_flags" (líneas 363 y 424) ) y sale con un error si especificamos una flag no válida como MODE_SHELL (líneas 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);
---------------------------------------------------------------------
Pero la gente de Qualys encontró una pequeña laguna: si ejecutamos Sudo como "sudoedit" en lugar de "sudo", entonces parse_args() establece automáticamente MODE_EDIT (línea 270) pero no restablece "valid_flags", y los "valid_flags" incluyen MODE_SHELL por defecto (líneas 127 y 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 }
------------------------------------------------------------------------
Por lo tanto, si ejecutamos "sudoedit -s", entonces seteamos MODE_EDIT y MODE_SHELL (pero no MODE_RUN), evitamos el código de escape, llegamos al código vulnerable y desbordamos el heap "user_args" a través de nuestro argumento que termina con un solo carácter "\":
---------------------------------------------------------------------
sudoedit -s '\' `perl -e 'print "A" x 65536'`
malloc(): corrupted top size
Aborted (core dumped)
---------------------------------------------------------------------
Desde el punto de vista de un atacante, este desbordamiento de búfer es ideal debido a las siguientes razones:
1) El atacante controla el tamaño del búfer “user_args” que puede desbordarse (el tamaño de nuestros argumentos de línea de comandos concatenados, en las líneas 852-854)
2) El atacante controla de forma independiente el tamaño y el contenido del desbordamiento en sí (nuestro último argumento de línea de comandos es seguido convenientemente por nuestras primeras variables de entorno, que no se incluyen en el cálculo de tamaño en las líneas 852-853);
3) El atacante puede incluso escribir bytes nulos en el búfer que se desbordó (cada argumento de línea de comando o variable de entorno que termine con una sola barra invertida escribe un byte nulo en "user_args", en las líneas 866-868).
Por ejemplo, en un amd64 Linux, el siguiente comando asigna un búfer "user_args" de 24 bytes (un fragmento de pila de 32 bytes) y sobrescribe el campo de tamaño del fragmento siguiente con "A=a\0B=b\0" (0x00623d4200613d41), su campo fd con "C=c\0D=d\0" (0x00643d4400633d43), y su campo bk con "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
Vídeo con la PoC
PoCs exploit:
https://github.com/lockedbyte/CVE-Exploits/tree/master/CVE-2021-3156
Comentarios
Publicar un comentario