- A+
这波三个白帽子挑战已经过去有段时间段时间了,但仍有同学对此题有很多疑问,回复了几个私信以后,我想还是有必要写一个详细的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牛的指导以及各位大牛的捧场。
如果你觉得还不错的,请点“顶”,如果有错误的地方请指出。