UMCS CTF 2025
Jeopardy Writeup Qualifier - SCORP10N
Last updated
Jeopardy Writeup Qualifier - SCORP10N
Last updated
The event featured challenges in Web, Pwn, Reverse Engineering, Steganography, and Forensics. After a long break from CTFs, this was a great chance to push my limits, take on some tough problems, and reassess where I stand in terms of skills and problem-solving.
In this writeup, I will focus on Web Exploitation.
Description: “I left my hopes_and_dreams on the server. can you help fetch it for me?.”
Link/File: https://github.com/umcybersec/umcs_preliminary/tree/main/web-healthcheck
Points: Dynamic (216 – 43solved)
Solve by : smallcurl
FLAG: umcs{n1c3_j0b_ste4l1ng_myh0p3_4nd_dr3ams}
We are given only one file name as index.php
which contains both PHP and HTML components.
Upon analysing the source code, we can see that the most common command injection techniques are effectively blocked. The usage ofshell_exec
is handled with certain precautions to prevent direct exploitation.
The key issue lies in the command construction. Although basic injection vectors are blocked, the script allows curl
to fetch arbitrary resources, which can be abused to read local files.
By leveraging curl --data @<file>
syntax, we can attempt to read arbitrary files on the server. However, there’s a limitation: the command appends a grep -sF
at the end, which suppresses the output and only returns the status code.
This way, instead of relying on the command's direct output, we can exfiltrate the contents of the file to our webhook.
We can use any file path with the--data @<file>
structure. Once the request is triggered, the file content will be sent to the webhook and can be viewed externally.
Description: “Test out our game center. You'll have free claiming bonus for first timers!
Author: vicevirus
Link/File: straightforward_player.zip
Points: Dynamic (412)
Solve by : smallcurl
FLAG: UMCS{th3_s0lut10n_1s_pr3tty_str41ghtf0rw4rd_too!}
We are provided with a zip file named straightforward_player.zip, which contains the full source to this application.
Starting with the analysis of the app.py file, since this was the main file in Python.
· GET /claim: Increases balance by 1000 coins but is limited to once per day.
· GET /buy_flag: Allows purchasing the flag if the balance is ≥ 3000.
The problem was, we could only claim once a day, but the flag price is 3000, which makes us kind of short.
The challenge lies in the daily restriction enforced on the /claim endpoint. Since we can only claim once per day, it seems impossible to reach the required 3000 coins needed for /buy_flag
in a short timeframe.
The vulnerability arises because the application increases the user's balance, but there is a delay before the updated state is written to the database. This delay creates a critical window where multiple concurrent requests to /claim
can be made, all referencing the original balance.
By sending multiple simultaneous requests to /claim
, we can effectively bypass the "once-a-day" restriction. This technique is known as a Race Condition.
With just 3 successful race wins, we increased the balance from 0 → 3000, allowing us to access /buy_flag and retrieve the flag.
I have made a simple microservices application. Seperation of concerns at its finest!
Author: vicevirus
Flag format: UMCS{…}
Link: http://microservices-challenge.eqctf.com:7777/api/quotes
Doesn't manage to solve during competition, but I think this was a very good challenge that is applicable to real life.
Thanks to benkyou and vicevirus for helping me solving this challenge.
In this challenge, we are provided with the source code for the application. The system architecture consists of three main components:
A frontend proxy built with Node.js that serves as the user-facing interface.
Two backend services:
A Quotes API, which the frontend interacts with.
A Flag API, which is accessible only internally via Nginx.
The key detail is that the frontend proxy only communicates directly with the Quotes API, while the Flag API is isolated and intended to be unreachable from the public interface.
During local testing with Docker, we can retrieve the flag by making a direct request to http://127.0.0.1:5555/flag
. However, attempting the same request on the live challenge server does not succeed.
Given that external access is limited to the frontend proxy, the first idea that came to mind was exploiting a Server-Side Request Forgery (SSRF). Unfortunately, the application doesn't accept any user-controlled input that could be used to trigger such behavior. I also explored the possibility of HTTP request smuggling, but after a deep dive, it became clear that this technique does not apply to the current setup.
Upon reviewing the Nginx configuration files, the following block stood out as potentially significant:
Digging deeper, we observed that the proxy is configured to accept connections from Cloudflare's IP address ranges, which are commonly used for traffic routing and protection. This presents a potential avenue for bypassing the restriction: if a request appears to originate from one of Cloudflare's trusted IPs, Nginx will allow it through to the backend.
Initially, I attempted to use DNS Rebinding and Re-route VPS to Cloudflare, hoping it would route requests through the allowed address space. However, the technique uses a different IP range that was not included in the allowlist, so that method didn’t work.
The solution was to leverage Cloudflare Workers—a serverless platform that runs on Cloudflare's infrastructure and inherently uses IPs from their trusted pool. By crafting a Cloudflare Worker to send a request to the Flag API endpoint, I was able to successfully retrieve the flag.
To bypass this limitation, we can use an Out-Of-Band (OOB) technique, such as sending the content to an external webhook (like ).