今天对四月份的DASCTF赛中的web题进行复现,在此总结一下。这道题是典型的代码审计,代码量非常的多。很容易就被劝退了。
先看代码:
<?php
class Base
{
public function __get($name)
{
$getter = 'get' . $name;
if (method_exists($this, $getter)) {
return $this->$getter();
} else {
throw new Exception("error property {$name}");
}
}
public function __set($name, $value)
{
$setter = 'set' . $name;
if (method_exists($this, $setter)) {
return $this->$setter($value);
} else {
throw new Exception("error property {$name}");
}
}
public function __isset($name)
{
$getter = 'get' . $name;
if (method_exists($this, $getter))
return $this->$getter() !== null;
return false;
}
public function __unset($name)
{
$setter = 'set' . $name;
if (method_exists($this, $setter))
$this->$setter(null);
}
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_);
}
}
}
<?php
class Filter extends Base
{
public $lastModified;
public $lastModifiedExpression;
public $etagSeed;
public $etagSeedExpression;
public $cacheControl='max-age=3600, public';
public function preFilter($filterChain)
{
$lastModified=$this->getLastModifiedValue();
$etag=$this->getEtagValue();
if($etag===false&&$lastModified===false)
return true;
if($etag)
header('ETag: '.$etag);
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])&&isset($_SERVER['HTTP_IF_NONE_MATCH']))
{
if($this->checkLastModified($lastModified)&&$this->checkEtag($etag))
{
$this->send304Header();
$this->sendCacheControlHeader();
return false;
}
}
elseif(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']))
{
if($this->checkLastModified($lastModified))
{
$this->send304Header();
$this->sendCacheControlHeader();
return false;
}
}
elseif(isset($_SERVER['HTTP_IF_NONE_MATCH']))
{
if($this->checkEtag($etag))
{
$this->send304Header();
$this->sendCacheControlHeader();
return false;
}
}
if($lastModified)
header('Last-Modified: '.gmdate('D, d M Y H:i:s', $lastModified).' GMT');
$this->sendCacheControlHeader();
return true;
}
protected function getLastModifiedValue()
{
if($this->lastModifiedExpression)
{
$value=$this->evaluateExpression($this->lastModifiedExpression);
if(is_numeric($value)&&$value==(int)$value)
return $value;
elseif(($lastModified=strtotime($value))===false)
throw new Exception("error");
return $lastModified;
}
if($this->lastModified)
{
if(is_numeric($this->lastModified)&&$this->lastModified==(int)$this->lastModified)
return $this->lastModified;
elseif(($lastModified=strtotime($this->lastModified))===false)
throw new Exception("error");
return $lastModified;
}
return false;
}
protected function getEtagValue()
{
if($this->etagSeedExpression)
return $this->generateEtag($this->evaluateExpression($this->etagSeedExpression));
elseif($this->etagSeed)
return $this->generateEtag($this->etagSeed);
return false;
}
protected function checkEtag($etag)
{
return isset($_SERVER['HTTP_IF_NONE_MATCH'])&&$_SERVER['HTTP_IF_NONE_MATCH']==$etag;
}
protected function checkLastModified($lastModified)
{
return isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])&&@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])>=$lastModified;
}
protected function send304Header()
{
header('HTTP/1.1 304 Not Modified');
}
protected function generateEtag($seed)
{
return '"'.base64_encode(sha1(serialize($seed),true)).'"';
}
}
<?php
abstract class ListView extends Base
{
public $tagName='div';
public $template;
public function run()
{
echo "<".$this->tagName.">\n";
$this->renderContent();
echo "<".$this->tagName.">\n";
}
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];
}
}
<?php
class TestView extends ListView
{
const FILTER_POS_HEADER='header';
const FILTER_POS_BODY='body';
public $columns=array();
public $rowCssClass=array('odd','even');
public $rowCssClassExpression;
public $rowHtmlOptionsExpression;
public $selectableRows=1;
public $data=array();
public $filterSelector='{filter}';
public $filterCssClass='filters';
public $filterPosition='body';
public $filter;
public $hideHeader=false;
public function renderTableHeader()
{
if(!$this->hideHeader)
{
echo "<thead>\n";
if($this->filterPosition===self::FILTER_POS_HEADER)
$this->renderFilter();
if($this->filterPosition===self::FILTER_POS_BODY)
$this->renderFilter();
echo "</thead>\n";
}
elseif($this->filter!==null && ($this->filterPosition===self::FILTER_POS_HEADER || $this->filterPosition===self::FILTER_POS_BODY))
{
echo "<thead>\n";
$this->renderFilter();
echo "</thead>\n";
}
}
public function renderFilter()
{
if($this->filter!==null)
{
echo "<tr class=\"{$this->filterCssClass}\">\n";
echo "</tr>\n";
}
}
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";
}
}
大体看一下代码,他们之间是有继承关系的。
Filter.php和ListView都继承于Base。而TestView继承于ListView。这是题目附件中的四个源码文件。 先看题目,
我们可以通过action进行GET传参,通过properties进行POST传参。下面的foreach数组遍历可以看出properties是要传数组进去。通过action传入的值会被当作类名,从而调用该类的run方法。
在四个类中检索run方法。只有在ListView.php中存在run方法。TestView类是子类中的子类,所以用action参数传入TestView类,实例化这个类,这四个类所以的方法都可以调用。妙~
接着浏览父类Base,看一下有没有执行命令的地方,有一个eval函数。
这个eval函数在这个evaluateExpression方法里面,然后在其他类中检索有没有可以来调用这个evaluateExpression方法的函数,因此在Filter.php中找到getEtagValue()和getLastModifiedValue()
再查看这两个类可不可以被调用。发现Base类中的_get魔术方法可以调用任意类。但是,没什么可以来触发_get方法的。
换个思路,来从run()函数走,在ListView类中有run()函数,可以调用renderContent()函数,
往下看,renderContent()函数功能是将template参数的内容当作renderSection函数的内容传入。那么接着往下看,传入的内容与render拼接,然后当作函数名调用。这样的话,我们就可以随意调用以render开头的函数了 。全局搜索以render为首的函数。
通过代码分析,我们只能去调用无参的函数,我们调用TestView类中的renderTableBody函数,在该函数调用renderTableRow函数,这个函数中有我们要用的目的函数,就达成了命令执行。
整理一下思路:
run() -->renderContent() -->renderSection() -->renderTableBody() -->renderTableRow()-->evaluateExpression()
为什么可以这么调用,因为子类可以用父类的方法,而父类不能用子类的方法。回看上面写的继承
关系,应该就明白了。
目前来说,分析怎么构造payload。前面已说action=TestView,现在看properties参数。
其中template变量要赋值TableBody,来调用renderTableBody函数。因为preg_replace_callback函数进行正则匹配时被{}包裹,所以传参也需要{}包裹,不然不能匹配。
在这个函数中需要满足data变量大于0才能调用renderTableRow函数。
接着就是对目标函数传参,进行命令执行了。该目标函数的参数就是rowHtmlOptionsExpression,
对这个参数传入恶意代码即可。
最终payload:
?action=TestView
properties[template]={TableBody} &properties[data]=1&properties[rowHtmlOptionsExpression]=system('/readflag');
得出flag。
在审计大量代码时,最痛苦的就是无用的东西太多,有用的信息太难找,在审计这些大量的代码的时候,一定不能浮躁。我们不需要读懂每一行代码,精读有用的代码,找出首尾,并且找出一个个有用的函数,形成一条逻辑思路。这样审计代码就不会太难。