|
| 1 | +Overview |
| 2 | +======== |
| 3 | + |
| 4 | +libgd [1] is an open-source image library. It is perhaps primarily used |
| 5 | +by the PHP project. It has been bundled with the default installation |
| 6 | +of PHP since version 4.3 [2]. |
| 7 | + |
| 8 | +A signedness vulnerability (CVE-2016-3074) exist in libgd 2.1.1 which |
| 9 | +may result in a heap overflow when processing compressed gd2 data. |
| 10 | + |
| 11 | + |
| 12 | +Details |
| 13 | +======= |
| 14 | + |
| 15 | +4 bytes representing the chunk index size is stored in a signed integer, |
| 16 | +chunkIdx[i].size, by `gdGetInt()' during the parsing of GD2 headers: |
| 17 | + |
| 18 | +libgd-2.1.1/src/gd_gd2.c: |
| 19 | +,---- |
| 20 | +| 53 typedef struct { |
| 21 | +| 54 int offset; |
| 22 | +| 55 int size; |
| 23 | +| 56 } |
| 24 | +| 57 t_chunk_info; |
| 25 | +`---- |
| 26 | + |
| 27 | + |
| 28 | +libgd-2.1.1/src/gd_gd2.c: |
| 29 | +,---- |
| 30 | +| 65 static int |
| 31 | +| 66 _gd2GetHeader (gdIOCtxPtr in, int *sx, int *sy, |
| 32 | +| 67 int *cs, int *vers, int *fmt, int *ncx, int *ncy, |
| 33 | +| 68 t_chunk_info ** chunkIdx) |
| 34 | +| 69 { |
| 35 | +| ... |
| 36 | +| 73 t_chunk_info *cidx; |
| 37 | +| ... |
| 38 | +| 155 if (gd2_compressed (*fmt)) { |
| 39 | +| ... |
| 40 | +| 163 for (i = 0; i < nc; i++) { |
| 41 | +| ... |
| 42 | +| 167 if (gdGetInt (&cidx[i].size, in) != 1) { |
| 43 | +| 168 goto fail2; |
| 44 | +| 169 }; |
| 45 | +| 170 }; |
| 46 | +| 171 *chunkIdx = cidx; |
| 47 | +| 172 }; |
| 48 | +| ... |
| 49 | +| 181 } |
| 50 | +`---- |
| 51 | + |
| 52 | + |
| 53 | +`gdImageCreateFromGd2Ctx()' and `gdImageCreateFromGd2PartCtx()' then |
| 54 | +allocates memory for the compressed data based on the value of the |
| 55 | +largest chunk size: |
| 56 | + |
| 57 | +libgd-2.1.1/src/gd_gd2.c: |
| 58 | +,---- |
| 59 | +| 371|637 if (gd2_compressed (fmt)) { |
| 60 | +| 372|638 /* Find the maximum compressed chunk size. */ |
| 61 | +| 373|639 compMax = 0; |
| 62 | +| 374|640 for (i = 0; (i < nc); i++) { |
| 63 | +| 375|641 if (chunkIdx[i].size > compMax) { |
| 64 | +| 376|642 compMax = chunkIdx[i].size; |
| 65 | +| 377|643 }; |
| 66 | +| 378|644 }; |
| 67 | +| 379|645 compMax++; |
| 68 | +| ...|... |
| 69 | +| 387|656 compBuf = gdCalloc (compMax, 1); |
| 70 | +| ...|... |
| 71 | +| 393|661 }; |
| 72 | +`---- |
| 73 | + |
| 74 | + |
| 75 | +A size of <= 0 results in `compMax' retaining its initial value during |
| 76 | +the loop, followed by it being incremented to 1. Since `compMax' is |
| 77 | +used as the nmemb for `gdCalloc()', this leads to a 1*1 byte allocation |
| 78 | +for `compBuf'. |
| 79 | + |
| 80 | +This is followed by compressed data being read to `compBuf' based on the |
| 81 | +current (potentially negative) chunk size: |
| 82 | + |
| 83 | +libgd-2.1.1/src/gd_gd2.c: |
| 84 | +,---- |
| 85 | +| 339 BGD_DECLARE(gdImagePtr) gdImageCreateFromGd2Ctx (gdIOCtxPtr in) |
| 86 | +| 340 { |
| 87 | +| ... |
| 88 | +| 413 if (gd2_compressed (fmt)) { |
| 89 | +| 414 |
| 90 | +| 415 chunkLen = chunkMax; |
| 91 | +| 416 |
| 92 | +| 417 if (!_gd2ReadChunk (chunkIdx[chunkNum].offset, |
| 93 | +| 418 compBuf, |
| 94 | +| 419 chunkIdx[chunkNum].size, |
| 95 | +| 420 (char *) chunkBuf, &chunkLen, in)) { |
| 96 | +| 421 GD2_DBG (printf ("Error reading comproessed chunk\n")); |
| 97 | +| 422 goto fail; |
| 98 | +| 423 }; |
| 99 | +| 424 |
| 100 | +| 425 chunkPos = 0; |
| 101 | +| 426 }; |
| 102 | +| ... |
| 103 | +| 501 } |
| 104 | +`---- |
| 105 | + |
| 106 | + |
| 107 | +libgd-2.1.1/src/gd_gd2.c: |
| 108 | +,---- |
| 109 | +| 585 BGD_DECLARE(gdImagePtr) gdImageCreateFromGd2PartCtx (gdIOCtx * in, int srcx, int srcy, int w, int h) |
| 110 | +| 586 { |
| 111 | +| ... |
| 112 | +| 713 if (!gd2_compressed (fmt)) { |
| 113 | +| ... |
| 114 | +| 731 } else { |
| 115 | +| 732 chunkNum = cx + cy * ncx; |
| 116 | +| 733 |
| 117 | +| 734 chunkLen = chunkMax; |
| 118 | +| 735 if (!_gd2ReadChunk (chunkIdx[chunkNum].offset, |
| 119 | +| 736 compBuf, |
| 120 | +| 737 chunkIdx[chunkNum].size, |
| 121 | +| 738 (char *) chunkBuf, &chunkLen, in)) { |
| 122 | +| 739 printf ("Error reading comproessed chunk\n"); |
| 123 | +| 740 goto fail2; |
| 124 | +| 741 }; |
| 125 | +| ... |
| 126 | +| 746 }; |
| 127 | +| ... |
| 128 | +| 815 } |
| 129 | +`---- |
| 130 | + |
| 131 | + |
| 132 | +The size is subsequently interpreted as a size_t by `fread()' or |
| 133 | +`memcpy()', depending on how the image is read: |
| 134 | + |
| 135 | +libgd-2.1.1/src/gd_gd2.c: |
| 136 | +,---- |
| 137 | +| 221 static int |
| 138 | +| 222 _gd2ReadChunk (int offset, char *compBuf, int compSize, char *chunkBuf, |
| 139 | +| 223 uLongf * chunkLen, gdIOCtx * in) |
| 140 | +| 224 { |
| 141 | +| ... |
| 142 | +| 236 if (gdGetBuf (compBuf, compSize, in) != compSize) { |
| 143 | +| 237 return FALSE; |
| 144 | +| 238 }; |
| 145 | +| ... |
| 146 | +| 251 } |
| 147 | +`---- |
| 148 | + |
| 149 | +libgd-2.1.1/src/gd_io.c: |
| 150 | +,---- |
| 151 | +| 211 int gdGetBuf(void *buf, int size, gdIOCtx *ctx) |
| 152 | +| 212 { |
| 153 | +| 213 return (ctx->getBuf)(ctx, buf, size); |
| 154 | +| 214 } |
| 155 | +`---- |
| 156 | + |
| 157 | + |
| 158 | +For file contexts: |
| 159 | + |
| 160 | +libgd-2.1.1/src/gd_io_file.c: |
| 161 | +,---- |
| 162 | +| 52 BGD_DECLARE(gdIOCtx *) gdNewFileCtx(FILE *f) |
| 163 | +| 53 { |
| 164 | +| ... |
| 165 | +| 67 ctx->ctx.getBuf = fileGetbuf; |
| 166 | +| ... |
| 167 | +| 76 } |
| 168 | +| ... |
| 169 | +| 92 static int fileGetbuf(gdIOCtx *ctx, void *buf, int size) |
| 170 | +| 93 { |
| 171 | +| 94 fileIOCtx *fctx; |
| 172 | +| 95 fctx = (fileIOCtx *)ctx; |
| 173 | +| 96 |
| 174 | +| 97 return (fread(buf, 1, size, fctx->f)); |
| 175 | +| 98 } |
| 176 | +`---- |
| 177 | + |
| 178 | + |
| 179 | +And for dynamic contexts: |
| 180 | + |
| 181 | +libgd-2.1.1/src/gd_io_dp.c: |
| 182 | +,---- |
| 183 | +| 74 BGD_DECLARE(gdIOCtx *) gdNewDynamicCtxEx(int initialSize, void *data, int freeOKFlag) |
| 184 | +| 75 { |
| 185 | +| ... |
| 186 | +| 95 ctx->ctx.getBuf = dynamicGetbuf; |
| 187 | +| ... |
| 188 | +| 104 } |
| 189 | +| ... |
| 190 | +| 256 static int dynamicGetbuf(gdIOCtxPtr ctx, void *buf, int len) |
| 191 | +| 257 { |
| 192 | +| ... |
| 193 | +| 280 memcpy(buf, (void *) ((char *)dp->data + dp->pos), rlen); |
| 194 | +| ... |
| 195 | +| 284 } |
| 196 | +`---- |
| 197 | + |
| 198 | + |
| 199 | +PoC |
| 200 | +=== |
| 201 | + |
| 202 | +Against Ubuntu 15.10 amd64 running nginx with php5-fpm and php5-gd [3]: |
| 203 | + |
| 204 | +,---- |
| 205 | +| $ python exploit.py --bind-port 5555 http://1.2.3.4/upload.php |
| 206 | +| [*] this may take a while |
| 207 | +| [*] offset 912 of 10000... |
| 208 | +| [+] connected to 1.2.3.4:5555 |
| 209 | +| id |
| 210 | +| uid=33(www-data) gid=33(www-data) groups=33(www-data) |
| 211 | +| |
| 212 | +| uname -a |
| 213 | +| Linux wily64 4.2.0-35-generic #40-Ubuntu SMP Tue Mar 15 22:15:45 UTC |
| 214 | +| 2016 x86_64 x86_64 x86_64 GNU/Linux |
| 215 | +| |
| 216 | +| dpkg -l|grep -E "php5-(fpm|gd)" |
| 217 | +| ii php5-fpm 5.6.11+dfsg-1ubuntu3.1 ... |
| 218 | +| ii php5-gd 5.6.11+dfsg-1ubuntu3.1 ... |
| 219 | +| |
| 220 | +| cat upload.php |
| 221 | +| <?php |
| 222 | +| imagecreatefromgd2($_FILES["file"]["tmp_name"]); |
| 223 | +| ?> |
| 224 | +`---- |
| 225 | + |
| 226 | + |
| 227 | +Solution |
| 228 | +======== |
| 229 | + |
| 230 | +This bug has been fixed in git HEAD [4]. |
| 231 | + |
| 232 | +Full Proof of Concept: |
| 233 | +https://github.com/offensive-security/exploit-database-bin-sploits/raw/master/sploits/39736.zip |
| 234 | + |
| 235 | +Footnotes |
| 236 | +_________ |
| 237 | + |
| 238 | +[1] [http://libgd.org/] |
| 239 | +[2] [https://en.wikipedia.org/wiki/Libgd] |
| 240 | +[3] [https://github.com/dyntopia/exploits/tree/master/CVE-2016-3074] |
| 241 | +[4] [https://github.com/libgd/libgd/commit/2bb97f407c1145c850416a3bfbcc8cf124e68a19] |
0 commit comments