
CVE-2021-3156 POC, Docker et compte rendu d'analyse
[toc]
Identifiant de vulnérabilité : CVE-2021-3156
Score de vulnérabilité :
Produit concerné : linux sudo
Versions affectées : 1.8.2-1.8.31sp12; 1.9.0-1.9.5sp1
Conditions d'exploitation : linux local ; sudo est suid et exécutable
Impact : élévation de privilèges locale
Téléchargement des sources : https://www.sudo.ws/getting/source/
Environnement docker : chenaotian/cve-2021-3156
J'ai moi-même construit l'environnement docker, qui fournit :
Tout se trouve dans le répertoire /root :
image-20220124223312224
Test de l'exp :``` cd exp su test ./exp whoami
Le contenu lié au débogage est décrit plus loin dans [quelques commandes de débogage](#一些调试命令)
## Principe de la vulnérabilité
Payload de déclenchement de la vulnérabilité```shell
sudoedit -s '\' `python3 -c "print('A'*80)"`
Analyse du code source (sudo-1.8.21) : Tout d'abord, la fonction main dans sudo.c (sudo.c: 133) :```c int main(int argc, char *argv[], char *envp[]) { int nargc, ok, status = 0; char **nargv, **env_add; char **user_info, **command_info, **argv_out, **user_env_out; struct sudo_settings *settings; struct plugin_container *plugin, *next; sigset_t mask; debug_decl_vars(main, SUDO_DEBUG_MAIN)
··· ···
··· ···
/* Parse command line arguments. */
//在这里处理输入参数,设置sudo_mode
sudo_mode = parse_args(argc, argv, &nargc, &nargv, &settings, &env_add);
··· ···
··· ···
switch (sudo_mode & MODE_MASK) {
··· ···
··· ···
case MODE_EDIT:
case MODE_RUN:
ok = policy_check(&policy_plugin, nargc, nargv, env_add,
&command_info, &argv_out, &user_env_out);
··· ···
··· ···
}
··· ···
··· ···
}
- D'abord, nous appelons la fonction `parse_args` pour traiter les paramètres saisis. En réalité, ici nous n'avons saisi qu'un `-s`, il n'y a rien de particulier à configurer, nous définissons sudo_mode sur MODE_EDIT et MODE_SHELL.
- Ensuite, selon la valeur de sudo_mode, MODE_EDIT appelle policy_check.
Ensuite, la fonction policy_check dans sudo.c (sudo.c: 1136) :```c
static int
policy_check(struct plugin_container *plugin, int argc, char * const argv[],
char *env_add[], char **command_info[], char **argv_out[],
char **user_env_out[])
{
··· ···
··· ···
ret = plugin->u.policy->check_policy(argc, argv, env_add, command_info,
argv_out, user_env_out);
···
}
Le rappel plugin->u.policy->check_policy est appelé ; on peut déboguer pour voir la véritable fonction de ce rappel :
image-20220123113326096
Ce qui est appelé est la fonction sudoers_policy_check de policy.c (policy.c : 760) :```c static int sudoers_policy_check(int argc, char * const argv[], char *env_add[], char **command_infop[], char **argv_out[], char **user_env_out[]) { ··· ···
exec_args.argv = argv_out;
exec_args.envp = user_env_out;
exec_args.info = command_infop;
ret = sudoers_policy_main(argc, argv, 0, env_add, &exec_args);
··· ···
··· ···
}
Ensuite, la fonction sudoers_policy_main dans sudoers.c a été appelée (sudoers.c: 224) :```c
int
sudoers_policy_main(int argc, char * const argv[], int pwflag, char *env_add[],
void *closure)
{
··· ···
··· ···
/*
* Make a local copy of argc/argv, with special handling
* for pseudo-commands and the '-i' option.
*/
if (argc == 0) {
··· ···
} else {
/* Must leave an extra slot before NewArgv for bash's --login */
NewArgc = argc;
NewArgv = reallocarray(NULL, NewArgc + 2, sizeof(char *));
··· ···
}
memcpy(++NewArgv, argv, argc * sizeof(char *));
NewArgv[NewArgc] = NULL;
··· ···
}
}
··· ···
cmnd_status = set_cmnd();
··· ···
··· ···
··· ···
}
Ici, quelques variables globales sont définies, NewArgc et NewArgv comme ci-dessous, qui correspondent en fait aux paramètres passés.
image-20220123113819116
Ensuite, on entre dans la fonction set_cmnd de sudoers.c (sudoers.c : 796) :```c
static int
set_cmnd(void)
{
··· ···
··· ···
/* set user_args */
if (NewArgc > 1) {
char *to, *from, **av;
size_t size, n;
/* Alloc and build up user_args. */
//根据参数总长度计算size, 后续malloc 申请,没有问题
for (size = 0, av = NewArgv + 1; *av; av++)
size += strlen(*av) + 1;
if (size == 0 || (user_args = malloc(size)) == NULL) {
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
debug_return_int(-1);
}
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
/*
* When running a command via a shell, the sudo front-end
* escapes potential meta chars. We unescape non-spaces
* for sudoers matching and logging purposes.
*/
//将所有参数拷贝到一起放到堆中,逻辑是遇到'\'加非空格类型字符则只拷贝非空格字符
//但这里\x00 并不算空格类型字符
//他没有考虑参数如果只有一个'\'或以'\'结尾并且下两个字符后就是另一个字符串情况
for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
from++;
*to++ = *from++;
}
*to++ = ' ';
}
*--to = '\0';
}
··· ···
}
}
··· ···
··· ···
}
L'overflow se produit également ici. D'après les commentaires dans le code, on peut voir que le débordement de tas se produit lors de la copie vers le tas. L'intention originale de ce code est assez simple à comprendre : copier tous les arguments de NewArgv dans le tas, séparés par des espaces, et lorsqu'on rencontre `\+非空格类字符`, ne copier que ce caractère.
**Mais il ne prend pas en compte un cas : si un élément de NewArgv se termine par `\`, cela donne une structure `\+\x00`, et `\x00` n'est pas un caractère de type espace (incroyable). Cela signifie qu'après avoir copié `\x00` dans le tas, la variable from est incrémentée (deux fois dans une seule boucle), ce qui lui fait sauter l'occasion de passer la condition while qui détecte le marqueur de fin `\x00`, et elle considère que les arguments n'ont pas fini d'être copiés et continue à copier jusqu'au prochain `\x00`.**
Dans ce scénario, on peut voir que `\+\x00` est immédiatement suivi du paramètre suivant `A*80`, donc la copie continue jusqu'à la fin de `A*80`. Mais n'oubliez pas que le traitement réel de `A*80` continue ensuite, et qu'il sera copié à nouveau. Au total, `A*80` est donc copié deux fois, alors que le chunk a été alloué pour une taille correspondant à une seule chaîne `A*80`, ce qui dépasse largement la longueur demandée pour le chunk.
image-20220123113907744
Ensuite, cela provoque le débordement. Avant la copie :
image-20220123114036691
Après la copie :
image-20220123114137794
Le chemin global de déclenchement de la vulnérabilité est le suivant (lors du débogage, il suffit de placer des points d'arrêt sur ces fonctions) :
- sudo.c : main
- sudo.c : policy_check
- policy.c : sudoerrs_policy_check
- sudoers.c : sudoers_policy_main
- sudoers.c : set_cmnd
- sudoers.c : 859
## Principe d'exploitation de la vulnérabilité