PHP :: Sec Bug #71912 :: libgd: signedness vulnerability
| Sec Bug #71912 | libgd: signedness vulnerability | ||||
|---|---|---|---|---|---|
| Submitted: | 2016-03-29 03:41 UTC | Modified: | 2016-04-27 05:55 UTC | ||
| From: | hji at dyntopia dot com | Assigned: | stas (profile) | ||
| Status: | Closed | Package: | GD related | ||
| PHP Version: | 5.5.34 | OS: | |||
| Private report: | No | CVE-ID: | 2016-3074 | ||
[2016-03-29 03:41 UTC] hji at dyntopia dot com
Description:
------------
A signedness vulnerability (CVE-2016-3074) exist in libgd 2.1.1 which
may result in a heap overflow when processing compressed gd2 data.
The libgd project has been informed; however, since it's bundled with
PHP, you might want to know about this too.
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 }
`----
Test script:
---------------
CVE-2016-3074.py: https://gist.github.com/dyntopia/bc4eaa90c377abfcc947
upload.php: https://gist.github.com/dyntopia/8054b37b4f1ab9e4fcd2
Actual result:
--------------
,----
| $ python CVE-2016-3074.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-34-generic #39-Ubuntu SMP Thu Mar 10 22:13:01 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
Patches
Pull Requests
History
AllCommentsChangesGit/SVN commits
[2016-04-18 00:32 UTC] stas@php.net
-Summary: CVE-2016-3074: libgd: signedness vulnerability +Summary: libgd: signedness vulnerability -CVE-ID: +CVE-ID: 2016-3074
[2016-04-19 05:17 UTC] stas@php.net
-Assigned To: +Assigned To: pajoye
[2016-04-19 05:27 UTC] stas@php.net
-PHP Version: 7.0.4 +PHP Version: 5.5.34 -Assigned To: pajoye +Assigned To: stas
[2016-04-19 05:27 UTC] stas@php.net
[2016-04-27 05:55 UTC] stas@php.net
-Status: Assigned +Status: Closed
[2016-04-27 05:55 UTC] stas@php.net