APISEC CTF 2025 - Writeup

APISEC CTF 2025 - Writeup

in

Hello everyone,

APISEC CTF 2025 - One Request to Rule Them All just finished and I decided to do this write-up and record a video walkthrough. I ended up ranking 6th place although I only started the CTF 15 hours after it had already started, so I guess it was a good result all things considered.

Thanks to APISEC University https://www.apisecuniversity.com and all the staff for having put this CTF together, I had a blast!

Table of Contents:

Video


Challenges Introduction

Word has reached us of a secret meeting — a gathering of warlords, Númenóreans, and other remnants of the Enemy’s forces. Their purpose? The exchange of a relic long thought lost: the Moonstone of Minas Ithil.

Before the city fell to the Nazgûl and became Minas Morgul, its craftsmen created this stone, embedding it with knowledge of the city’s original enchantments — wards against darkness, secrets of its foundations, and perhaps even hidden paths now buried beneath Morgul Vale. If this artifact falls into the wrong hands, its knowledge could be twisted to strengthen the stronghold of evil or unlock ancient defenses best left forgotten. We cannot let that happen.

The meeting is set in a location too secure for us to intervene. But villains are cautious and paranoid creatures. They trust no one, not even each other. That is where you come in.

Your Task:
• Ensure the meeting does not happen where planned. Their own paranoia can be used against them. With the right deception, they will move it themselves.
• Guide them — unknowingly — to a location of our choosing. Somewhere we have the advantage, where we can seize the Moonstone before it disappears into shadow.
• Be ready when they arrive. The moment must be right. If they suspect treachery too soon, they will scatter into the night, and the Moonstone will be lost.

A single misstep, a word out of place, and all will unravel. But you are not alone. Others work in the shadows — some with blades, others with wits. If you accept, know this: there is no glory in this job, only risk. But if you succeed, you will not only deny the Enemy a powerful relic — you may uncover knowledge long thought lost, a weapon against the darkness growing in the East.

So, will you take the job?

Start by making an account on https://one-request.malteksolutions.com.

Tip: If utilizing Postman, a custom-tailored postman.json is available to offer the best experience. You can find it at https://one-request.malteksolutions.com/postman.json.

Write-up

1 - The Shadow Artisan’s Mark (50 Points)

Objective: Discover Celebrimbor’s User ID on the platform to infiltrate their communications.

Solution

First step, register an account with endpoint /register:

I already started by trying a possible cheeky mass assignment vuln

Get a token using the endpoint /token:

There is an endpoint to fetch all users at /v2/users/ although we don’t seem to have the right permission to access it:

Looking for endpoints that we can access, we end up finding /v2/locations/?page=1&size=100, which discloses names as well as the user_id:

Searching for the word “Celebrimbor” in the response we are presented with Celebrimbor’s user_id:

Solution: ccb14650-5388-4d90-abcb-df0f388817c3

2 - The Forge-Master’s Circle (100 Points)

Objective: Identify the specific Group ID owned by Celebrimbor to map the network of allies involved in the exchange.

Solution

We need to find the group_id owned by Celebrimbor, we just got his user_id from the previous challenge so we probably need to use it in some other endpoint.

Continuing to explore the endpoints available to us, we come across the endpoint /v2/locations/:location_id/activities?page=1&size=100 where :location_id would be replaced by a location_id.

Grabbing the location_id disclosed on the screenshot from the previous question (7e63c222-fa15-4e47-ae2f-e77d27a1a8ce) and replacing it in the request below, we are presented with a total of 2474 records.

The objective states that the group was created by “Celebrimbor”, so searching for his user_id we can maybe find an entry created by him.

Searching for his user_id recursively for all the pages from 1 to 25 as seen in the screenshot above, leads us to page 25 where we will find the entry below:

Solution: 737530c6-7980-42d7-8c8f-9ace9949dfba

3 - Whispers in the Dark (150 Points)

Objective: Uncover the hidden Activity ID associated with the secret meeting about the Moonstone.

Solution

This time we are looking for an activity_id, associated with a secret meeting.

Since the endpoint from the challenge number 2 (The Forge-Master’s Circle) is returning the activities, the solution from the 2nd challenge already discloses this value.

Solution : fd78acfc-7839-4fee-b654-31a6209e4cb0

4 - Forging the Seeing Stone’s Key (200 Points)

Objective: Create a token imbued with the PALANTIR role to gain administrative access to their systems.

Solution

In my opinion, this was the hardest challenge in this CTF.

Analyzing the OpenAPI spec, it is possible to observe that there are some “Legacy” endpoints.

