📌 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 live chat được xây dựng sử dụng WebSocket. Tin nhắn mà bạn gửi đi được nhìn thấy bởi một nhân viên hỗ trợ trong thời gian thực.
Mục tiêu: Để giải bài này, dùng tin nhắn WebSocket để khiến cho thông báo alert()
hiện lên trong trình duyệt của người kia.
Giới thiệu
Bài này thì yêu cầu PoC nghe “rất là” XSS nhưng lại là tấn công giao thức WebSocket; và không phải tự dưng mà tôi làm bài lab này – có một bài lab kết hợp WebSocket với CSRF (tấn công Cross-site WebSocket Hijacking – CSWSH) nên tôi phải đi học chủ đề này.
Tôi rút ra một đặc điểm phân biệt các bài lab được gán nhãn “Practitioner” với các bài “Apprentice” ở chỗ là đôi khi bạn phải kết hợp nhiều kỹ thuật với nhau thì mới có thể “bem” người khác.
Đấy là tôi nói bài lab CSWSH trên kia, chứ bài WebSocket này là bài đầu tiên, cũng chỉ là “Apprentice” đơn giản thôi.
WebSocket là gì?
WebSocket là một công nghệ cung cấp kết nối hai chiều giữa máy khách và máy chủ qua một kết nối duy nhất, giúp cho việc trao đổi dữ liệu trở nên dễ dàng hơn.
Điều này khác biệt với HTTP, một giao thức mà trình duyệt thường sử dụng để giao tiếp với máy chủ, mà chỉ hỗ trợ kết nối một chiều.

Để khởi tạo một kết nối WebSocket, trình duyệt và máy chủ thực hiện bắt tay nhau qua giao thức HTTP sẵn có. Trình duyệt của bạn sẽ gửi một yêu cầu kiểu như thế này:

Dòng cuối cùng chính là yêu cầu “nâng cấp” kết nối HTTP hiện tại lên thành WebSocket.
Nếu phía máy chủ chấp thuận, nó sẽ gửi lại một cái bắt tay như sau:

WebSocket được thiết kế để vượt qua những hạn chế của HTTP và cung cấp một giao tiếp thời gian thực hiệu quả cho các ứng dụng web. Giao thức này giúp tăng cường khả năng tương tác và tốc độ truyền dữ liệu giữa máy chủ và trình duyệt, điều này rất hữu ích cho các ứng dụng web yêu cầu truyền dữ liệu thời gian thực như trò chơi, chat trực tuyến, hoặc các dịch vụ streaming.
Phân tích
Vì là câu chuyện thay đổi kết nối, nên ý tưởng đầu tiên của tôi là đi inspect xem liệu có phần code nào xử lý việc bắt tay này ở phía client hay không.
Nếu như có phần nào đó nó cố gắng ngăn chặn tôi khiến cho alert()
xuất hiện, tôi sẽ cần phải nghĩ đến việc bypass điều này bằng việc “chọc ngoáy” request như thường lệ.
Lời giải
- Xem mã nguồn của trang. Đọc thấy lab này có vài hàm hay ho hơn mọi khi:
(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 ---"); }; }); } })();
- Hàm
htmlEncode()
, thứ sẽ phá bung bét những dấu ngoặc của tôi nếu tôi định inject một câu lệnh nào đó. - Hàm
openWebSocket()
, thiết lập một kết nối WebSocket đến máy chủ cho tính năng Live chat.
Bạn cũng có thể kiểm chứng sự tồn tại của WebSocket từ tab “Network” trong công cụ Inspector của trình duyệt. Tôi tìm thấy một sự thật khá hay ho là những Lab Header của Portswigger cũng dùng phương thức này.
2. Thử gửi mã ở trong tin nhắn:
<img src=1 onerror=alert(1)>
Và nhận thấy nó đúng là đã bị cái hàm htmlEncode()
kia phá:

3. Dăm ba cái hàm client-side vớ vẩn. Gửi request này sang Repeater để khôi phục lại dấu ngoặc của mình và gửi lại:

Lưu ý: Trong quá trình làm bài này cái LabHeader chuối kia nó đã không chịu ghi nhận thành quả của tôi. Mặc dù tôi làm chuẩn chỉ không khác gì cái video bài giải của nó:
Nếu như bạn cũng bị như thế này, bạn có thể gửi request theo kiểu “man in the middle” từ Burp Proxy trong chế độ vẫn bật Intercept. Lấy được cái tin WebSocket rồi thì sửa lại dấu ngoặc và forward tiếp. Tôi thử lại như vậy mới được tính.
