XSS Tricky: Function Hoisting
Another day, another hunt for XSS. A simplified version of the target code:
<script>
func().val('<?php echo htmlspecialchars($input, ENT_COMPAT, 'UTF-8'); ?>');
</script>
A user-supplied $input is echoed into a JavaScript context after having HTML entities encoded. We can’t inject additional script tags due to the HTML encoding. So we’ll stick with JavaScript and try to inject a payload:
'); payload; //
We use '); to close out the val() call with an empty string. Next comes our payload. Finally, // comments out any remaining characters from the original code. We end up with:
<script>
func().val(''); payload; //');
</script>
But no dice. The func() JavaScript function is undefined, causing an exception and halting execution before the injected payload can be executed. And that HTML encoding means we can’t just start a new script context, so an XSS vector isn’t immediately apparent.
But it’s there.
To gain execution, we’ll need to make sure func() is defined. To do this, we can use a JavaScript feature called function hoisting. Hoisting allows a function to be defined after it’s been used. The JavaScript interpreter will look ahead for an appropriate function definition and “hoist” it up in the code, so the function call can execute correctly.
We’ll use the following payload:
'); function func() {payload}; //
This time, we’ll finish the val() call by supplying an empty string again (with the '); section). Next, we’ll provide a definition of func() for the interpreter to hoist. We can insert our payload into func()’s definition and let the original call execute it. Lastly, we’ll comment out the trailing “');” that was left over from the original code. This gives us:
<script>
func().val(''); function func() {payload}; //');
</script>
Now the func() function is defined and will execute with our injected payload. You can read a bit more about hoisting here on MDN.
Stay beautiful, XSS Rangers.