php+cve+2018+5711,最近GD库拒绝服务漏洞分析与EXP构造(CVE-2018-5711)

66b39cefea89699334f2cf28c29bb452.gif

0x00 前言

最近爆出PHP GD库拒绝服务

0x01

There is a do-while in file `ext/gd/libgd/gd_gif_in.c` and function `LWZReadByte_`

do{

sd->firstcode = sd->oldcode =

GetCode(fd, &sd->scd, sd->code_size, FALSE, ZeroDataBlockP);

} while (sd->firstcode == sd->clear_code);

https://github.com/php/php-src/blob/c5767db441e4db2a1e07b5880129ad7ce0b25b6f/ext/gd/libgd/gd_gif_in.c#L460

The implementation of `GetCode` is in `GetCode_`

static int

GetCode_(gdIOCtx *fd, CODE_STATIC_DATA *scd, int code_size, int flag, int *ZeroDataBlockP)

{

int i, j, ret;

unsigned char count;

...

if ((count = GetDataBlock(fd, &scd->buf[2], ZeroDataBlockP)) <= 0)

scd->done = TRUE;

...

}

https://github.com/php/php-src/blob/c5767db441e4db2a1e07b5880129ad7ce0b25b6f/ext/gd/libgd/gd_gif_in.c#L376

As you can see, `GetDataBlock` will read the image data and return the length. If EOF, returned -1.

But the variable `count` is `unsigned char`, will always be positive value.

So the line `scd->done = TRUE` will never be executed.

根据官方的报告,LWZReadByte_ 这个函数会造成死循环,原因是由于count变量是unsigne char,永远不会是负数,从而无法判断图片是否读取完毕,造成scd->done = TRUE无法执行,一开始没有想到这个报告很懒,还疑问那岂不是所有的GIF图片都会造成拒绝服务了(还真去拿普通的GIF图片试了试)。

其实还要满足sd->firstcode == sd->clear_code才能造成死循环。

do {

sd->firstcode = sd->oldcode =

GetCode(fd, &sd->scd, sd->code_size, FALSE, ZeroDataBlockP);

} while (sd->firstcode == sd->clear_code);

那为什么报告中要指出scd->done = TRUE无法执行。看这个函数上面,发现有一个if的判断,如果scd->done为True,则会直接返回-1。那么sd->firstcode == sd->clear_code永远不会成立了,造成循环退出。所以scd->done一定不能为True。

gd_gif_in.c#L389

if ( (scd->curbit + code_size) >= scd->lastbit) {

if (scd->done) {

if (scd->curbit >= scd->lastbit) {

/* Oh well */

}

return -1;

}

scd->buf[0] = scd->buf[scd->last_byte-2];

scd->buf[1] = scd->buf[scd->last_byte-1];

if ((count = GetDataBlock(fd, &scd->buf[2], ZeroDataBlockP)) <= 0)

scd->done = TRUE;

scd->last_byte = 2 + count;

scd->curbit = (scd->curbit - scd->lastbit) + 16;

scd->lastbit = (2+count)*8 ;

}

上面仅仅是为了满足不返回-1,但是还要满足返回结果等于sd->clear_code。接下来的ret结果由下面的代码控制。通过构造GIF,可以控制ret的返回结果。而sd->clear_code也是可以控制。从而达到死循环。

gd_gif_in.c#L407

if ((scd->curbit + code_size - 1) >= (CSD_BUF_SIZE * 8)) {

ret = -1;

} else {

ret = 0;

for (i = scd->curbit, j = 0; j < code_size; ++i, ++j) {

ret |= ((scd->buf[i / 8] & (1 << (i % 8))) != 0) << j;

}

}

scd->curbit += code_size;

return ret;

0x02 EXP构造

漏洞成因分析完了,知道EXP的关键点是控制sd->clear_code与GetCode_函数返回结果一致。

1.控制sd->clear_code

首先分下sd->clear_code是从哪里获取的。

获取函数的参数input_code_size,然后再把1左移input_code_size位。得到sd->clear_code。

gd_gif_in.c#L431

static int