So far, I have only been using the /v2/ endpoints for the first three challenges.

This challenge mentions about forging a token, my first thought was trying to manipulate the JWT. I tried pretty much everything and I ran out of options. Which led me to move to the authentication flow.

Analyzing the /token endpoint, I couldn’t help but notice the x-api-version header. My spider senses were tingling telling me this might be the path forward.

Trying to change the value of x-api-version to v1 instead of the default v2, leads to the following response:

Reading the error message, I can understand that our role (user) cannot request this token.

raise ApiVersionException(user.role, requested_api_version=x_api_version, user_api_version=ApiVersion.V2)\none_request.exceptions.ApiVersionException: user is not permitted to use this API version\n","role":"user","requested_api_version":"v1","allowed_api_version":"v2"}

Analyzing the rest of the endpoints, I noticed that by deleting my own account using the endpoint /v2/users/:user_id, where the user_id would be my user_id, my role changed from user to deleted.

Since our user role is not user anymore, I can try to change the x-api-version to v1 now.

Although, by replacing the JWT with the one provided, and trying to make request to legacy endpoints, it just doesn’t work and throws an error:

This error gives us a very important piece of information. This endpoint is not expecting the v1 api version. It is expecting the legacy api version.

Repeating the process of changing the x-api-version but instead of v1 to legacy also gives us a JWT token.

Performing a request to a legacy endpoint such as /support/summary with the new JWT from the screenshots above, the error goes away and we get some data in the response:

Scrolling through the data, something caught my attention. A support ticket with the title [ADMIN] Urgent: Orc Raid Relocation Request

I guess this could be very useful, reading a support ticket from an admin can contain juicy information, so I need to look for a way to do that.

Verifying the rest of the support endpoints, there is a GET request to support tickets, a PUT request to add a new message and a DELETE request to delete a ticket as can seen below:

The GET request just takes an integer in the request_id parameter. Bruteforcing this id from 0 to 100 leads to nothing. The response would always be:

HTTP/2 404 Not Found
Date: Fri, 28 Feb 2025 21:46:16 GMT
Content-Type: application/json
Content-Length: 37
Strict-Transport-Security: max-age=31536000; includeSubDomains

{"detail":"SupportRequest not found"}

Which led me to move on the PUT request. This one takes a request_id as an integer and a message parameter. Trying a valid request leads to information disclosure as seen below:

I can now read the messages! I just need to find the correct request_id to access the [ADMIN] Urgent: Orc Raid Relocation Request support ticket. Trying the request_id number 9, gives us the correct ticket:

By reading all the messages, it is possible to find a new endpoint mentioned that was not available through the documentation - /palantir/glimpse:

Hitting the /palantir/glimpse with a GET request gives us a 405 Method now allowed and the information that a POST request is allowed:

Solution:: JWT from /palantir/glimpse endpoint.

uff, that was tough!!

5 - The Key of Passage (250 Points)

Objective: Use your newly forged PALANTIR token to uncover the original activity’s invite code before your access expires.

Solution

Reading the the messages that we were able to return from the previous challenge, there is also another new endpoint leaked /palantir/groups/:id/chats

I remember that we already had a group_id from a previous challenge answer. Replacing the :id in this new endpoint with the one from the challenge number 2, gives us the entire chat history:

Scrolling through the messages we can see the secret code onerequest{melon}:

Solution : onerequest{melon}

6 - The Trap’s Staging Ground (250 Points)

Objective: Locate the perfect ambush site that meets our strategic requirements.

Solution

As seen in the screenshot from challenge 2:

The description points to a location related to artifacts.

Using the endpoint to fetch all locations /v2/locations/?page=1&size=100 and searching for the word artifact gives us a single match and guess who left a review for this location? CELEBRIMBOR!!

7 - One Request to Rule Them All (1000 Points)

Objective: Craft the ultimate request that will redirect the meeting and set our trap for the Moonstone exchange.

Solution

This challenge is about gluing all the pieces together.

We need all we gathered so far for the previous challenges:

PARAMETERS:

  • group_id: 737530c6-7980-42d7-8c8f-9ace9949dfba
  • activity_id: fd78acfc-7839-4fee-b654-31a6209e4cb0
  • location_id: 1273b3dd-34c0-421e-9c3c-8d79f67742ab

BODY:

  • invite_code: onerequest{melon}
  • user_id: ccb14650-5388-4d90-abcb-df0f388817c3 (Celebrimbor’s id)

AUTHORIZATION:

  • PALANTIR role JWT

Sending the request with all the above information gives us the final flag:

Solution: onerequest{osgiliath_passages_protect_us}