APISEC-CON CTF May 2025 - Write-ups

APISEC-CON CTF May 2025 - Write-ups

in

Hello everyone,

APISec University had yet another APISEC-CON on the 21st of May 2025, followed by a CTF competition, which I was happy to play and ended up ranking 4th place.

It was a fun CTF, albeit not their best one to date, had way too many rate-limiting bypass challenges, which I don’t enjoy exploring too much, some of them just straight away annoying. Regardless, I decided to do the write-ups for the challenges, which you can see below.

Table of Contents:

Shadow APIs:

Injection Junction:

Slash and Dash:

BugBountyHub:

Massive:

Permission Slip:

Rate My API:


Shadow APIs

Undocumented (50 Points)

Solution:

Visiting the page:

Create an account and login leads to:

Analyzing the website source code there is api.min.js and app.min.js:

Visiting api.min.js file leads to a DEBUG_TOKEN disclosure as well as the /debug endpoint:

Replacing my JWT with the disclosed JWT and visiting /api/debug leads to the flag.:

Flag: flag{h1dd3n_3ndp01nt5_4r3_n0t_s3cur3}


API Obscura (100 Points)

Solution:

The app is almost the same as the previous one, although following the same steps as before leads us to:

Changing this to a POST request leads us to:

A parameter named debug is necessary, using the debug parameter as a JSON post request leads us to:

It only accepts true or false values. So we will be changing that:

Visiting /logs/system.log leads us to an exposed Admin token:

Using this JWT on /admin endpoint, leads us to the flag:

Flag: flag{mult1_st3p_t0k3n_3sc4l4t10n_vuln}


Injection Junction:

Break the Gate (100 Points)

Solution:

Trying to log in normally leads us to Invalid crendetials.

Trying a login bypass using SQL injection leads to being logged in as alice:

We didn’t want to be logged in as alice but rather as admin so we changed the logic to false (2=1) and we get presented with the flag:

Flag: flag{wh0_n33ds_p4ssw0rds_wh3n_y0u_h4v3_sql1}


The Search for Secrets (50 Points)

Solution:

Try to search for any user:

Trying sql injection leads to all users being returned:

Trying to retrieve all columns from the table users leads to the below error:

Googling the above error we can see that this error is from a SQLite database. Searching online from queries that will return the tables and columns we are presented with the below query:

Using SQL Injection to get the value from the flag column from the flags table.

Flag: flag{SQL1_4P1_BYPA55_C0MP13T3!}


Slash and Dash

Encoded Escapades (150 Points)

Solution:

Using ...// gets detected as malicious:

The name of the challenges talks about encoding, so we can try that first:

That gets translated to ..../ so we just need to pass the /private/flag.txt and retrieve the flag:

Unintended way to solve would be to just pass /app/app.py where the flag was disclosed and the rest of the source code:

Flag: flag{D0UBL3_3NC0D1NG_TR4V3R54L_M45T3R}


Filtered Fun (100 Points)

Solution:

Trying the common bypass ....// instantly works, so we just need to pass the path /private/flag.txt along side it:

Same as before an unintended way to solve this was just to pass /app/app.py and get the flag and source code:

Flag: flag{F1LT3R_BYP4SS_D0T_D0T_SL4SH}


Roads Less Traveled (50 Points)

Solution:

We are presented with the below page:

Trying to go down one directory using ../ and going into /private/flag.txt gets us the flag:

Same as before, unintended way to solve this would be to pass ../app.py or /app/app.py and get the flag and source code:

Flag: flag{D1R_TR4V3RS4L_F1L3_4CC3SS}


BugBountyHub

Exception Excavation (50 Points)

Solution:

The page allows us to submit a bug report:

Visiting our newly generated report takes us to /report/17569. The challenges mentions finding some hidden information, so I immediately though of IDOR.

Changing to report id to -1 would leads us to:

Flag: flag{st4ck_tr4c3s_r3v34l_s3cr3ts}


Render Bender (150 Points)

Solution:

Challenge description mentions a new rendering engine, so it was obvious that this was SSTI related so we need to find an endpoint vulnerable to this.

Visiting /templates leads us to:

The Template Syntax discloses that the engine is Jinja2 so we just need to find code execution payload as the one below:

Code Execution confirmed:

We just need to find the flags:

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('ls ./').read() }}

Returns us:
__pycache__ app.py flags requirements.txt static templates

Editing the command to cat ./flags/flag*.txt:

Gives us:

We get 3 flags back, which means that the next challenge “Secret Hunter” is also solved.

