PHP session反序列化漏洞解析
PHP session反序列化漏洞解析
PHPsession反序列化漏洞,是当序列化存储Session数据与反序列化读取Session数据的方式不同时产生的
一、什么是session
官方Session定义:在计算机中,尤其是在网络应用中,称为“会话控制”。Session对象存储特定用户会话所需的属性及配置信息。主要有以下特点:
session保存的位置是在服务器端
session通常是要配合cookie使用
因为HTTP的无状态性,服务端产生了session来标识当前的用户状态。一旦开启了 session 会话,便可以在网站的任何页面使用或保持这个会话,从而让访问者与网站之间建立了一种“对话”机制。不同语言的会话机制可能有所不同,这里仅讨论PHP session机制。
本质上,session就是一种可以维持服务器端的数据存储技术。即**session技术就是一种基于后端有别于数据库的临时存储数据的技术**
PHP session可以看做是一个特殊的变量,且该变量是用于存储关于用户会话的信息,或者更改用户会话的设置,需要注意的是,PHP Session 变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的,且其对应的具体 session 值会存储于服务器端,这也是与 cookie的主要区别,所以seesion 的安全性相对较高。
二、PHP session工作流程
PHP脚本使用 session_start()时开启
session会话,会自动检测PHPSESSID- 如果
Cookie中存在,获取PHPSESSID - 如果
Cookie中不存在,创建一个PHPSESSID,并通过响应头以Cookie形式保存到浏览器
- 如果
初始化超全局变量
$_SESSION为一个空数组PHP通过
PHPSESSID去指定位置(PHPSESSID文件存储位置)匹配对应的文件- 存在该文件:读取文件内容(通过反序列化方式),将数据存储到
$_SESSION中 - 不存在该文件: session_start()创建一个
PHPSESSID命名文件
- 存在该文件:读取文件内容(通过反序列化方式),将数据存储到
程序执行结束,将
$_SESSION中保存的所有数据序列化存储到PHPSESSID对应的文件中
有时候浏览器用户设置会禁止 cookie,当在客户端cookie被禁用的情况下,php也可以自动将session id添加到url参数中以及form的hidden字段中,但这需要将php.ini中的session.use_trans_sid设为开启,也可以在运行时调用ini_set来设置这个配置项。
具体原理图:

php.ini session配置
php.ini里面有较重要的session配置项
1 | |
三、PHP session 的存储机制
上文中提到了 PHP session的存储机制是由session.serialize_handler来定义引擎的,默认是以文件的方式存储,且存储的文件是由sess_sessionid来决定文件名的,当然这个文件名也不是不变的,如Codeigniter框架的 session存储的文件名为ci_sessionSESSIONID,如下图所示:

session.serialize_handler定义的引擎有三种,如下表所示:
| 处理器名称 | 存储格式 |
|---|---|
| php | 键名 + 竖线 + 经过serialize()函数序列化处理的值 |
| php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值 |
| php_serialize | 经过serialize()函数序列化处理的数组 |
注:自 PHP 5.5.4 起可以使用 *php_serialize*
上述三种处理器中,php_serialize在内部简单地直接使用 serialize/unserialize函数,并且不会有php和 php_binary所具有的限制。 使用较旧的序列化处理器导致$_SESSION 的索引既不能是数字也不能包含特殊字符(| 和 !) 。
下面我们实例来看看三种不同处理器序列化后的结果。
php 处理器
首先来看看session.serialize_handler等于 php时候的序列化结果,demo 如下:
1 | |

(图从大佬博客中所拿)
序列化的结果为:session|s:7:"xianzhi";
session 为$_SESSION['session']的键名,|后为传入 GET 参数经过序列化后的值
php_binary处理器
再来看看session.serialize_handler等于 php_binary时候的序列化结果。
demo 如下:
1 | |
为了更能直观的体现出格式的差别,因此这里设置了键值长度为 35,35 对应的 ASCII 码为#,所以最终的结果如下图所示:

