php反序列化一直是一个难点,需要我们对代码熟悉,也是CTF常出的题型,今天来详细聊聊反序列化!
php反序列化基础
php类与对象
类是定义一系列属性和操作的模板,而对象,就是把属性进行实例化,完事交给类里面的方法,进行处理。
1 |
|
分析一下这里的列子,为什么会调用smile方法呢?
这里先定义了一个people类,然后 实例化了一个对象 给 $psycho , $psycho就可以调用people 类的方法了,所以输出了!
这就是php类与对象最基础的使用。
魔术方法
为什么被称为魔法方法呢?因为是在触发了某个事件之前或之后,魔法函数会自动调用执行,而其他的普通函数必须手动调用才可以执行。PHP 将所有以 __
(两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议不要以 __
为前缀。下表为php常见的魔术方法:
方法名 | 作用 |
---|---|
__construct | 构造函数,在创建对象时候初始化对象,一般用于对变量赋初值 |
__destruct | 析构函数,和构造函数相反,在对象不再被使用时(将所有该对象的引用设为null)或者程序退出时自动调用 |
__toString | 当一个对象被当作一个字符串被调用,把类当作字符串使用时触发,返回值需要为字符串,例如echo打印出对象就会调用此方法 |
__wakeup() | 使用unserialize时触发,反序列化恢复对象之前调用该方法 |
__sleep() | 使用serialize时触发 ,在对象被序列化前自动调用,该函数需要返回以类成员变量名作为元素的数组(该数组里的元素会影响类成员变量是否被序列化。只有出现在该数组元素里的类成员变量才会被序列化) |
__destruct() | 对象被销毁时触发 |
__call() | 在对象中调用不可访问的方法时触发,即当调用对象中不存在的方法会自动调用该方法 |
__callStatic() | 在静态上下文中调用不可访问的方法时触发 |
__get() | 读取不可访问的属性的值时会被调用(不可访问包括私有属性,或者没有初始化的属性) |
__set() | 在给不可访问属性赋值时,即在调用私有属性的时候会自动执行 |
__isset() | 当对不可访问属性调用isset()或empty()时触发 |
__unset() | 当对不可访问属性调用unset()时触发 |
__invoke() | 当脚本尝试将对象调用为函数时触发 |
额外提一下__tostring的具体触发场景:
(1) echo($obj) / print($obj) 打印时会触发
(2) 反序列化对象与字符串连接时
(3) 反序列化对象参与格式化字符串时
(4) 反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
(5) 反序列化对象参与格式化SQL语句,绑定参数时
(6) 反序列化对象在经过php字符串函数,如 strlen()、addslashes()
(7) 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
(8) 反序列化的对象作为 class_exists() 的参数的时候
举个例子:
1 |
|
php序列化/反序列化
在开发的过程中常常遇到需要把对象或者数组进行序列化存储,反序列化输出的情况。把数组存储到mysql数据库中时,我们时常需要将数组进行序列化操作。
php序列化(serialize):是将变量转换为可保存或传输的字符串的过程
php反序列化(unserialize):就是在适当的时候把这个字符串再转化成原来的变量使用
这两个过程结合起来,可以轻松地存储和传输数据,使程序更具维护性。
常见的php系列化和反系列化方式主要有:serialize,unserialize;json_encode,json_decode。
序列化
举个序列化小栗子:
1 |
|
以上是序列化之后的结果,o代表是一个对象,4是对象test的长度,3的意思是有三个类属性,后面花括号里的是类属性的内容,s表示的是类属性team的类型,4表示类属性team的长度,后面的以此类推。值得一提的是,类方法并不会参与到实例化里面。
需要注意的是变量受到不同修饰符(public,private,protected)修饰进行序列化时,序列化后变量的长度和名称会发生变化。
- 使用public修饰进行序列化后,变量$team的长度为4,正常输出。
- 使用private修饰进行序列化后,会在变量$team_name前面加上类的名称,在这里是test,并且长度会比正常大小多2个字节,也就是9+4+2=15。
- 使用protected修饰进行序列化后,会在变量$team_group前面加上*,并且长度会比正常大小多3个字节,也就是10+3=13。
通过对比发现,在受保护的成员前都多了两个字节,受保护的成员在序列化时规则:
受Private修饰的私有成员,序列化时: \x00 + [私有成员所在类名] + \x00 [变量名]
受Protected修饰的成员,序列化时:\x00 + * + \x00 + [变量名]
其中,”\x00”代表ASCII为0的值,即空字节,” * “ 必不可少。
序列化格式中的字母含义:
1 | a - array b - boolean |
反序列化
反序列化的话,就依次根据规则进行反向复原。
这边定义一个字符串,然后使用反序列化函数unserialize进行反序列化处理,最后使用var_dump进行输出:
1 |
|
php反序列化漏洞(对象注入)
在反序列化过程中,其功能就类似于创建了一个新的对象(复原一个对象可能更恰当),并赋予其相应的属性值。如果让攻击者操纵任意反序列数据, 那么攻击者就可以实现任意类对象的创建,如果一些类存在一些自动触发的方法(魔术方法),那么就有可能以此为跳板进而攻击系统应用。命令执行函数——-> system eval assert call_user_func
挖掘反序列化漏洞的条件是:
1. 代码中有可利用的类,并且类中有__wakeup(),__sleep(),__destruct()这类特殊条件下可以自己调用的魔术方法。
2. unserialize()函数的参数可控。
php对象注入示例一:
1 |
|
如上代码,最终的目的是通过调用__destruct()
这个析构函数,将恶意的payload注入,导致代码执行。根据上面的魔术方法的介绍,当程序跑到unserialize()
反序列化的时候,会触发__destruct()
方法,同时也可以触发__wakeup()
方法。但是如果想注入恶意payload,还需要对$test的值进行覆盖,题目中已经给出了序列化链,很明显是对类A的$test变量进行覆盖。
可以看到当我们传入的参数为 phpinfo()
我们的$p = "O:1:"A":1:{s:4:"test";s:10:"phpinfo();";}"
这样的话在调用__destruct
方法执行eval之前就把变量$test的值替换成恶意payload。
这里__destruct
方法是在程序退出的时候执行的!
php对象注入示例二:
这是来自bugku的一道题。题目地址
index.php
1 |
|
hint.php
1 |
|
hint.php文件中使用了魔术方法__tostring()
方法,当一个对象被当作一个字符串被调用时即可触发,方法的主要作用是读取并打印传进来的$file,估计是通过反序列化漏洞来读取flag.php的内容。追踪以下调用链,在index.php文件中发现使用echo将反序列化的对象当作字符串打印,此处就会触发__tostring()
方法,并且unserialize()内的变量可控,满足反序列化漏洞条件。直接构造payload:(关于使用php://filter进行任意文件的读取,参照p牛:《谈一谈php://filter的妙用》)
poc.phg
1 |
|
password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}
php对象注入示例三:
1 | <?php |
如上代码主要通过调用魔术方法__wakeup将$test的值写入flag.php文件中,当调用unserialize()反序列化操作时会触发__wakeup魔术方法,接下来就需要构造传进去的payload,先生成payload:
poc.php
1 |
|
payload:
?id=O:4:"test":1:{s:4:"test";s:26:"<?php%20system("whoami");%20?>";}
内容改变
在执行unserialize()方法时会触发__wakeup()方法执行,将传入的字符串反序列化后,会覆盖掉test类里面$test变量的值,将php探针写入flag.php文件中,并通过下面的require引用,导致命令执行。
php反序列化利用—POP链构造
上面的两个例子都是基于 “ 自动调用 “ 的magic function。但当漏洞/危险代码存在类的普通方法中,就不能指望通过 “ 自动调用 “ 来达到目的了。这时我们需要去寻找相同的函数名,把敏感函数和类联系在一起。一般来说在代码审计的时候我们都要盯紧这些敏感函数的,层层递进,最终去构造出一个有杀伤力的payload。
POP链简介
1. POP 面向属性编程(Property-Oriented Programing)
常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的。类似于PWN中的ROP,有时候反序列化一个对象时,由它调用的__wakeup()中又去调用了其他的对象,由此可以溯源而上,利用一次次的 “ gadget “ 找到漏洞点。
2. POP CHAIN
把魔术方法作为最开始的小组件,然后在魔术方法中调用其他函数(小组件),通过寻找相同名字的函数,再与类中的敏感函数和属性相关联,就是POP CHAIN 。此时类中所有的敏感属性都属于可控的。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制POP CHAIN达到利用特定漏洞的效果。
POP链利用技巧
1. 一些有用的POP链中出现的方法:
php
需要魔术方法 因为魔术方法是他最终执行点 执行函数
执行函数
利用链
需要unserialize方法
session
phar
php://filter
1 | - 命令执行:exec()、passthru()、popen()、system() |
2. 反序列化中为了避免信息丢失,使用大写S支持字符串的编码。
PHP 为了更加方便进行反序列化 Payload 的 传输与显示(避免丢失某些控制字符等信息),我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示,使用如下形式即可绕过,即:
1 | s:4:"user"; -> S:4:"use\72"; |
3. 深浅copy
在php中如果我们使用 & 对变量A的值指向变量B,这个时候是属于浅拷贝,当变量B改变时,变量A也会跟着改变。在被反序列化的对象的某些变量被过滤了,但是其他变量可控的情况下,就可以利用浅拷贝来绕过过滤。
1 | $A = &$B; |
- 利用PHP伪协议
配合PHP伪协议实现文件包含、命令执行等漏洞。如glob:// 伪协议查找匹配的文件路径模式。
POP链构造小例子一
1 |
|
如上代码,危险的命令执行方法eval不在魔术方法中,在evil类中。但是魔术方法__construct()
是调用normal类,__destruct()
在程序结束时会去调用normal类中的action()方法。而我们最终的目的是去调用evil类中的action()方法,并伪造evil类中的变量$data,达成任意代码执行的目的。这样的话可以尝试去构造POP利用链,让魔术方法__construct()
去调用evil这个类,并且给变量$data赋予恶意代码,比如php探针phpinfo(),这样就相当于执行<?php eval("phpinfo();")?>
。尝试构造payload:
poc
1 |
|
编写我们想要执行的效果,然后进行序列化。
但是由于$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%3A17%3A%22system%28%27whoami%27%29%3B%22%3B%7D%7D |
POP链构造小例子二
1 |
|
像如上代码比较复杂的可以先定位魔术方法与漏洞触发点。在代码中发现__toString()
魔术方法调用了file_get_contents()来读取变量$name的数据。当程序执行结束或者变量销毁时就会自动调用析构函数__destruct()并使用echo输出变量,__toString()
方法在此时会被自动调用。关键在于如果能控制变量$name,就可以造成任意文件读取漏洞。但是通读代码发现前端传入的可控数据只有变量$user,并且传入的$user还不能包含 “user” 子符串。解决方法:
1. $input 前端传进来的参数不允许包含”user”字段,可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示,使用16进制即可绕过
2. $name字段不可控,$user字段可控,可以使用浅copy来实现赋值。
尝试构造payload:
1 |
|
1 | ?input=O:6:"MyFile":2:{s:4:"name";N;S:4:"use\72";R:2;}&user=1.txt |
传入user=1.txt,就相当于替换$this->name的值,成功读取文件。
POP链构造小例子三
这个小例子来自于《PHP反序列化由浅入深》,这个例子有点意思。
1 |
|
poc
1 |
|
1 | ?a=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":2:{s:4:"str1";O:7:"GetFlag":0:{}s:4:"str2";N;}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;}s:4:"mod2";N;} |
POP链构造小例子四
1 |
|
通读代码,发现漏洞点在于可以通过调用Modifier类中的include方法造成任意文件包含漏洞。这是一个类的普通方法,要让这个方法执行,需要构造一个POP链。
- Modifier类中append方法被
__invoke()
调用,并传入$this->var参数。当类Modifier被当作函数调用的时候,会自动调用魔术方法__invoke()
。
最后在Test类的构造函数看到了$this->p,这里可以直接通过反序列化控制属性p的值,然后通过调用魔术方法__get()
来return一个p(),类被当作函数调用就可以触发魔术方法__invoke()
,需要把p赋值为Modifier类的对象,$this->var可以传入想要包含的文件。
1 | $this->p = new Modifier() |
- Test类中的魔术方法~是在读取不可访问属性的值时会被调用,发现Show类中的魔术方法
__toString()
访问了str的source属性,如果str是Test类的对象,则不存在source属性,Test类的__get()
魔术方法就会被调用。
1 | $this->str = new Test() |
- Show类中的魔术方法
__toString()
是当一个对象被当作一个字符串被调用。发现Show类的构造方法__construct()
使用echo输出字符串,如果$this->source指向一个对象,就会调用__toString()
方法。
1 | $a = new Show(); |
最终的调用链如下:
1 | include <-- Modifier::__invoke() <-- Test::__get() <-- Show::__toString() |
尝试构造payload:
1 | <?php |
成功读取到文件
我们改未phpinfo看一下是否能解析出来
也没问题成功解析
PHP Session反序列化
PHP Session
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数据,立即清除进度信息。默认开启 |
在PHP中Session有三种序列化的方式,分别是php,php_serialize,php_binary,不同的引擎所对应的Session的存储的方式不同
存储引擎 | 存储方式 |
---|---|
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数序列化处理的值 |
php | 键名 + 竖线 + 经过 serialize() 函数序列处理的值 |
php_serialize | (PHP>5.5.4) 经过 serialize() 函数序列化处理的数组 |
下面通过小例子来展示一下存储方式的不同:
php处理器:
1 | <?php |
序列化的结果为:username|s:7:”JGwebre”;
文件名为:sess_p1jic0levu5o94e3359qst9b5p 其中p1jic0levu5o94e3359qst9b5p这个为当前会话的session_id
Session文件内容为:$_SESSION[‘username’]的键名 + | + GET参数经过serialize序列化后的值。
php_binary处理器
1 | <?php |
序列化的结果为:usernames:7:”JGwebre”;
文件名为sess_p1jic0levu5o94e3359qst9b5p,其中p1jic0levu5o94e3359qst9b5p为当前会话的sessionid
Session文件内容为:键名的长度对应的 ASCII 字符 + $_SESSION[‘username’]的键名 + GET参数经过serialize序列化后的值。
php_serialize处理器
1 | <?php |
序列化的结果为:a:1:{s:8:”username”;s:7:”JGwebre”;}
文件名为sess_p1jic0levu5o94e3359qst9b5p,其中p1jic0levu5o94e3359qst9b5p为当前会话的sessionid
Session文件内容为:GET参数经过serialize序列化后的值。
Session反序列化漏洞
PHP在session存储和读取时,都会有一个序列化和反序列化的过程,PHP内置了多种处理器用于存取 $_SESSION 数据,都会对数据进行序列化和反序列化,PHP中的Session的实现是没有的问题的,漏洞主要是由于使用不同的引擎来处理session文件造成的
存在对$_SESSION变量赋值
php引擎存储Session的格式为
php | 键名 + 竖线 + 经过 serialize() 函数序列处理的值 |
---|---|
php_serialize | (PHP>5.5.4) 经过 serialize() 函数序列化处理的数组 |
如果程序使用两个引擎来分别处理的话就会出现问题。比如下面的例子,先使用php_serialize引擎来存储Session:
Session1.php
1 | <?php |
接下来使用php引擎来读取Session文件
Session2.php
1 | <?php |
漏洞的主要原因在于不同的引擎对于竖杠’ | ‘的解析产生歧义。
对于php_serialize引擎来说’ | ‘可能只是一个正常的字符;但对于php引擎来说’ | ‘就是分隔符,前面是$_SESSION[‘username’]的键名 ,后面是GET参数经过serialize序列化后的值。从而在解析的时候造成了歧义,导致其在解析Session文件时直接对’ | ‘后的值进行反序列化处理。
可能有的人看到这里会有疑问,在使用php引擎读取Session文件时,为什么会自动对’ | ‘后面的内容进行反序列化呢?也没看到反序列化unserialize函数。
这是因为使用了session_start()这个函数 ,看一下官方说明:https://www.php.net/session_start/
可以看到PHP能自动反序列化数据的前提是,现有的会话数据是以特殊的序列化格式存储。
明白了漏洞的原理,也了解了反序列化漏洞的位置,现在来构造payload:
poc
1 | <?php |
O:4:"user":2:{s:4:"name";s:7:"JGwebre";s:3:"age";s:3:"888";}
这里就是php 是以 key|value 的方式存储读取的,而 php_serialize 不是以这种方式写入读写,有差异性,所以造成了反序列化
成功触发了user类的魔术方法__wakeup()
,结合POP反序列化链就可以造成一些其他的漏洞。
但这种方法是在可以对$_SESSION
进行赋值的情况下实现的,那如果代码中不存在对$_SESSION
变量赋值的情况下又该如何利用?
不存在对$_SESSION变量赋值
在PHP中还存在一个upload_process机制,即自动在$_SESSION中创建一个键值对(key:value),value中刚好存在用户可控的部分,可以看下官方描述的,这个功能在文件上传的过程中利用session实时返回上传的进度。
更多细节请参考:http://php.net/manual/zh/session.upload-progress.php
从上面的大概描述大概得知此漏洞需要session.upload_progress.enabled为on,在上传文件的时候同时POST一个与session.upload_process.name的同名变量。后端会自动将POST的这个同名变量作为键进行序列化然后存储到session文件中。下次请求就会反序列化session文件,从中取出这个键。所以漏洞的根本原因还是使用了不同的Session处理引擎。
来看一道Jarvis OJ 平台的 PHPINFO 题目
环境地址:http://web.jarvisoj.com:32784/
index.php
1 | <?php |
通过index.php代码可以得知:
1. 是使用php的引擎来读取Session。
2. 如果存在GET方式传递进来的参数,就实例化Oowo类的对象,就会自动调用构造函数__construct()
,将phpinfo()
赋值给变量$mdzz,在程序结束的时候调用析构函数__destruct()
通过eval执行$mdzz,说白了就是随便传一个参数,就可以看到php探针。
直接传入?phpinfo=1 看到phpinfo
通过读取php探针文件发现了两个比较重要的信息:
1. 默认的Session存储引擎为php_serialize,但是index.php告诉我们Session读取使用的是php引擎,因为反序列化和序列化使用的处理器不同,由于格式的原因会导致数据无法正确反序列化,那么就可以通过构造伪造任意数据。
2. index.php代码中虽然没有对$_SESSION变量赋值,但是session.upload_progress.enabled 为 On。符合使用upload_process机制对变量$_SESSION赋值,并结合上面的Session反序列化来构造利用。
session.upload_progress.name 为 PHP_SESSION_UPLOAD_PROGRESS,可以本地创建 up_sess.html,一个向 index.php 提交 POST 请求的表单文件,其中包括PHP_SESSION_UPLOAD_PROGRESS 变量。
up_sess.html
1 | <form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data"> |
我们抓上传包
poc
1 | <?php |
发现flag所在php文件
查看根目录的路径
读取flag文件:
Session反序列化POP链构造
注:以下例子在本地搭建,需要在php.ini中对以下选项进行配置:
1 | session.auto_start = Off //默认为0 不开启 |
session.auto_start = on 表示PHP在接收请求的时候会自动初始化Session,不再需要执行session_start()。
session.serialize_handler = php_serialize 表示默认使用php_serialize引擎进行存储。
session.upload_progress.cleanup = On 导致文件上传后,Session文件内容立即清空,这个时候就需要利用时间竞争,在Session文件内容清空前进行包含利用。
前期为了演示反序列化效果,暂时将这个选项关闭Off,后面会打开来展示利用条件竞争Session反序列化rce。
class.php
1 | <?php |
index.php
1 | <?php |
通读class.php文件,发现漏洞点在于可以通过调用foo3类中的eval方法造成命令执行漏洞。这是一个类的普通方法,要让这个方法执行,需要构造一个POP链。
这个链子很简单就不分析了
1 | <?php |
再来分析一下index.php文件:
1. 发现使用php引擎来读取Session文件,而系统默认是使用php_serialize引擎来存储Session, 通过不同引擎的差异解析就可以反序列化rce。
2. 文件直接require(class.php),并且紧接着实例化一个foo1类的对象,这意味着使用php引擎解析完Session文件,反序列化payload直接就可以rce。
本地创建 up_sess.html,一个向 index.php 提交 POST 请求的表单文件,其中包括PHP_SESSION_UPLOAD_PROGRESS变量。
1 | <form action="http://127.0.0.1/index.php" method="POST" enctype="multipart/form-data"> |
这里不知道为啥一直成功不了,换了php版本也不行,通过phpinfo查看配置,也确实修改过去了,但是些不进去session文件!!!!