Explotando un PHP Array Injection en MongoDB

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:

Creación de base de datos miappdb y usuario
luisadm@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:

Inserción 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:

Contenido 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>
Contenido 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";
		}
	}
}
?>
Contenido 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();
}
?>
Contenido logout.php
<?php

session_start();
if(session_destroy()) {
	header("Location: index.php");
}

?>
Contenido 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.

Login mongo
Figura 1. Usando una expresión mongo en el login

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:

Petición zap
Figura 2. Envío post realizado

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.

Petición zap reenvio
Figura 3. Reenvío desde zap

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.

Petición respuesta
Figura 4. Respuesta en zap

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.

Acceso desde firefox
Figura 5. Acceso a la aplicación

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

Parcheando login.php
     $username=implode($_POST['username']);
     $password=implode($_POST['password']);

Tras esta corrección, la manipulación ya no funciona…​

Respuesta tras implode
Figura 6. Intento de autenticación tras parche con implode

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