序列化的结果为:#sessionsessionsessionsessionsessions:7:"xianzhi";
#为键名长度对应的 ASCII 的值,sessionsessionsessionsessionsessions为键名,s:7:"xianzhi";为传入 GET 参数经过序列化后的值
php_serialize 处理器
最后就是session.serialize_handler等于 php_serialize时候的序列化结果,同理,demo 如下:
1 | |

序列化的结果为:a:1:{s:7:"session";s:7:"xianzhi";}
a:1表示$_SESSION数组中有 1 个元素,花括号里面的内容即为传入 GET 参数经过序列化后的值
四、利用漏洞
漏洞成因
session的反序列化漏洞,就是利用php处理器和php_serialize处理器的存储格式差异而产生,通过具体的代码我们来看下漏洞出现的原因
首先创建session.php,使用php_serialize处理器来存储session数据
1 | |
test.php,使用默认php处理器来存储session数据
1 | |
接着,我们构建URL进行访问session.php:
1 | |

打开PHPSESSID文件可看到序列化存储的内容
1 | |

漏洞分析:
在
session.php程序执行,我们将|O:4:"f4ke":1:{s:4:"name";s:10:"phpinfo();";}通过php_serialize处理器序列化保存成PHPSESSID文件;由于浏览器中保存的
PHPSESSID文件名不变,当我们访问test.php,session_start();找到PHPSESSID文件并使用php处理器反序列化文件内容,识别格式即
键名 竖线 经过 serialize() 函数反序列处理的值 a:1:{s:7:”session”;s:45:” | O:4:”f4ke”:1:{s:4:”name”;s:10:”phpinfo();”;} php处理器会以|作为分隔符,将
O:4:"f4ke":1:{s:4:"name";s:10:"phpinfo();";}反序列化,就会触发__wakeup()方法,最后对象销毁执行__destruct()方法中的eval()函数,相当于执行如下:
1
2$_SESSION['session'] = new f4ke();
$_SESSION['session']->name = 'phpinfo();';
我们访问test.php,即可直接执行phpinfo()函数

在这里通过一个具体的题目来看
[HNCTF 2022 WEEK4]unf1ni3hed_web3he1

进来让你传cmd参数,发现一传就是302跳转,抓包得到

访问/Rea1web3he11.php,得到
然后就有点脑洞大开了,要访问/t00llll.php,得到一个工具

说真正webshell在/Rea1web3he11.php,那用这个得到/Rea1web3he11.php源码,payload为?include_=php://filter/convert.base64-encode/resource=Rea1web3he11.php,源码base64解码后如下
1 | |
因为有ini_set('session.serialize_handler', 'php');,可以看到,这里使用 PHP 处理器,又有session_start();所以这个就是考察php session反序列化,那我们如何将控制session的注入呢?
这里需要用到这个配置
session.upload_progress.enabled= On --启用上传进度跟踪,并填充$ _SESSION变量,默认启用,这就意味着我们在上传文件的时候,当请求中包含PHP_SESSION_UPLOAD_PROGRESS字段,PHP 会自动将该字段的值作为$_SESSION的键名,并在这个键名下存储上传进度数据(如文件名、已上传大小等)。
等上传结束后,
$_SESSION的键名与键名下存储上传进度数据都会保存到存储介质(通常是服务器的临时文件,如/tmp/sess_<PHPSESSID>),并在这个过程中进行序列化。
而这个
/tmp/sess_<PHPSESSID>文件中的在session_start();的时候会又反序列化成为$_SESSION变量,这时候如果在PHP_SESSION_UPLOAD_PROGRESS字段中写入恶意payload被当作$_SESSION的键名话,反序列化就会执行恶意payload。
同时,在
session.upload_progress.enabled= On --启用上传进度跟踪,并填充$ _SESSION变量,默认启用启动的时候,session.upload_progress.cleanup= On --读取所有POST数据(即完成上传)后立即清理进度信息,默认启用也是默认启动的,上传后会立即清理掉,session会立刻被清空覆盖,所以这里就需要使用条件竞争来将session给覆盖掉
这里再引用一下别人博客中的解释
1 | |
原理在上面已经说过了,下面就直接给出exp吧
1 | |
然后就有flag了
参考博客
https://www.freebuf.com/articles/web/324519.html