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