Flag2 (Render Bender): flag{t3mpl4t3_1nj3ct10n_fwt}
Flag3 (Secret Hunter): flag{ch41n1ng_vuln3r4b1l1t13s_f0r_th3_w1n}


Massive

Massive (150 Points)

Solution:

We find a login portal where we can login or register an account:

We can register an account, but trying logging in with the newly created account leads nowhere. A message ‘Login Successful’ appears, but apart from that, nothing happens.

I decided to check the source code and as well as analyzing the javascript files. Found a interesting file: script.js which contained a lot of information about endpoints, a very interesting one would be the below PUT request to /api/profiles/me:

Trying to send the above request gives us the following:

The challenge’s name is Massive, possibly hinting for a Mass Assignment vulnerability. We can see a role property in the response, maybe we can change this value if we send a PUT request to the server with role: admin

The attack is successful and our role has been updated to admin.

Checking the javascript file again we can find an admin endpoint /api/admin/dashboard:

Accessing the endpoint using our now-admin session cookie leads us to the flag:

Flag: flag{pr0t3ct3d_f13lds_byp4ss3d}


Permission Slip

Molehill (50 Points)

Solution:

We get presented with the below page:

We download the OpenAPI Spec, extract the endpoints and make a GET request to each of the endpoints.

Used a python script such as to extract all paths from the file openapi.json file :

import json

# Load the OpenAPI JSON from a file
with open('openapi.json', 'r') as file:
    openapi_spec = json.load(file)

# Extract paths
paths = openapi_spec.get('paths', {})

# Print each path
for path in paths:
    print(path)

Checking all paths on Burp Intruder there is a request with a 200 status code:

Flag: flag{0p3n_4p1_vuln3r4b1l1ty_1ntr0}


Mountain (200 Points)

Solution:

We download the OpenAPI Spec, extract the endpoints and make a GET request to each of the endpoints.

Used a python script such as to extract all paths from the file openapi.json file :

import json

# Load the OpenAPI JSON from a file
with open('openapi.json', 'r') as file:
    openapi_spec = json.load(file)

# Extract paths
paths = openapi_spec.get('paths', {})

# Print each path
for path in paths:
    print(path)

Checking all paths on Burp Intruder there is a request with a 200 status code:

Flag: flag{0p3n_4p1_sp3c_vuln3r4b1l1ty}


Rate My API

Rate Advanced (150 Points)

Solution:

Visiting the website leads us to:

The hint reveals something related to a session token. After some trial and error, I noticed we could get a token by hitting /api/session with a POST request:

The /verify endpoint (Verify PIN button) seemed to have each token limited to only a few tries, but the /vault endpoint (Access Vault button) didn’t, so we will use that request to brute-force the PIN code, using this session token above with the X-Session-Token: header.

Sorting by 200 OK response we are presented with:

Flag: flag{t0k3n_r3fr3sh_byp4ss_m4st3r}


Rate Limited (100 Points)

Solution:

Looks like we have some security measures to bypass here. Although after review, these security measures only apply to the /verify endpoint (Verify PIN button) and not /vault (Access Vault button).

Knowing this, we can just repeat what the done on the previous challenge:

Reading the flag, it just leads me to think that this was not the intended way to solve the challenge but instead by using the X-Forwarded-For header. Oh well!

This is how you could do it:

Payloads Position:

Payloads:

Result:

Flag: flag{x_f0rw4rd3d_f0r_byp4ss}


Rate Quantum (250 Points)

Solution:

We are presented with the following page:

And there are two requests being made automatically:

/api/quantum-session:

/api/inspect-token:

Failing one attempt would deduct and attempt from our token as such:

I think the intended path would be getting a token, use it for 3 tries, then get a new token, use it for 3 tries and rinse and repeat. Although, same as the previous challenge, the /api/vault endpoint did not have any security controls implemented.

So we will just use that one instead to brute-force the PIN:

Flag: flag{qu4ntum_t0k3n_f0rg3ry_m4st3r}


Rate Unlimited (50 Points)

Solution:

We are presented with the above page:

We need to brute-force a PIN from 0000 to 9999. We can do this by capturing the request:

Send this request to Burp Intruder and Select the Payloads:

As well as automated throttling for 429 and 503 status codes:

After a while we find the correct PIN code revealing the flag:

Flag: flag{br00t3_f0rc3_w1th0ut_r4t3_l1m1ts}


Final Thoughts

This CTF had a good mix of beginner and intermediate challenges, though the overuse of rate-limiting tricks made some of it repetitive. I especially enjoyed the SSTI and Mass Assignment ones for their depth. Looking forward to the next APISEC CTF!

Happy hacking 🐱‍💻