PHP反序列化漏洞

百家 作者:焦点安全应急响应中心 2021-09-18 20:26:36
01
前言

    “FSRC 经验分享”系列文章,旨在分享焦点安全工作过程中的经验和成果,包括但不限于漏洞分析、运营技巧、SDL推行、等保合规、自研工具等。

    欢迎各位安全从业者持续关注~


正文约3300字    推荐阅读时间8分钟


02
概念的简单梳理



类与对象


一个共享相同结构和行为的对象的集合(类是抽象的,不占用内存)。每个类的定义都以关键字 class 开头,后面跟着类的名字


对象可以对其做具体操作处理的一些东西(对象是具体的,占用内存)。一个对象有状态、行为和标识三种属性。




序列化与反序列化


序列化serialization):在数据处理中,将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。序列化的目的是方便数据的传输和存储


反序列化(unserialization):序列化的逆过程,就是将字符串重新恢复成对象。


用一个例子来理解序列化与反序列化:

我们在淘宝买桌子的时候,序列化就是商家将桌子拆解成零件邮寄给买家的过程,反序列化就是买家将收到的桌子零件按图纸组装成桌子的过程




魔术函数


魔术函数类中些特殊函数叫 magic 函数, magic 函数命名是以符号“__”开头的(双下划线),比如 __construct,__destruct,__toString,__sleep,__wakeup 等。


这些函数在某些情况下会自动调用。比如:创建对象时调用 __construct;销毁对象时调用 __destruct;当对象被当作一个字符串时调用 __toString




序列化字符串解析


以下图PHP代码及其输出为例,来展示序列化字符串及其对应含义。

PHP代码图

输出及字符串含义



03
PHP反序列化实例应用


为了更好地帮助大家理解漏洞原理,下面以CVE-2016-7124改编的一个CTF小题为基础,进一步探究漏洞的真实本质



题目内容


题目代码:

阅读题目代码后我们发现, 在类 Demo 中有三个方法,一个构造,一个析构,还有就是一个魔术方法。构造函数 __construct() 在程序执行开始的时候对变量进行赋初值。析构函数 __destruct(),在对象所在函数执行完成之后,会自动调用,这里就会高亮显示出文件。在反序列化执行之前,会先执行 __wakeup() 这个魔术方法




解题思路


第一步

因为在 PHP5 对序列化字符串的解析过程中,若 "O:+4 :" 后面出现 "+",会跳过它直接检查下一位是否是数字,若是数字则继续解析下去。

preg_match('/[oc]:\d+:/i'$var)//在不区分大小写的情况下,若字符串出现"o:数字" 或者"c:数字"这样的格式,那么就被过滤

从而实现正则绕过,代码如下

$var = str_replace('O:4', 'O:+4',$var)


第二步

如果存在 __wakeup 方法,调用 unserilize() 方法前则先调用 __wakeup 方法,预先准备对象需要的资源。但是序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过 __wakeup 的执行

我们首先将上述代码序列化:

输出结果为

O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}

要绕过 __wakeup 的执行,按上述说的更改对象属性个数的值也就是把 Demo:1 改成 Demo:2 或者其他比1大的数字即可实现绕过。

所以只需利用如下代码,完成题目,得到 fl4g




实际应用思路


PHP序列化和反序列化注入步骤如下:以反序列化函数 unserialize() 为例:

(1)寻找一个可以控制的反序列化函数(在反序列化中,我们所能控制的数据就是对象中的各个属性值)通过这个反序列化函数调用类;

(2)检查类,查找每一个类,找出包含一些魔法函数的类;

(3)查找魔法函数里有没有可控的危险操作(也就是有没有调用对象属性值,因为满足上面的条件时,属性值用户可以控制);

(4)构造反序列化字符串,触发魔法函数的执行,魔法函数里的危险操作拼接了我们注入的数据执行了



04
问题解答



问题一


__sleep 魔术函数为什么会在序列化操作执行之前被调用,__wake up魔术函数为什么会在反序列化操作执行之前被调用?


用 serialize() 函数实例化一个类的时候会检查类中是否存在一个魔术方法  __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法不返回任何内容,则  NULL 被序列化,导致错误。


与 serialize() 相反,unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。__wakeup() 经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作




问题二


利用反序列化漏洞注入来达到恶意目的的场景有哪些?


场景一:利用 __destruct() 函数构造代码执行漏洞,示例代码如下所示:

我们只需要在构造的本地漏洞环境 localhost/test.php 后输入下列语句即可执行 phpinfo:

?obj=O:6:"person":1:{s:4:"name";s:10:"phpinfo();";}



场景二:利用 __destruct() 函数构造任意文件删除漏洞,示例代码如下所示:

比如在同一目录下有一个文件 1.txt,我们只需要在构造的本地漏洞环境 localhost/test.php 后输入下列语句即可删除 1.txt:

?obj=O:6:"person":1:{s:4:"name";s:5:"1.txt";}




问题三


PHP中 public、protected 和 private(访问修饰符)的区别


以示例代码为例

输出结果为:

O:4:"Test":6:{s:7:"Testa";i:1;s:4:"*b";i:1;s:1:"c";i:1s:1:"d";i:1:s:1:"e";i=1;}

其中当被 private 修饰时,输出的结果为 Testa,对象名称的长度本应为 4,但由于构造注入对象的时候被 private 修饰,输出结果其实是 %00Test%00a(%00是用来区分类名和属性名的不可见字符),所以这里却显示为7;

当被 protect 修饰时,输出结果其实是 %00*%00b,所以对象名称的长度不是 1 而是 4 。



05
关于防御

PHP反序列化漏洞依赖几个条件: 

1、unserialize 函数的参数可控;

2、脚本中存在一个构造函数(__construct())、析构函数(__destruct())、__wakeup() 函数,且其中有向php文件中写数据的操作的类;

3、所写的内容需要有对象中的成员变量的值。


故防范的方法有:

1、严格控制 unserialize 函数的参数,坚持用户所输入的信息都是不可靠的原则;

2、对于 unserialize 后的变量内容进行检查,以确定内容没有被污染



06
总结

本篇文章从一个由CVE改编的PHP反序列化小题出发,带大家认识了PHP反序列化漏洞。这个漏洞的原理不是那么容易理解,大家看完后不妨从靶场或者一道CTF题出发,将漏洞具象化,读懂PHP代码每一句的含义,结合上下文分析出它究竟是哪一步出现了问题,完整地过一遍PHP反序列化的流程,会帮助你更快地理解并掌握漏洞原理。


最后,作为一个Web安全初学者,诚挚地邀请大家一起讨论关于漏洞的问题,同时欢迎大家向我提问或指正文章的不当之处



07
免责声明

本文中提到的相关资源已在网络公布,仅供研究学习使用,请遵守《网络安全法》等相关法律法规


责任编辑:渊微、周周


焦点安全,因你而变
焦点科技漏洞提交网址:https://security.focuschina.com



关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接