skip to content
Profile Photo Ricky Lu (Cheng-Hung Lu)
(Draft)

Aegis CTF 2024 Writeup

/ 5 min read

WEB - 1. JSFBOX

題目中提供一個網頁,只能輸入符號 (Symbol),有提供原始碼, 要讓網頁爆出 Flag。

題目

const express = require('express');
const app = express();
const fs = require('fs');
const fa = fs.readFileSync('/flag', 'utf-8');
const port = 1024;
app.use(express.text());
app.use("/", express.static("static"));
app.post("/escape", (req, res) => {
body = req.body;
let validationResult = validateString(body);
if (validationResult !== "String is valid.") {
return res.send(validationResult);
}
console.log(body);
let result;
try {
result = eval(body).toString();
} catch (e) {
return res.send("Something went wrong with your code.");
}
try {
if (String (eval(result)) === fa) {
return res.send("WOW! How did you know the flag?");
}
} catch (e) {}
return res.send("Good job! Try harder.");
});
app.listen(port, () => {
console.log(`App running on http://localhost:${port}`);
});
function validateString(input) {
if (!/^[^\w=]+$/.test(input)) {
return "ERROR: Input must only contain symbols.";
}
const charCount = {};
for (let char of input) {
charCount[char] = (charCount[char] || 0) + 1;
}
const sortedSymbols = Object.keys(charCount).sort((a, b) => charCount[b] - charCount[a]);
if (sortedSymbols.length > 2**4) {
return "ERROR: More than 8 different symbols used.";
}
for (let i = 0; i < sortedSymbols.length; i++) {
const maxAllowed = 2**7 - i;
if (charCount[sortedSymbols[i]] > maxAllowed) {
return `ERROR: Symbol '${sortedSymbols[i]}' appeared ${charCount[sortedSymbols[i]]} times, which exceeds the limit of ${maxAllowed}.`;
}
}
return "String is valid.";
}

解法

使用 eval 函數對比,可以直接帶入 res.send(fa),就能成功把 Flag 搞出來。

題目既然只能用 symbols 作為輸入,那第一步就聯想到 JSFuck, 但原生 JSFuck 產生器會有很多符號,需要自己改良跟運用他比較寬鬆的 regex。

(!![]+[])[+!+[]] + // r
(!![]+[])[!+[]+!+[]+!+[]] + // r
(![]+[])[+!+[]+!+[]+!+[]] + // s
"." + // .
(![]+[])[+!+[]+!+[]+!+[]] + // s
(!![]+[])[!+[]+!+[]+!+[]] + // e
([][[]]+[])[+!+[]] + // n
([][[]]+[])[!+[]+!+[]] + // d
"(" + // (
((![]+[])[+[]]+(![]+[])[+!+[]]) + // fa
")" // )

MISC - 1. Eazy Jail

題目有分兩個 Stage, 分別是 Python 跟 JS, 第一個 Stage,是在 Python int(input) 的情況下, 要分別用 1, 3, 4, 6, 7, 8, 10, 11 長度的輸入解出 2。

第二個 Stage,是在 JS 的請況下, 要同時滿足 Number(input) 跟 safeEval(input) 分別為 1024 與 532。

題目

import os
def validate_and_execute(user_input, expected_length):
EXPECTED_RESULT = 2
allowed_characters = set('abcdefg123456{}"')
if len(user_input) != expected_length:
print(f"Invalid input length. Your input must be exactly {expected_length} characters long.")
return False
if set(user_input).difference(allowed_characters):
print("Invalid input. Only a,b,c,d,e,f,g,1,2,3,4,5,{,} certain characters are allowed.")
return False
expression = f"int({user_input})"
result = eval(expression, {'__builtins__': None}, {'int': int})
try:
if result != EXPECTED_RESULT:
print(f"Error.")
return False
else:
return True
except Exception:
print(f"Error.")
return False
levels = [1, 3, 4, 6, 7, 8, 10, 11]
def start_game():
logo="""
░█████╗░███████╗░██████╗░██╗░██████╗ Eazy Jail For Beginner
██╔══██╗██╔════╝██╔════╝░██║██╔════╝
███████║█████╗░░██║░░██╗░██║╚█████╗░
██╔══██║██╔══╝░░██║░░╚██╗██║░╚═══██╗
██║░░██║███████╗╚██████╔╝██║██████╔╝
╚═╝░░╚═╝╚══════╝░╚═════╝░╚═╝╚═════╝░ STAGE 1"""
print(logo)
print("Your task is to enter a string that, when used in a command like 'int(YOUR_INPUT)', results in the number 2.")
print("Each level requires a different input length. If you succeed through all levels, you will reach the next stage!")
for level in levels:
print(f"\nLevel {levels.index(level) + 1} of {len(levels)}: Enter a payload exactly {level} characters long.")
while True:
user_input = input("Enter a payload: ")
if validate_and_execute(user_input, level):
print(f"Congratulations on passing level {levels.index(level) + 1}!")
if level == 11:
os.system('clear')
os.system('node stage2.js')
return
break
else:
print("An error occurred.")
return
if __name__ == "__main__":
start_game()
exit()

 

const fs = require('fs');
const fa = fs.readFileSync('./flag', 'utf-8');
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
const QUESTION = `
What's my favorite number?
- 1024 is my birthday
- 532 is my last 3 digits of my phone
`;
const CORRECT_ANSWER = 1024;
const EXPECTED_RESULT = 532;
console.log(QUESTION);
readline.question('Guess the number: ', input => {
const guessedNumber = Number(input);
console.log(guessedNumber);
let spinnerInterval;
let spinnerIndex = 0;
const spinnerChars = ['|', '/', '-', '\\'];
const startSpinner = () => {
spinnerInterval = setInterval(() => {
process.stdout.write(`\rChecking your answer... ${spinnerChars[spinnerIndex]}`);
spinnerIndex = (spinnerIndex + 1) % spinnerChars.length;
}, 100);
};
const stopSpinner = () => {
clearInterval(spinnerInterval);
process.stdout.write('\rChecking your answer... Done!\n');
};
startSpinner();
setTimeout(() => {
stopSpinner();
console.log(`\n=====================================\n`);
if (guessedNumber === CORRECT_ANSWER) {
console.log(`Great! You got the number - Awesome!`);
try {
const evaluationResult = safeEval(input);
if (evaluationResult === EXPECTED_RESULT) {
displaySuccess();
} else {
displayFailure();
}
} catch (error) {
displayError();
}
} else {
displayFailure();
}
readline.close();
}, 1500);
});
function safeEval(input) {
const allowedChars = /^[0-9+\-*/.\s]+$/;
if (!allowedChars.test(input)) {
throw new Error('Invalid characters detected!');
}
return new Function(`return (${input})`)();
}
function displaySuccess() {
console.log(`Good job! You got it! :)`);
console.log(`Flag: ${fa.trim()}\n`);
}
function displayFailure() {
console.log('Oops! Wrong answer! Try again :)\n');
}
function displayError() {
console.log(`Error occurred! Please try again!\n`);
}

解法

第一個 Stage: [ 2, “2”, b”2”, f”{2}”, """2""", f"""2""", f"""{2}""", f”{f”{2}”}” ]

第二個 Stage: 運用新版 JS Number(input) 不會辨識進位的特性,使用 01024 讓他取得 1024, 後面 safeEval(input) 時會辨識進為八進位,1024 即為 532,可滿足兩個條件。