🖊️Author: DJumanto 🛡️Team: proof by feeling 📖Note: This Writeup contains solver for SelfLove challenge in NCW CTF 2025
TL;DR
This write-up explains how we can escalate a self‑XSS, combined with CSRF, into stealing information from other users.
Summary
Vulnerabilities: Cross Site Scripting (XSS), CSRF.
Target: Read the flag in high privilege user endpoint
The App
In this challenge, we’re given a web application with some functionalities, where we able to register, login, and report admin, an obvious client side challenge.
from flask import Flask, request, g, render_template, flash, session import sqlite3 import os from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.chrome.options import Options from flask_limiter import Limiter from flask_limiter.util import get_remote_address import time import threading
defget_db(): """Return a DB connection for this request, creating it if needed.""" if"db"notin g: g.db = sqlite3.connect(DATABASE, timeout=30) g.db.row_factory = sqlite3.Row return g.db
@app.route('/flag') defflag(): print(f"{session.get('username','Guest')} is trying to access /flag") if session.get('admin'): return FLAG return"kamu siapa bang?"
@app.route('/report', methods=['GET', 'POST']) @limiter.limit("5 per 10 minutes", methods=["POST"]) defreport(): if request.method == 'GET': return render_template('report.html') url = request.form['url'] ifnot url.startswith("http://") andnot url.startswith("https://"): return"Only http and https protocol allowed.", 400 threading.Thread(target=run_bot, args=(url,)).start() returnf"Report submitted! Our bot will visit url shortly."
definit_db(): with app.app_context(): db = get_db() cur = db.cursor() cur.execute( """ CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, admin BOOLEAN NOT NULL DEFAULT 0 ) """ ) db.commit() if __name__ == '__main__': init_db() app.run(host='0.0.0.0', port=40111, ssl_context='adhoc')
The target is to read the flag in the /flag endpoint where only admin can visit it.
Self XSS
There are several clear HTML injection points, both of which appear in the content returned to the user after the authentication process:
However, the injection that happens right after registration isn’t very useful, since the page immediately redirects via a meta tag. The one that appears after logging in is much more useful, though, because we can simply comment out the <meta> tag using the username below.
However, because the CSP sets default-src ‘self’, our stored payload has to come from the same origin. To work around that, we can register a user whose username acts as both JavaScript and HTML. The trick is to turn the Hello string into an arrow function, so our username ends up looking like this:
Great now we have XSS… but stil a self-XSS, how do we escalate it so we can compromise the victim?
CSRF Attack
During the login process, we can see that the application doesn’t enforce CSRF protection. That means we can create a page containing a credentialles iframe, then trick the user into visiting it so they log in as the attacker with our payload. The iframe won’t use the parent’s cookies, though. So the question is: “How do we actually get the flag?” If we look closely at the CSP configuration:
The cookie’s SameSite value is set to None, which means it can be sent across different origins. That’s a good sign, because now we can create another iframe that uses the admin’s original cookies. Since both iframes share the same origin, iframe 1 can access properties inside iframe 2. Here’s a visual breakdown of the plan:
Since there’s no endpoint that reflects the cookies in the response, our only option is to extract the flag directly from the page’s body. We can do that easily using window.top[1].document.body.innerText. So here’s my final XSS payload: