CREATIVE CHAOS   ▋ blog

The Flag Safe (rev)

PUBLISHED ON 07/07/2025 — EDITED ON 07/07/2025 — 247CTF, INFOSEC

Instructions

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.

Files

Lets take a look at provided files:

flag.html
flag.js
flag.wasm

So a simple website, some javascript and a wasm file?

WASM

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.

Local server

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/) ...

Check files

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

TAGS: CTF, JAVASCRIPT, JS, WASM