En este artículo veremos cómo explotar una vulnerabilidad de tipo php array injection en MongoDB. Para ello continuaremos donde lo dejamos en el artículo en el que vimos la instalación de la base de datos NoSQL MongoDB 3.4 y el soporte de PHP7.
Antes de continuar, me gustaría señalar que este artículo está basado en el paper NoSQL, No Injection?, del que hay también esta magnífica presentación. En él se detallan algunas de las vulnerabilidades que pueden darse en aplicaciones y despliegues que hagan uso de este tipo de bases de datos. Recomiendo su lectura porque es la fuente original y es mucho más rigurosa. El valor que aporto con este artículo es describir en lengua hispana cómo explotar paso a paso un ejemplo de aplicación vulnerable del primer tipo de vulnerabilidad analizada en el paper: las php array injection contra un gestor MongoDB.
1. Creación de base de datos de prueba
Para construir nuestro escenario, continuaremos donde lo dejamos en el artículo anterior: ya tenemos la base de datos instalada, creado un usuario administrador para que así no tenga acceso administrativo cualquier proceso local de la máquina e instalado un servidor web apache con php7 y soporte de MongoDB.
Ahora vamos a crear la base de datos de nuestra aplicación. Para ello en primer lugar crearemos un usuario con el que nuestra aplicación se conectará a dicha base de datos:
miappdb
y usuarioluisadm@mongodb:~$ mongo -u luisadm -p --authenticationDatabase admin
MongoDB shell version v3.4.10
Enter password:
connecting to: mongodb://127.0.0.1:27017
MongoDB server version: 3.4.10
> use miappdb
switched to db miappdb
> db.createUser(
... {
... user: "usuariodb",
... pwd: "12345",
... roles: [ { role: "readWrite", db: "miappdb"} ]
... }
... )
Successfully added user: {
"user" : "usuariodb",
"roles" : [
{
"role" : "readWrite",
"db" : "miappdb"
}
]
}
> quit()
Ahora probamos a conectar con dicho usuario e insertaremos los datos de nuestra base de datos:
luisadm@mongodb:~$ mongo -u usuariodb -p --authenticationDatabase miappdb
MongoDB shell version v3.4.10
Enter password:
connecting to: mongodb://127.0.0.1:27017
MongoDB server version: 3.4.10
> use miappdb
switched to db miappdb
> db.usuarios.insert(
... [ { login: "juan", nombre: "Juan Martínez", password: "juan23" },
... { login: "manolo", nombre: "Manolo García", password: "manolo23" },
... { login: "pepe", nombre: "Pepe Perez", password: "pepe23" }
... ]
... )
BulkWriteResult({
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 3,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
})
> quit()
2. Instalación de la aplicación de prueba
Para esta aplicación me he inspirado en esta que hay en: https://www.formget.com/login-form-in-php/ adaptándola a MongoDB.
Nótese que esta aplicación es una Prueba De Concepto (PoC), nunca deberíamos hacer algo así en "el mundo real". |
Para ello crearemos los siguientes ficheros dentro de la trayectoria
/var/www/html/mongodb
de nuestro servidor, donde tendremos las librerías
instaladas mediante composer
:
index.php
<?php
include('login.php');
if(isset($_SESSION['login_user'])){
header("location: profile.php");
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Acceso a aplicación</title>
<link href="style.css" rel="stylesheet" type="text/css">
</head>
<body>
<div id="main">
<h1>Login</h1>
<div id="login">
<h2>Login Form</h2>
<form action="" method="post">
<label>UserName :</label>
<input id="name" name="username" placeholder="username" type="text">
<label>Password :</label>
<input id="password" name="password" placeholder="**********" type="password">
<input name="submit" type="submit" value=" Login ">
<span><?php echo $error; ?></span>
</form>
</div>
</div>
</body>
</html>
login.php
<?php
// libreria mongodb
require 'vendor/autoload.php';
session_start();
$error='';
//si es un POST
if (isset($_POST['submit'])) {
if (empty($_POST['username']) || empty($_POST['password'])) {
$error = "Usuario o password incorrecto";
} else {
// obtengo datos del post
$username=$_POST['username'];
$password=$_POST['password'];
// conecto a mongodb
$client = new MongoDB\Client(
"mongodb://localhost",
[ 'username' => "usuariodb",
'password' => "12345",
'authSource' => "miappdb"
]
);
// busco usuario con login y pass
$collection = $client->miappdb->usuarios;
$usuario = $collection->findOne(
[ 'login' => $username,
'password' => $password
]
);
// si encuentra usuario con ese user+pass
if($usuario) {
$_SESSION['login_user'] = $usuario['login'];
header("location: profile.php");
exit();
} else {
$error = "Username or Password is invalid";
}
}
}
?>
session.php
<?php
// librería mongodb
require 'vendor/autoload.php';
// chequeo la sesión
session_start();
if(!isset($_SESSION['login_user'])) {
header('Location: index.php');
exit;
}
$user_check=$_SESSION['login_user'];
// conecto a mongodb
$client = new MongoDB\Client(
"mongodb://localhost",
[ 'username' => "usuariodb",
'password' => "12345",
'authSource' => "miappdb"
]
);
//busco el usuario
$collection = $client->miappdb->usuarios;
$usuario = $collection->findOne(
[ 'login' => $user_check ]
);
// obtengo los datos
if($usuario) {
$login_session = $user_check;
}
if(!isset($login_session)){
header('Location: index.php');
exit();
}
?>
logout.php
<?php
session_start();
if(session_destroy()) {
header("Location: index.php");
}
?>
profile.php
<?php
include('session.php');
?>
<!DOCTYPE html>
<html>
<head>
<title>Dentro de aplicación</title>
<link href="style.css" rel="stylesheet" type="text/css">
</head>
<body>
<div id="profile">
<b id="welcome">Bienvenido : <i><?php echo $login_session; ?></i></b>
<b id="logout"><a href="logout.php">Log Out</a></b>
</div>
</body>
</html>
Si navegamos a la aplicación con el navegador y nos validamos veremos que funciona correctamente.
3. Explotando el php array injection
Bien, ya tenemos el escenario de la prueba de conepto preparado, ahora vamos a proceder a realizar la explotación. Para ello vamos a usar las aplicaciones OWASP ZAP y Firefox. Ya existen numerosos tutoriales sobre cómo configurar OWASP ZAP, así que no me voy a repetir.
La inyección que buscamos es sustituir la búsqueda de login
y password
que
se realiza en el fichero login.php
por una expresión que nos saque datos de
la colección. Para ello nos aprovechamos de una funcionalidad muy útil que
provee PHP para procesar formularios que hace que los datos que vienen por
POST
en la forma formulario[campo]=valor
, sean convertidos automáticamente
en un array asociativo llamado formulario
con la clave campo
y el valor
definido.
De forma general, la consulta:
$usuario = $collection->findOne(
[ 'login' => $username,
'password' => $password
]
);
devolverá el primer registro en el cual (login == username) and (password ==
password)
. Pero MongoDB admite expresiones como login $ne 1
donde $ne
es
un operador "no igual a". De este modo si convertimos la consulta en (login
$ne 1) and (password $ne 1)
, se devolverá el primer registro que cumpla esa
condición (que serán todos). Y esto es posible convirtiendo la variable
$username
en un array '$ne' ⇒ '1'
mediante el array injection.
Vamos a usarlo directamente en Firefox con Zap activo.

Pero vemos en Zap que firefox lo envía de la siguiente forma, que no es como espera PHP que sea para convertir los datos en un array:

Vamos a modificar la solicitud POST y reenviarla con Zap, e importante, vamos a desactivar que Zap siga redirección para ver qué nos devuelve en el cuadrito de la flecha verde de la derecha.

Observamos la respuesta de Zap que es una redirección hacia… profile.php
.
Esto se debe a que hemos cambiado la consulta que realiza la aplicación PHP a
mongo, diciendo que el campo login no sea igual a 1 y el campo password tampoco
lo sea. Como todos los registros cumplen esta condición, mongo los devuelve y
se selecciona el primero que encuentra de ellos, que en este caso es el del
usuario juan
.

Y ahora que la cookie que identifica la sesión es la misma que con la que nos
hemos autenticado con Zap, accedemos con el navegador directamente a
profile.php
y estamos dentro.

4. Solución
Ya se habla en la
documentación de PHP
sobre este problema, y nos recomienda el uso de la función filter_var()
.
En esta otra página: http://blog.securelayer7.net/mongodb-security-injection-attacks-with-php/
nos recomiendan una solución más sencilla mediante la función implode
en la
que extraeremos el primer elemento en caso de que el usuario tratase de pasar
un array.
Simplemente hay que modificar en login.php
login.php
$username=implode($_POST['username']);
$password=implode($_POST['password']);
Tras esta corrección, la manipulación ya no funciona…

implode
Espero que te haya gustado y si es así comparte a quien creas que le puede interesar, ¡¡happy hacking!!.