Injection NoSQL : MongoDB

To the non-french speaker, note that you can translate the articles using the Google Trad widget situated at the bottom of all pages.


MongoDB  est un moteur de base de données NoSQL écrit en C++. Contrairement à Mysql, Oracle et autres SGBD classiques, les bases de données MongoDB ne reposent pas sur un schéma défini. Vous n’y trouverez pas de tables/colonnes comme on peut en avoir sur un SGBD SQL.

D’ailleurs les informations que l’on importe dans MongoDB sont généralement uploadées au format json. Mais ça on le verra plus tard. Pour commencer nous allons voir comment installer MongoDB et bordel rien que ça c’est une épreuve.

I/ Installation

La première chose à faire c’est d’installer le package MongoDB

sudo apt install mongodb

Ensuite j’ai du installer l’utilitaire PECL afin d’installer la librairie PHP correspondante :

sudo apt install php-pear
sudo pecl install php-mongodb

Si vous avez une erreur XML de ce type :

4.png

Alors il faudra réinstaller toutes les librairies PHP-XML en utilisant cette commande:

sudo apt-get purge php*-xml
sudo apt-get autoremove php*-xml
sudo apt-get install php-xml php7.0-xml

Dans le fichier de configuration php.ini de votre serveur Apache il va falloir ajouter cette ligne :

extension=mongodb.so

Enfin, j’ai été obligé d’installer composer pour régler certaines dépendances étranges :

sudo apt install composer
composer require mongodb/mongodb

Et j’ai redémarré à la fois le service PHP et Apache2… il doit clairement y avoir des manières bien plus simples de le faire mais pour je ne sais quelles raisons j’ai galéré pendant plus d’une heure…

II/ Mise en place d’une BDD mongoDB

Avant tout on va démarrer le service à l’aide de l’utilitaire mongod :

mongod -f /etc/mongodb.conf

Puis on va accéder à l’interface mongodb en utilisant cette commande :

mongo

Et créer une base de données que l’on appellera users :

use users

Pour l’import des données, comme je vous l’ai dis plus haut, ça se fera au format Json. J’ai donc créé quatre utilisateurs qui ont tous un identifiant, un username, un password et un email :

{"id" : "1", "username" : "admin", "password" : "admin_password", "email" : "admin@whiteflag.fr"}
{"id" : "2", "username" : "user1", "password" : "user_password", "email" : "user1@whiteflag.fr"}
{"id" : "3", "username" : "user2", "password" : "user2_password", "email" : "use2@whiteflag.fr"}
{"id" : "4", "username" : "whatever", "password" : "whatver_pass", "email" : "whatever@whiteflag.fr"}

Il ne nous reste plus qu’à importer ce fichier dans la base de données users :

mondoimport --db users --collection users "fichier_json"

5

Comme dans un SGBD SQL, on va pouvoir effectuer plusieurs actions sur nos données :

  • Sélectionner des données :
db.users.find({"user":"admin"})

2.png

  • Ajouter des données :
db.users.insert({"id":"5", "username":"whatever", "password":"whatever_again", "email":"whatever@whatever.com"})

3.png

  • Supprimer des données :
db.users.deleteOne({"id":"4"})

6.png

  • Supprimer une base de données :
db.users.dropDatabase()

10.png

Mais le plus intéressant c’est les techniques d’exploitation d’injections NoSQL 😈 !

III/ Injection NoSQL

Comme d’habitude j’ai créé un fichier PHP test que l’on utilisera tout au long de cet article et que vous pourrez télécharger ici : nosqlinjection.

6.png

Ce fichier PHP ne fait rien de plus que récupérer les identifiants soumis via le formulaire et requêter la base afin de voir si l’utilisateur existe ou non.

Comme vous pouvez vous en douter, les habituels payloads tels que ‘ »or 1=1;– -‘ ne fonctionneront pas ici. De toute façon nous n’en aurons pas besoin.

En effet MongoDB met à disposition des utilisateurs de nouveaux opérateurs tels que l’opérateur or ($or), and ($and), equal ($eq), not equal ($ne) ou encore regex ($regex). C’est avec ces opérateurs que nous allons pouvoir exploiter la SQLi.

Notre but en tant qu’attaquant va être de se connecter en tant qu’administrateur du site et pour simplifier l’explication qui va suivre je vous montre de suite comment exploiter l’injection avec ce payload :

localhost/test/?user[$regex]=^a.*&pass[$regex]=(.*)

Comme prévu je suis connecté en tant qu’administrateur :

9.png
Alors que s’est-il passé ? Pour le comprendre il est important de savoir que par défaut le langage PHP encode les tableaux en JSON. Du coup un tableau déclaré ainsi en PHP :

array("username" => "admin", "password" => "admin_password");

est encodé de cette manière :

{"username" : "admin", "password" : "admin_password"}

Lorsque un utilisateur lambda entre ses identifiants, la requête NoSQL qui est exécutée est la suivante :

$cursor = $collections(array("username" : "un_identifiant", "password" : "un_password"));

Ce qui revient à exécuter cette requête depuis l’invite de commande interactif mongodb :

db.users.find({"username" : "un_identifiant", "password" : "un_password"})

Là où ça devient un peu plus tricky c’est que nous, avec notre payload, nous n’avons pas juste déclarer une variable « user » et une variable « pass ». Nous avons déclaré deux variables qui sont en fait des tableaux !

Eh oui, il est possible de déclarer des tableaux directement dans l’URL en utilisant cette syntaxe :

http://localhost/test/?my_tab[index]=valeur

Par conséquent lorsque nous utilisons ce payload :

user[$regex]=^a.*&pass[$regex]=(.*)

Nous créons tout simplement deux variable de type tableau qui seront ensuite réarrangées dans la requête NoSQL de cette manière :

$cursor = $collections(array("username" => array("$regex" => "^a.*"), "password" => array( "$regex" => "(.*)));

Or comme nous l’avons vu plus haut, PHP encode automatique les tableaux en Json par conséquent la requête finale qui est exécutée est la suivante :

db.user.find({"username" : {$regex : "^a.*"}, "password" : {$regex : "(.*)"}})

Ce qui revient donc à rechercher tous les noms d’utilisateur qui commencent par la lettre « a » et ce peu importe leurs mots de passe (la regex (.*) match toutes les chaînes de caractères possibles).

En exploitant un comportement par défaut du langage PHP nous avons pu exploiter notre injection et ainsi nous connecter à l’application.

IV/ Remédiations

MongoDB ne supporte pas les requêtes préparées. Par conséquent il peut être assez complexe d’empêcher toutes injections. La seule méthode que je connaisse et de caster l’input de l’utilisateur en string. Il suffira donc de modifier la requête :

$cursor = $collection->find(array(
        'username' => $_GET['user'],
        'password' => $_GET['pass'],
    ));

en

$cursor = $collection->find(array(
        'username' => (string)$_GET['user'],
        'password' => (string)$_GET['pass'],
    ));

Pour empêcher toute injection.

EDIT : après avoir fait d’autres recherches je me suis rendu compte qu’on pouvait aussi empêcher les injections NoSQL en vérifiant que les variables passées en paramètre ne sont pas de type « array » :

if (!is_array($_GET['user']) and !is_array($_GET['pass'])) {
    // On exécute la requête
}

😛

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion /  Changer )

Photo Google

Vous commentez à l'aide de votre compte Google. Déconnexion /  Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion /  Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion /  Changer )

Connexion à %s