b01lersc CTF 2025 - Web Challenge Writeup
Write-up of two web challenges for the b01lersc CTF 2025.

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.
Shadow APIs:
Injection Junction:
Slash and Dash:
BugBountyHub:
Massive:
Permission Slip:
Rate My API:

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}

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}

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}

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!}

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}

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}

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}

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}

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}

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}

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}

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}

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}

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}

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}

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}
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 🐱💻
