A secret flag is safely hidden away within client side scripts. We were told JavaScript is an insecure medium for secret storage, so we decided to use C++ instead.
Lets take a look at provided files:
flag.html
flag.js
flag.wasm
So a simple website, some javascript and a wasm file?
WebAssembly (WASM) is a low-level, binary format that lets you run high-performance code (from C, C++, or Rust) right in the browser. It’s built for tasks JavaScript struggles with: cryptography, graphics, data processing.
WASM is fast, secure, and portable. While it makes reverse engineering harder than JavaScript, it’s still accessible - making it perfect for browser-based puzzles and security problems.
To run the “website”, we can spin-up a local web server with Python:
$ python3 -m http.server 8080
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
flag.html
<script>
function getFlag(){
var valid_flag = false;
var flag = document.getElementById('flag').value;
var error = document.getElementById('error-msg');
error.textContent = "";
try {
valid_flag = Module.CompareFlag(flag); // IMPORTANT
} catch (err){
error.textContent = err.message;
return false;
}
if (valid_flag){
alert(flag);
} else {
error.textContent = "Invalid flag!";
}
return false;
}
</script>
We can see that Module.CompareFlag(flag), so we can check if there is any more hidden functions.
To do that we can use Object.keys(Module).
Open the browser, open developer tools, F12 on Firefox:
> Object.keys(Module)
Array(27) [ ...
0: "preloadedImages"
1: "preloadedAudios"
2: "BindingError"
...
24: "calledRun"
25: "CompareFlag"
26: "CompareFlagIndex" // IMPORTANT
length: 27
We can see that there is another function that we could use CompareFlagIndex.
Lets see how to use it.
Start to play with stuffing, as we know the format of the flag, we have some advantage:
> Module.CompareFlag("247CTF{aaaabbbbccccaaaabbbbccccaaaabbbb}");
> Module.CompareFlagIndex("2", 0);
true
> Module.CompareFlagIndex("2", 1);
Uncaught Error: Invalid flag index!
> Module.CompareFlagIndex("24", 1);
true
> Module.CompareFlagIndex("246", 1);
true
> Module.CompareFlagIndex("246", 2);
false
> Module.CompareFlagIndex("247", 2);
true
Logic found, lets brute-force it:
(async () => {
const charset = "0123456789abcdef"; // hex charset
let flag = "247CTF{"; // known prefix
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
console.log("Brute-forcing flag of format: 247CTF{32-hex}");
for (let index = 7; index < 39; index++) { // 32 hex chars from index 7 to 38
let found = false;
for (const c of charset) {
const guess = flag + c;
await delay(10); // throttle to avoid freezing
try {
if (Module.CompareFlagIndex(guess, index)) {
flag = guess;
console.log(`Index ${index}: '${c}' → ${flag}`);
found = true;
break;
}
} catch (e) {
if (!e.message.includes("Invalid flag index")) {
console.error(`Error at index ${index} with '${guess}':`, e.message);
}
}
}
// this part should not happen if we have all of our facts straight
if (!found) {
console.error(`No match at index ${index}. Current: '${flag}'`);
return;
}
}
flag += "}"; // we know the last char is }
// Final confirmation
try {
if (Module.CompareFlag(flag)) {
console.log("Full FLAG recovered!");
console.log(flag);
} else {
console.error("Final flag did not validate:", flag);
}
} catch (e) {
console.error("Error validating final flag:", e.message);
}
})();
As of publishing, 29 solves