参考:
大部分内容参考自《php反序列化从入门到放弃(入门篇)》,强烈建议前往原blog参观
序列化与反序列化
PHP序列化
序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程,PHP中使用serialize()进行序列化
实例:
1 |
|
1 | O: 代表这是一个对象 |
序列化格式中的字母含义:
1 | a - array b - boolean |
此处属性名的变化涉及到PHP的属性访问权限问题,修改代码,将序列化结果保存
1 |
|
在010Editor中观察16进制结果
其私有属性的构成为 %00类名%00属性名
其保护属性的构成为 %00*%00属性名
由此,php序列化的两个重要点
1. 对private、protected、public属性的序列化方式
2. 对类进行序列化操作时,只序列化属性,不序列化方法
PHP反序列化
与序列化相对的是反序列化,它将流转换为对象
实例:
1 |
|
serialize.txt里的内容是
1 | O:4:"test":3:{s:10:" test flag";s:4:"flag";s:7:" * test";s:4:"test";s:5:"test1";s:5:"test1";} |
但如果在传输过程中serialize的内容被修改
1 | O:4:"test":3:{s:10:" test flag";s:13:"You're hacked";s:7:" * test";s:4:"test";s:5:"test1";s:5:"test1";} |
注:此处需要对16进制的文件进行修改
PHP反序列化漏洞
常见的php系列化和反系列化方式主要有:serialize,unserialize;json_encode,json_decode。
概念解释
将用户可控的数据进行了反序列化,就是PHP反序列化漏洞
反序列化漏洞的成因在于代码中的 unserialize() 接收的参数可控,从上面的例子看,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击。
魔法方法
为什么被称为魔法方法呢?因为是在触发了某个事件之前或之后,魔法函数会自动调用执行,而其他的普通函数必须手动调用才可以执行。PHP 将所有以 __
(两个下划线)开头的类方法保留为魔术方法。
1 | __construct(): 当对象创建时会自动调用(但在unserialize()时是不会自动调用的)。 |
__toString
触发的条件比较多,也因为这个原因容易被忽略,常见的触发条件有下面几种
- echo (
$obj
) / print($obj
) 打印时会触发 - 反序列化对象与字符串连接时
- 反序列化对象参与格式化字符串时
- 反序列化对象与字符串进行
==
比较时(PHP进行==
比较的时候会转换参数类型) - 反序列化对象参与格式化SQL语句,绑定参数时
- 反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
- 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
- 反序列化的对象作为 class_exists() 的参数的时候
实例:
1 |
|
漏洞实例
首先利用php伪协议与文件包含漏洞获取useless.php
useless.php文件中使用了魔术方法__tostring()
方法,当一个对象被当作一个字符串被调用时即可触发,方法的主要作用是读取并打印传进来的$file,估计是通过反序列化漏洞来读取flag.php的内容。追踪以下调用链,在index.php文件中发现使用echo将反序列化的对象当作字符串打印,此处就会触发__tostring()
方法,并且unserialize()内的变量可控,满足反序列化漏洞条件
1 |
|
最后构造反序列化对象
最后flag在源码中
PHP反序列化利用—POP链构造
上面的例子是基于 “ 自动调用 “ 的magic function。但当漏洞/危险代码存在类的普通方法中,就不能指望通过 “ 自动调用 “ 来达到目的了。这时我们需要去寻找相同的函数名,把敏感函数和类联系在一起。一般来说在代码审计的时候我们都要盯紧这些敏感函数的,层层递进,最终去构造出一个有杀伤力的payload。
POP链简介
POP 面向属性编程(Property-Oriented Programing)
常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的。类似于PWN中的ROP,有时候反序列化一个对象时,由它调用的__wakeup()
中又去调用了其他的对象,由此可以溯源而上,利用一次次的 “ gadget “ 找到漏洞点。
POP CHAIN
把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是POP CHAIN 。此时类中所有的敏感属性都属于可控的。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制POP CHAIN达到利用特定漏洞的效果。
POP链利用技巧
常用方法
1 | - 命令执行:exec()、passthru()、popen()、system() |
反序列化中为了避免信息丢失,使用大写S支持字符串的编码
PHP 为了更加方便进行反序列化 Payload 的 传输与显示(避免丢失某些控制字符等信息),我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示,使用如下形式即可绕过,即:
1 | s:4:"user"; -> S:4:"use\72"; |
深浅copy
在php中如果我们使用 & 对变量A的值指向变量B,这个时候是属于浅拷贝,当变量B改变时,变量A也会跟着改变。在被反序列化的对象的某些变量被过滤了,但是其他变量可控的情况下,就可以利用浅拷贝来绕过过滤
1 | $A = &$B; |
php伪协议
配合PHP伪协议实现文件包含、命令执行等漏洞。如glob:// 伪协议查找匹配的文件路径模式
实例
1 |
|
如上代码,危险的命令执行方法eval不在魔术方法中,在evil类中。但是魔术方法 __construct()
是调用normal类,__destruct()
在程序结束时会去调用normal类中的action()方法。而我们最终的目的是去调用evil类中的action()方法,并伪造evil类中的变量 $data
,达成任意代码执行的目的。这样的话可以尝试去构造POP利用链,让魔术方法 __construct()
去调用evil这个类,并且给变量 $data
赋予恶意代码,比如php探针phpinfo(),这样就相当于执行 <?php eval("phpinfo();")?>
编写我们想要执行的效果,然后进行序列化。
但是由于$ClassObj
是protected类型修饰,$data
是private类型修饰,在序列化的时候,多出来的字节都被\x00
填充,需要进行在代码中使用urlencode对序列化后字符串进行编码,否则无法复制解析。
最后payload为:
1 | O%3A4%3A%22main%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A4%3A%22evil%22%3A1%3A%7Bs%3A10%3A%22%00evil%00data%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D |
PHP Session 反序列化
PHP Session
Session
一般称为“会话控制“,简单来说就是是一种客户与网站/服务器更为安全的对话方式。一旦开启了 session
会话,便可以在网站的任何页面使用或保持这个会话,从而让访问者与网站之间建立了一种“对话”机制。不同语言的会话机制可能有所不同,这里仅讨论PHP session
机制。
PHP session
可以看做是一个特殊的变量,且该变量是用于存储关于用户会话的信息,或者更改用户会话的设置,需要注意的是,PHP Session
变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的,且其对应的具体 session
值会存储于服务器端,这也是与 cookie
的主要区别,所以seesion
的安全性相对较高。
session请求过程
当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。
session_start的作用
当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION
超级全局变量中。如果不存在对应的会话数据,则创建名为sess_PHPSESSID(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。
Session 存储
PHP中的Session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。存储的文件是以sess_sessionid来进行命名的,文件的内容就是Session值的序列化之后的内容。
先来大概了解一下PHP Session在php.ini中主要存在以下配置项:
Directive | 含义 |
---|---|
session.save_handler | 设定用户自定义session存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)。默认为files |
session.save_path | 设置session的存储路径,默认在/tmp |
session.serialize_handler | 定义用来序列化/反序列化的处理器名字。默认使用php。 |
session.auto_start | 指定会话模块是否在请求开始时启动一个会话,默认为0不启动 |
session.upload_progress.enabed | 将上传文件的进度信息存储在session中。默认开启 |
session.upload_progress.cleanup | 一旦读取了所有的POST数据,立即清除进度信息。默认开启 |
注:在PHP5.2.17里好像没有一些设置
在PHP中Session有三种序列化的方式,分别是php,php_serialize,php_binary,不同的引擎所对应的Session的存储的方式不同
存储引擎 | 存储方式 |
---|---|
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数序列化处理的值 |
php | 键名 + 竖线 + 经过 serialize() 函数序列处理的值 |
php_serialize | (PHP>5.5.4) 经过 serialize() 函数序列化处理的数组 |
php处理器
session文件为
键名+|+经过反序列化的值
php_serialize处理器
session文件为经过反序列化的值
php_binary
session文件为
键名长度对应的 ASCII 字符 + 键名 + 经过序列化的值
Session 反序列化漏洞
PHP在session存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取 $_SESSION
数据,都会对数据进行序列化和反序列化,PHP中的Session的实现是没有的问题的,漏洞主要是由于使用不同的引擎来处理session文件造成的。
php引擎存储Session的格式为
存储引擎 | 存储方式 |
---|---|
php | 键名 + 竖线 + 经过 serialize() 函数序列处理的值 |
php_serialize | (PHP>5.5.4) 经过 serialize() 函数序列化处理的数组 |
如果程序使用两个引擎来分别处理的话就会出现问题。比如下面的例子,先使用php_serialize引擎来存储Session:
Session1.php
1 |
|
接下来使用php引擎来读取Session文件
Session2.php
1 |
|
漏洞的主要原因在于不同的引擎对于竖杠’ | ‘的解析产生歧义。
对于php_serialize引擎来说’ | ‘可能只是一个正常的字符;但对于php引擎来说’ | ‘就是分隔符,前面是$_SESSION[‘user’]的键名 ,后面是GET参数经过serialize序列化后的值。从而在解析的时候造成了歧义,导致其在解析Session文件时直接对’ | ‘后的值进行反序列化处理。
可以看到PHP能自动反序列化数据的前提是,现有的会话数据是以特殊的序列化格式存储
payload:
1 |
|
如上生成的payload如果想利用php引擎读取Session文件时对’ | ‘解析产生的反序列化漏洞,需要在payload前加个’ | ‘,这个时候经过php_serialize引擎存储就会变成:
二当使用php处理器处理此session时
1 | a:1:{s:4:"user";s:61:"|O:4:"user":2:{s:4:"name";s:7:"scerush";s:3:"age";s:3:"666";}";}| 此部分作为session的key| 此部分经过反序列化后作为session的值 | |
phar:// 伪协议造成的反序列化
phar介绍
简单来说phar
就是php
压缩文档。它可以把多个文件归档到同一个文件中,而且不经过解压就能被 php 访问并执行,与file://
php://
等类似,也是一种流包装器。
phar
结构由 4 部分组成
stub
phar 文件标识,格式为xxx<?php xxx; __HALT_COMPILER();?>;
manifest
压缩文件的属性等信息,以序列化存储;contents
压缩文件的内容;signature
签名,放在文件末尾;
这里有两个关键点,一是文件标识,必须以 __HALT_COMPILER();?>
结尾,但前面的内容没有限制,也就是说我们可以轻易伪造一个图片文件或者 pdf
文件来绕过一些上传限制;二是反序列化,phar
存储的meta-data
信息以序列化方式存储,当文件操作函数通过 phar://
伪协议解析 phar
文件时就会将数据反序列化,而这样的文件操作函数有很多,包括下面这些:
构造有序列化的phar文件
本地生成一个phar文件,要想使用Phar类里的方法,必须将php.ini文件中的phar.readonly配置项配置为0或Off(默认为On)
PHP内置phar类,其中的一些方法如下:
1 | //实例一个phar对象供后续操作 |
生成phar文件的代码如下:
phar.php
1 |
|
运行代码会生成一个phar.phar文件在当前目录下,使用010Editor打开
可以明显的看到meta-data是以序列化的形式存储的,有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,除了之前提及的相关函数,还有常用的文件包含的几个函数 include、include_once、requrie、require_once
对刚才生成的phar使用文件操作函数实现反序列化读取:
1 |
|
成功对meta-data里面的数据进行反序列化输出
将phar文件伪造成其它格式的文件
在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是 __HALT_COMPILER();?>
这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件
1 |
|
采用这种方法可以绕过一些通过校验文件头的上传点
CTF实例
PHP反序列化对象逃逸
在php中,反序列化的过程中必须严格按照序列化规则才能成功实现反序列化,例如:
1 |
|
反序列化按照一定的序列化规则,但是有一定的识别范围,在这个范围之外(花括号}之后)的字符都会被忽略,不影响反序列化的正常进行
比如在$str结尾的花括号后增加一些字符:
1 |
|
逃逸原理
1 |
|
得到的序列化字段为:
1 | a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";} |
这里如果增加了过滤机制,会将flag字段替换为空,那么上面序列化字符串过滤结果为:
1 | a:3{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";} |
如果将上面过滤之后的字符串进行反序列化,会不会报错呢?
1 |
|
打印出了过滤前与过滤后的反序列化字符串,对比就可以发现当把flag过滤之后,string(24)规定需要24个字符,为了满足反序列化的规则,会向后读取字符,直至凑齐24个字符,也就是读取”;s:8“function”;s:59:”a,当凑齐24个字符后以 “; 结尾。之后[“img”]就按照string(20)读取20个字符,[“dd”]按照string(1)读取一个字符,剩余的字符就直接被忽略,不影响正常的反序列化过程。
写成数组的形式为:
1 | $_SESSION["user"]='";s:8:"function";s:59:"a'; |
看完上面的例子,发现本来想读取的内容是 $_SESSION["img"]
的值为:L2QwZzNfZmxsbGxsbGFn
,但是由于过滤掉了flag,string(24)位数不够往后读取,就把 $_SESSION["function"]
的值的前24位存放在 $_SESSION["user"]
中,把 $_SESSION["funcion"]
的值的后20为存放在 $_SESSION["img"]
中,导致 ZDBnM19mMWFnLnBocA==
代替了 $_SESSION["img"]
对应的原本的值。而识别完成后序列化最后面的 ";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
被忽略掉了,不影响正常的反序列化过程。
可以看到本例中 $_SESSION["img"]
对应的值发生了变化。这样的话岂不是可以做到”隔山打牛”,如果我们能够控制原来 $_SESSION
数组的funcion的值,但无法控制img的值,我们就可以通过这种方式间接控制到img对应的值