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