PHP的session机制
在学习 session 反序列化之前,我们需要了解这几个参数的含义。
Directive | 含义 |
---|---|
session.save_handler | session保存形式。默认为files |
session.save_path | session保存路径。 |
session.serialize_handler | session序列化存储所用处理器。默认为php。 |
session.upload_progress.cleanup | 一旦读取了所有POST数据,立即清除进度信息。默认开启 |
session.upload_progress.enabled | 将上传文件的进度信息存在session中。默认开启。 |
我们先通过一个样例代码,看看3种不同的 session 序列化处理器处理 session 的情况。
1 |
|
当 session.serialize_handler=php 时,session文件内容为: name|s:7:"mochazz";
当 session.serialize_handler=php_serialize 时,session文件为: a:1:{s:4:"name";s:7:"mochazz";}
当 session.serialize_handler=php_binary 时,session文件内容为: 二进制字符names:7:"mochazz";
再来看看 session.upload_progress.enabled 。
当 session.upload_progress.enabled INI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态。
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。 当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是session.upload_progress.prefix 与 session.upload_progress.name连接在一起的值。
通常这些键值可以通过读取INI设置来获得,例如:
1 |
|
更多细节请参考:http://php.net/manual/zh/session.upload-progress.php
如果想看上面说的这个存在 $_SESSION 数组中的信息,我们需要先将 session.upload_progress.cleanup 设置成 Off 。
接下来我们通过两个CTF题目来学习 session 反序列化问题。
例题一
可以看到题目环境中的 session.serialize_handler 默认为 php_serialize 处理器,而程序使用的却是 php 处理器,而且开头 第4行 使用了 session_start() 函数,那么我们就可以利用 session.upload_progress.enabled 来伪造 session ,然后在 PHP 反序列化 session 文件时,还原 OowoO 类,最终执行 eval 函数。
1 |
|
我们可以通过如下表单,抓包修改 filename 为 payload 即可。
1 | <form action="http://localhost/demo.php" method="POST" enctype="multipart/form-data"> |
最终会生成一个 session 文件, php 在获取 session 的时候,会按照 session.serialize_handler=php 规则来处理 session 文件,将 | 符号之前的所有内容认为是键名,之后的内容则用于反序列化。
在程序运行结束时,调用 OowoO 类的 __destruct 方法,最终触发代码执行操作。
例题二
在 index.php 文件中,看到 第5行 的 call_user_func 方法,其参数二的位置固定为 $_POST 数组,我们很容易便想到利用 extract 函数进行变量覆盖,以便配合后续利用。 第6-8 行代码又存在 session 伪造漏洞,我们可以考虑是否可以包含 session 文件或者利用 session 反序列化漏洞。 第12行 的 call_user_func 函数的第一个参数虽然被固定为 implode ,但是我们可以通过前面的 extract 函数进行变量覆盖(注意:在PHP7.x比较新的版本中,已经不允许动态调用 extract 函数了)。而 flag.php 文件中告诉我们,只有 127.0.0.1 请求该页面才能得到 flag ,所以这明显又是考察 SSRF 漏洞,这里我们便可以利用 SoapClient 类的 __call 方法来进行 SSRF 。
下面我们通过一个例子,来看看 SoapClient 类的 __call 方法如何使用。
1 |
|
由于 PHP 中的原生 SoapClient 类存在 CRLF 漏洞,所以我们可以伪造任意 header 信息,上面的请求结果如下:
而 call_user_func 函数中的参数可以是一个数组,数组中第一个元素为类名,第二个元素为类方法,例如:
1 |
|
这样题目中的 call_user_func($b,$a) 就可以变成 call_user_func(‘call_user_func’,array(‘SoapClient’,’welcome_to_the_lctf2018’)) ,即调用 SoapClient 类不存在的 welcome_to_the_lctf2018 方法,从而触发 __call 方法发起 soap 请求进行 SSRF 。
这里还要提及一点的是 session_start 函数从 PHP7 开始允许通过参数来设置 session 运行时配置。例如: session_start(array('serialize_handler' => 'php_serialize'))
将会设置 session.serialize_handler=php_serialize 。
最终我们的利用方法如下:
1 |
|
最后我们再将 PHPSESSID 修改成 mochazz 即可获得 flag ,整个攻击的流程图如下:
例二题目环境可以从 这里 下载。