LWZReadByte_(gdIOCtx *fd, LZW_STATIC_DATA *sd, char flag, int input_code_size, int *ZeroDataBlockP){

int code, incode, i;

if (flag) {

sd->set_code_size = input_code_size;

sd->code_size = sd->set_code_size+1;

sd->clear_code = 1 << sd->set_code_size ;

sd->end_code = sd->clear_code + 1;

sd->max_code_size = 2*sd->clear_code;

sd->max_code = sd->clear_code+2;

再追踪下调用LWZReadByte函数的地方,并且flag为TRUE。这里看到input_code_size为c。

gd_gif_in.c#L586

if (LWZReadByte(fd, &sd, TRUE, c, ZeroDataBlockP) < 0) {

return;

}

再追踪下c从哪里来的,通过ReadOK从fd获取到的。其实也就是读取GIF图片里面一个字节。

gd_gif_in.c#L569

if (! ReadOK(fd,&c,1)) {

return;

}

前面使用ReadOK函数读取GIF图片的一些信息,比如GIF89a、高、宽之类的。到这里读取到是哪个字节?读取的是UBYTE LZWMinimumCodeSize。如下图所示:

66b39cefea89699334f2cf28c29bb452.gif

所以更改UBYTE LZWMinimumCodeSize的值则可以控制sd->clear_code的值。

2.控制GetCode_返回结果ret

接下来就是控制GetCode_的返回结果ret,由如下代码控制。

gd_gif_in.c#L389

if ( (scd->curbit + code_size) >= scd->lastbit) {

if (scd->done) {

if (scd->curbit >= scd->lastbit) {

/* Oh well */

}

return -1;

}

scd->buf[0] = scd->buf[scd->last_byte-2];

scd->buf[1] = scd->buf[scd->last_byte-1];

if ((count = GetDataBlock(fd, &scd->buf[2], ZeroDataBlockP)) <= 0)

scd->done = TRUE;

scd->last_byte = 2 + count;

scd->curbit = (scd->curbit - scd->lastbit) + 16;

scd->lastbit = (2+count)*8 ;

}

if ((scd->curbit + code_size - 1) >= (CSD_BUF_SIZE * 8)) {

ret = -1;

} else {

ret = 0;

for (i = scd->curbit, j = 0; j < code_size; ++i, ++j) {

ret |= ((scd->buf[i / 8] & (1 << (i % 8))) != 0) << j;

}

}

scd->curbit += code_size;

return ret;

最为关键的是如下代码。

for (i = scd->curbit, j = 0; j < code_size; ++i, ++j) {

ret |= ((scd->buf[i / 8] & (1 << (i % 8))) != 0) << j;

}

scd->buf是通过GetDataBlock获取到如下图data蓝色部分。内容全部为一样,因为可以使scd->buf[i / 8]保证获取到一个固定值。便于控制ret的结果。

66b39cefea89699334f2cf28c29bb452.gif

还有(1 << (i % 8)),这个值是1、2、4、8、16、32、64、128的循环。综合这两点,是可以控制ret值了。

比如:如果想返回结果为2,code_size控制为2的时候,再scd->buf[i/8]= 0xAA满足下面条件就可以返回2的结果。

scd->buf[i/8]&1==0 and scd->buf[i/8]&2!=0 and scd->buf[i/8]&4==0 and scd->buf[i/8]&8!=0 and scd->buf[i/8]&16==0 and scd->buf[i/8]&32!=0 and scd->buf[i/8]&64==0 and scd->buf[i/8]&128!=0

3.完整构造EXP过程

在LZWMinimumCodeSize设置为1。那么sd->clear_code值为2。这个时候GetCode返回的值也必须是2。

do {

sd->firstcode = sd->oldcode =

GetCode(fd, &sd->scd, sd->code_size, FALSE, ZeroDataBlockP);

} while (sd->firstcode == sd->clear_code);

return sd->firstcode;

此时code_size为2。

for (i = scd->curbit, j = 0; j < code_size; ++i, ++j) {

ret |= ((scd->buf[i / 8] & (1 << (i % 8))) != 0) << j;

}

意味着或运算两次。我们把第一次((scd->buf[i / 8] & (1 << (i % 8))) != 0) << j结果控制为0,第二次结果控制为2。这样0或2结果还是2。

ret=ret|((scd->buf[i / 8] & (1 << (i % 8))) != 0) << j;

ret=ret|((scd->buf[i / 8] & (1 << (i % 8))) != 0) << j;

那该怎么做尼?scd->buf[i/8]一直是固定值,(1 << (i % 8)))是1、2、4、8、16、32、64、128的循环(至于为什么是这个循环,有点复杂,枯燥而且很长,由兴趣的可以自己scd->buf[i / 8]为哪个固定值的时候,可以满足上面提出的条件。下面python脚本跑出结果x的值是170(0XAA)。

for x in range(0,255):

if(x&1==0 and x&2!=0 and x&4==0 and x&8!=0 and x&16==0 and x&32!=0 and x&64==0 and x&128!=0):

print(x)

于是对正常的图片进行如下填充就完成EXP的构造了

66b39cefea89699334f2cf28c29bb452.gif

再看下官方给出的EXP

66b39cefea89699334f2cf28c29bb452.gif

code_size为4,所以python脚本如下:

for x in range(0,255):

if(x&1==0 and x&2==0 and x&4==0 and x&8!=0 and x&16==0 and x&32==0 and x&64==0 and x&128!=0):

print(x)

跑出结果x为136(0x88)。图片里面也是用0x88填充的。

0x03参考

66b39cefea89699334f2cf28c29bb452.gif

https://bugs.php.net/bug.php?id=75571

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值