Publican los detalles de una vulnerabilidad en sudo que permite escalar a root desde... 2011!

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

-------------------------------------------------------------------- 
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 otras palabras, set_cmnd() es vulnerable a un desbordamiento de búfer en el heap, porque los caracteres out-of-bounds que se copian en el búfer "user_args" no se incluyeron en su tamaño (calculado en líneas 852-853).

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

Fuente: https://blog.qualys.com/vulnerabilities-research/2021/01/26/cve-2021-3156-heap-based-buffer-overflow-in-sudo-baron-samedit

Comentarios