Posted in

No todo es «select * from» – NoSQL Injection (Parte 2) – Práctica

En esta serie de entradas analizaremos en detalle el ataque de inyección NoSQL, empleando diferentes técnicas que nos permitirán comprometer la información contenida en una base de datos no-relacional.

En esta segunda entrada, ejecutaremos algunas técnicas de ataque sobre entornos preparados, a fin de demostrar como funciona un ataque de inyección y como podemos aprovecharlo en nuestros ejercicios de pentesting.

Advertencia: La información y las técnicas compartidas en este post son sólo con fines educativos o para realizar pruebas debidamente autorizadas. Atacar aplicaciones sin consentimiento, puede ser considerado ilegal. Utilice este conocimiento bajo su propio riesgo.

Vulnerable Node App

En esta aplicación veremos dos tipos de ataque:

  • Inyección de sintaxis, para obtener la data completa de la colección de usuarios.
  • Inyección de parámetros, para evadir el control de autenticación.

Realicemos nuestro primer ejercicio de inyección por sintaxis:

  • Dentro de la página principal, seleccionaremos la opción «Pupulate/ResetDB», para asegurarnos que se creen los usuarios de la BD
  • Luego, nos dirigiremos a la opción «User Lookup – see if you can get all users»
  • Dentro de la página de búsqueda, colocaremos un nombre común: admin, para ver si nos devuelve resultados y cual es la interacción:
  • Como vemos, es un buscador simple, en donde el término de búsqueda se envía a través del método GET (en la URL). Veamos el detalle de la petición a través de Burp Suite:
  • Podemos inferir que la estructura de la consulta que procesa MongoDB es
{"$where":"this.username == 'admin'"}
  • Para verificar si nuestro parámetro username es inyectable, podemos utilizar caracteres especiales como o ;. Utilizaremos la siguiente cadena: ‘»`{
  • Como vemos, la aplicación nos devuelve un error, lo cual podría significar que el input no está siendo sanitizado.
  • Pensemos con un poco de lógica para construir una estructura de búsqueda que devuelva un valor verdadero en caso se cumpla la condición, y nos entregue todos los usuarios de la base:
{"$where":"this.username == 'admin' || '1'=='1'"}
  • Estamos inyectando el siguiente payload: admin’ || ‘1’==’1. En este caso, independientemente de que la primera condición se cumpla, la segunda siempre se cumplirá, por lo que en el caso de que admin sea correcto tendremos V OR V = V, y si no existe F OR V = V.
  • Inyectamos este payload en el cuadro de búsqueda, y podremos obtener toda la lista de usuarios:
  • Podemos inyectar este payload también a través de Burp Suite, teniendo en cuenta que debe estar encodeado como URL. Podemos hacer esto a través de Ctrl+U con el valor seleccionado o utilizando el módulo de Encoder.

Ahora realicemos nuestro ejercicio de inyección por operador:

  • Antes de realizar la inyección, podemos ver los logs que ha generado nuestro contenedor a partir del primer ataque. Podemos ver que nos devuelve la estructura completa de usuarios:
  • Utilizaremos la contraseña de admin para demostrar luego que nuestra inyección es válida. Si colocamos una contraseña incorrecta, la respuesta es la siguiente:
  • Ahora veamos la respuesta con la contraseña correcta
  • Aunque el paso anterior no era necesario, nos servirá para demostrar que hemos efectuado una inyección correcta. Llevamos nuestra petición al módulo Repeater en Burp Suite:
  • Ahora, buscaremos que la lógica de la consulta sea la siguiente: «Inicia sesión, si el usuario es admin, y la contraseña es diferente a 1»:
{"username":"admin","password":{"$ne":1}}
  • Inyectaremos esto en el cuerpo de la petición, ya que este se envía a través del método POST:
  • Y habremos conseguido evadir la autenticación:

NoSQL Injection Vuln App (NIVA)

Esta aplicación tiene un único vector de ataque, cuyo objetivo es traer la lista de todos los usuarios de la BD, sin embargo, dentro de la documentación, nos explica como es la implementación segura e insegura de diferentes métodos de Java para traer información de MongoDB.

Para poder realizar el ejercicio, podemos autenticarnos con las credenciales user1/pass1. Nos solicitará autenticarnos cuando demos click en alguno de los enlaces «Request»

Ahora sí, veamos un par de casos. Si quieres ver el detalle de todos los métodos, puedes consultar la documentación directamente en el repositorio: https://github.com/aabashkin/nosql-injection-vulnapp

  • El primer método es BasicDBObject::put, el cual cuando es incorrectamente implementado, permite la creación de una consulta usando el operador $where junto con los valores de los parámetros concatenados:
BasicDBObject query = new BasicDBObject();
query.put("$where", "this.sharedWith == \"" + userName + "\" && this.email == \"" + email + "\"");
MongoCursor<Document> cursor = collection.find(query).iterator();
  • Veamos una petición normal:
  • Como buena práctica, inyectaremos nuestro payload intencional para generar error: ‘»`{
  • Vemos como se ha generado el error, por lo que el parámetro es potencialmente inyectable. Ahora, ingresaremos el siguiente payload:
" || "0" != "1
  • Recuerda encodearlo si estás usando Burp Suite:
  • La respuesta nos entrega toda la lista de usuarios registrados:
  • Una correcta implementación del método es la siguiente:
BasicDBObject query = new BasicDBObject();
query.put("sharedWith", userName);
query.put("email", email);
MongoCursor<Document> cursor = collection.find(query).iterator();
  • Veamos ahora el método BasicDBObjectBuilder::start(Map), el cual crea un generador de objetos a partir de un mapa existente de pares clave-valor. Como input recibe un documento en formato de mapa. Cuando es mal implementado, de igual forma, permite la concatenación de los valores en el mapa:
HashMap<String, String> paramMap = new HashMap<>();
        paramMap.put("$where", "this.sharedWith == \"" + userName + "\" && this.email == \"" + email + "\"");
        BasicDBObject query = (BasicDBObject) BasicDBObjectBuilder
                .start(paramMap)
                .get();

MongoCursor<Document> cursor = collection.find(query).iterator();
  • Con una consulta normal, nos devuelve los siguiente:
  • Inyectamos el parámetro con nuestro mismo payload:
  • Obteniendo toda la lista de usuarios:

Portswigger Labs – NoSQL Injection

Ahora que ya hemos hecho ejercicios básicos, realizaremos dos más dentro del sitio: https://portswigger.net/web-security/nosql-injection. Recuerda crear tu cuenta de usuario, ya que los laboratorios que veremos acá son gratuitos.

Inyección de sintaxis para extracción de datos

En este primer ejercicio, nuestro objetivo es obtener la clave del usuario administrador, y poder autenticarnos con ella. Vayamos paso a paso:

  • Tras autenticarnos, veremos la página donde se muestra nuestra cuenta:
  • Revisando nuestra captura de tráfico en Burp Suite, podemos ver que como parte de las peticiones se encuentra la siguiente: /user/lookup?user=wiener
  • Nuevamente, validaremos si el parámetro user es inyectable, usando la siguiente cadena como valor: ‘»`{
  • Como vemos, el parámetro es potencialmente inyectable. Utilizaremos lo aprendido para inyectar el siguiente payload:
wiener'||'1'=='1
  • La petición nos ha devuelto los datos del usuario administrador. Además de comprobar que el parámetro es inyectable, que el nombre del usuario es administrator.
  • Ahora, lo que haremos es averiguar la lógica detrás de las respuestas. Para ello utilizaremos una condición lógica que nos permita tener una respuesta falsa y otra verdadera. Recordemos que en la petición anterior, siempre nos dará verdadero ya que hemos usado el condicional OR (||), por lo tanto utilizaremos el condicional AND (&&), de tal manera que nuestros payloads estén inyectados en consultas así:
{"$where":"this.username == 'wiener'&&'1'=='1'"}
{"$where":"this.username == 'wiener'&&'1'=='2'"}
  • Nuestro primer payload: wiener’&&’1’==’1, nos devolverá la respuesta del servidor en los casos que el resultado sea verdadero, recordemos que el usuario wiener existe y 1 es igual a 1 , por lo que V AND V = V
  • Nuestro segundo payload: wiener’&&’1’==’2, nos devolverá la respuesta del servidor en los casos que el resultado sea falso, recordemos que el usuario wiener existe y 1 no es igual a 2, por lo que V AND F = F
  • Utilicemos el primer primer payload:
  • Entonces, cuando el resultado de la consulta es verdadero, nos devuelve los datos del usuario. Ahora inyectemos nuestro segundo payload:
  • Cuando la consulta es falsa, nos devuelve el mensaje «Could not find user».
  • Ya que tenemos ambas respuestas, utilizaremos otra consulta para poder detectar cual es la longitud de la contraseña:
{"$where":"this.username == 'administrator' && this.password.length < 30 || 'a'=='b'}
  • Creo que ya notaste cual es el payload y qué es lo que hace: busca saber si la longitud de la contraseña del usuario administrator es menor a 30. Si lo es, la respuesta debería ser los datos del usuario (condición verdadera)
  • Hemos comprobado que la longitud es menor a 30, pero necesitamos saber con exactitud cuantos caracteres tiene, para luego poder extraerla caracter por caracter. Ahora repetiremos la consulta para saber si es menor a 5 caracteres:
  • Bien, la respuesta corresponde a una condición falsa, por lo que inferimos que la contraseña es mayor a 5. Si seguimos repitiendo las peticiones, validaremos que la contraseña tiene 8 caracteres.
  • Ahora, modificaremos nuestro payload con el objetivo de extraer los caracteres de la contraseña, según cada posición, para eso, nuestra consulta debe quedar así:
{"$where":"this.username == 'administrator' && this.password[posicion]=='caracter'}
  • Hacerlo de forma manual no es una opción, así que utilizaremos la opción Intruder de Burp Suite, en donde la primera iteración serán los valores del 0 al 7 y la segunda, las letras del alfabeto. Este será nuestro valor en el parámetro user
administrator'%26%26%20this.password[§0§]%3D%3d'§b§

  • Ejecutamos nuestro ataque y veremos, a través de la longitud de la respuesta, cuando el valor devuelto es verdadero, incluyendo su posición:
  • Vemos que el sétimo caracter de la contraseña es ‘b’. Al final del ataque, tendremos la contraseña completa, la cual es tqichwbp (esta contraseña varía, así que debes realizar tu propio ataque):
  • La validamos accediendo a la página con el usuario administrator:
  • Como ya te habrás dado cuenta, una mala práctica que también existe acá es que el password se almacena en texto plano, lo cual no suele pasar en un entorno real, ¿o sí?. De cualquier forma, nos sirve para probar el concepto.

Inyección de parámetros para extracción de campos

En este segundo ejercicio, nuestro objetivo es resetear la clave del usuario carlos, y poder autenticarnos posteriormente. Vayamos paso a paso:

  • Probaremos un intento de evasión de la autenticación utilizando la inyección de parametro $ne, tal como lo hicimos en el laboratorio Vulnerable Node App
  • En este caso no lograremos evadir la autenticación, pero podemos ver que la respuesta es diferente, por lo que podemos inferir que el parámetro es potencialmente inyectable:
  • Lo que haremos ahora, es intentar resetear la contraseña de carlos desde la página /forgot-password
  • La respuesta no nos lleva a ningún lado, ya que nos devuelve el mensaje «Please check your email for a reset password link».
  • Es común ver en las aplicaciones que tienen este tipo de mecanismo de reseteo de contraseñas, que al enviar el enlace al correo, utilizan el patron:
https://midominio.com/resetear?key=token
  • Entonces, si seguimos esta lógica, al ejecutar la petición de reseteo, se debió generar un token, el cual no conocemos ahora, pero podemos obtenerlo utilizando una técnica similar a la vista en el ejercicio anterior. Lo mismo necesitaremos hacer para obtener el nombre del parámetro, ya que podría ser «key», o cualquier otro valor. Dicho parámetro debe existir como campo dentro de la colección usuario, lo que necesitamos es saber exactamente como se llama.
  • Para poder obtenerlo, a nuestra consulta anterior, le añadiremos el parámetro $where a la estructura:
{"username":"wiener","password":"peter", "$where":"0"}
  • Cambiamos el valor a 1:
  • Bien, acabamos de comprobar que las respuestas son diferentes, por lo que nuestra aplicación está interpretando correctamente el valor expresado en el parámetro where. Algo importante que debemos saber es que hemos verificado que el servidor está evaluando las expresiones Javascript ingresadas en el valor del parámetro where, a este tipo de ataque se le conoce como SSJI (Server Side Javascript Injection. Si quieres saber más puedes consultar la siguiente fuente: https://secops.group/a-deep-dive-into-server-side-javascript-injection-ssji-vulnerabilities/
  • Modificaremos nuestro payload para obtener los nombres de campo usando el método keys() de Javascript:
{"username":"carlos","password":{"$ne":"error"}, "$where":"Object.keys(this)[0].match('^.{0}a.*')"}
  • Esto inspecciona el primer campo de datos del objeto usuario y devuelve el primer carácter del nombre del campo. Si el valor de la primera letra del nombre del campo es ‘a’, la respuesta será verdadera y arrojará el mensaje «Account Locked…». Esto permite extraer el nombre del campo carácter por carácter.
  • Como en el ejercicio anterior, llevaremos la petición al Intruder, y configuraremos un cluster bomb de la siguiente manera:
  • En la primera posición agregaremos las posiciones, en una lista continua de números con una longitud considerable, por ejemplo del 0 al 20:
  • En la segunda posición, agregaremos los caracteres alfanúmericos [a-z][A-Z][0-9]
  • Al lanzar nuestro ataque, veremos la respuesta diferenciada en la longitud:
  • Esto quiere decir que en la posición 2 del nombre del campo, la letra es ‘d’. Al final del primer ataque, tendremos que el campo es «id»:
  • Repetimos nuestro ataque de Cluster Bomb, pero esta vez con el campo 1
{"username":"carlos","password":{"$ne":"error"}, "$where":"Object.keys(this)[0].match('^.{0}a.*')"}
  • El segundo campo es username.:
  • Si seguimos ejecutando el ataque veremos que los cuatro primeros campos son son: id, username, password, email. Si probamos con el quinto campo, tenemos como resultado que el nombre es unlockToken (quizá esto puede variar en tu propio entorno):
  • Ahora que sabemos el nombre del campo, tenemos que hallar su valor, por lo que modificaremos nuestro payload en el Intruder, de la siguiente manera:
{"username":"carlos","password":{"$ne":"error"},"$where":"this.unlockToken.match('^.{0}a.*')"}
  • Aquí tenemos que ser pacientes, aunque como alternativa, puedes abrir múltiples pestañas de Intruder, o desarrollar tu propio código en el lenguaje que desees para automatizar las peticiones. En nuestro caso ejemplo, el token es 3e72b7d96c9fe656.
  • Utilizaremos el token, armando la siguiente petición, y nos llevará a la página para cambiar la contraseña del usuario carlos:
https://[id_único].web-security-academy.net/forgot-password?unlockToken=3e72b7d96c9fe656
  • La modificamos, y luego la usamos para conectarnos:
  • Y hemos resuelto el laboratorio:

Bien, ahora que ya hemos puesto en práctica lo aprendido, te dejamos algunas recomendaciones si lo que quieres es evitar tener esta vulnerabilidad en tus aplicaciones:

  • Utiliza herramientas de escaneo estático de código, incluyendo el escaneo de librerías de terceros, como parte de tu SDLC.
  • Usa sentencias preparadas. Al igual que en SQL, esto permitirá tener los datos separados de las instrucciones, comandos o consultas, lo que reduce la posibilidad de inyección.
  • Sanitiza todas las entradas de usuario. TODAS!. Incluye además filtros dependiendo del dato que recibirás.
  • Cuando trabajes con MongoDB, evita (en la medida de lo posible) integrar directamente los operadores where, mapReduce o group con datos proporcionados por los usuarios. Además, es recomendable deshabilitar la ejecución de JavaScript configurando javascriptEnabled en false dentro del archivo mongod.conf, cuando sea víable.
  • Realiza pruebas de penetración sobre tu aplicación cuando esta se encuentre disponible antes de liberarla a producción.
  • Mantén siempre las versiones de tus librerías y componentes actualizadas, siempre evaluando la factibilidad.

Y eso ha sido todo en esta serie de entradas. Esperamos que te haya sido de utilidad

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *