[三个白帽子]is_numeric的理解和PHP 脚本多字节字符解析模式带来的安全隐患writeup

L.N. (http://ln.sycsec.com/) | 2015-11-25 19:18

这波三个白帽子挑战已经过去有段时间段时间了,但仍有同学对此题有很多疑问,回复了几个私信以后,我想还是有必要写一个详细的writeup供有疑问的同学观看。

题目连接《【三个白帽子】来来来挑战一波》(题目虽然不能做了,但评论还是很有价值的)

题目代码

<?php

foreach(array('_GET','_POST','_COOKIE') as $key){

    foreach($$key as $k => $v){

        if(is_array($v)){

            errorBox("hello,sangebaimao!");

        }else{

            $k[0] !='_'?$$k = addslashes($v):$$k = "";

        }

    }

}

if(!$num){

    show_source(__FILE__);

}

$filter = array('0',' ','\n','\t','\r','\v','\f','+','-');

if(is_numeric($num)&&!in_array($num[0],$filter)&&strtolower(urlencode($num[0]))!='%0b'&&!preg_match('/[\s]{1,}/',$num[0]) && !intval($num)){

    $configfile = "<?php \$config =array('a'=>'$config');";

    file_put_contents('./tmp/cache.php',$configfile);

}else{

    errorBox("hello,sangebaimao!");

}

function errorBox($error){

    echo "$error";exit;

}

?>


通过阅读这段代码,可以看出想要RCE需要绕过两个地方,

第一个是:

if(is_numeric($num)&&!in_array($num[0],$filter)&&strtolower(urlencode($num[0]))!='%0b'&&!preg_match('/[\s]{1,}/',$num[0]) && !intval($num))

第二个是:

$configfile = "<?php \$config =array('a'=>'$config');";

file_put_contents('./tmp/cache.php',$configfile);

0×01 is_numeric理解

    简单翻看is_numeric实现代码,is_numeric对输入的参数,先做了样式判断如果是整型、浮点型就直接返回true,如果是字符串则进入is_numeric_string函数进行判断

switch (Z_TYPE_P(arg)) {

        case IS_LONG:

        case IS_DOUBLE:

            RETURN_TRUE;

            break;

        case IS_STRING:

            if (is_numeric_string(Z_STRVAL_P(arg), Z_STRLEN_P(arg), NULL, NULL, 0)) {

                RETURN_TRUE;

            } else {

                RETURN_FALSE;

            }

            break;

        default:

            RETURN_FALSE;

            break;


经过查找,找到真正的处理函数_is_numeric_string_ex,省略一些代码,我们只用知道哪些字符能够出现在is_numeric的参数中,很明显可以看出,

空格、\t、\n、\r、\v、\f、+、-能够出现在参数开头,“点”能够在参数任何位置,E、e只能出现在参数中间

ZEND_API zend_uchar ZEND_FASTCALL _is_numeric_string_ex(......) /* {{{ */

{  

    ......

    /* Skip any whitespace

     * This is much faster than the isspace() function */

    while (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r' || *str == '\v' || *str == '\f') {

        str++;

        length--;

    }

    ptr = str;

    if (*ptr == '-') {

        neg = 1;

        ptr++;

    } else if (*ptr == '+') {

        ptr++;

    }

    if (ZEND_IS_DIGIT(*ptr)) {

        /* Skip any leading 0s */

        while (*ptr == '0') {

            ptr++;

        }

....

        for (type = IS_LONG; !(digits >= MAX_LENGTH_OF_LONG && (dval || allow_errors == 1)); digits++, ptr++) {

check_digits:

            if (ZEND_IS_DIGIT(*ptr)) {

                tmp_lval = tmp_lval * 10 + (*ptr) - '0';

                continue;

            } else if (*ptr == '.' && dp_or_e < 1) {

                goto process_double;

            } else if ((*ptr == 'e' || *ptr == 'E') && dp_or_e < 2) {

                const char *e = ptr + 1;

                if (*e == '-' || *e == '+') {

                    ptr = e++;

                }

                if (ZEND_IS_DIGIT(*e)) {

                    goto process_double;

                }

            }

            break;

        }

......

    }

}


再看下我们的题目中第一个需要bypass的地方:

if(is_numeric($num)&&!in_array($num[0],$filter)&&strtolower(urlencode($num[0]))!='%0b'&&!preg_match('/[\s]{1,}/',$num[0]) && !intval($num))

1.is_numeric($num)检测

2.!in_array($num[0],$filter)过滤array('0',' ','\n','\t','\r','\v','\f','+','-');

3.strtolower(urlencode($num[0]))!='%0b'过滤%0b

4.!preg_match('/[\s]{1,}/',$num[0]) 过滤\t\n\r\f\v

5.!intval($num) 必须intval检测返回false


综合看下来".0"似乎是唯一选择。

0×02 [pch-018] 脚本多字节字符解析模式带来的安全隐患

第二个的bypass点是关于php中多字节字符的安全问题,@ryat牛的详细分析文档:https://github.com/80vul/phpcodz/blob/master/research/pch-018.md

通俗的说,在php中有些函数只能处理单字节字符,如:var_export() 、addslashes()等,当开启多字节解析模式的时候,这些单字节处理函数去处理多字节就带来了安全隐患。

比如:%ab%27经过addslashes处理变成%ab%5C%27,在多字节模式输出的时候%ab%5C被识别成一个字符,最后使得addslashes转义失效

题中:

$configfile = "<?php \$config =array('a'=>'$config');";

file_put_contents('./tmp/cache.php',$configfile);


当config被赋值成%ab%27);phpinfo();/*,再写入cache.php文件的时候phpinfo()就被执行了。

当然上边说的一切就建立在php开启多字节解析的时候:

zend.multibyte = On

zend.script_encoding = CP936


不知道又有人注意到,我在题目目录下放置了一个phpinfo.php文件,目的就是让大家注意到这个配置,本地测试的同学请修改这个配置。

最后感谢@ryat牛的指导以及各位大牛的捧场。

如果你觉得还不错的,请点“”,如果有错误的地方请指出。