Introduction
In Part 1 of this two-part blog series, we identified two checks introduced in AngularJS v1.5.9 to mitigate the vulnerabilities leveraged by the latest sandbox bypass:
- The
ensureSafeAssignContext
function now also disallows assignments to the prototype of the most common built-in objects, not only to their constructors. - The code generated by AngularJS now properly encodes unsafe identifiers, as the faulty regular expression in
nonComputedMember
of its parser was fixed.
In this post, we'll attempt a full bypass chain based on this knowledge of checks. In the end, although I wasn't successful, I hope that my research inspires further research that leverages some issues under the conditions described here.
Attempting to invoke call
, apply
and bind
functions
One of the checks AngularJS performs in ensureSafeFunction
since v1.2.19, is to make sure that we can't invoke call
, apply
or bind
functions. If we attempt to call any of these disallowed functions, we'll get an exception. For instance:
{{ [].constructor.call() }}
Will result in:
Error: [$parse:isecff] Referencing call, apply or bind in Angular expressions is disallowed! Expression: [].constructor.call()
Using the Array.map
or Array.filter
functions
Nothing prevents us from getting references to these disallowed functions, and even using them, as long as we either don't end up calling them or we find a way to call them without getting caught. For instance, we could try to invoke them by using some functions that accept references to other functions as a parameter. As an example, let's try using the Array.map
function which creates a new array by calling an arbitrary function for each element of the original array:
{{ ["a","b","c"].map("".constructor.prototype.toUpperCase, "d") }}
This results in this array:
["D","D","D"]
Next, let's try the bind
function:
{{ [1].map([].constructor.bind) }}
We'll get the following:
"TypeError": "Bind must be called on a function"
Similarly, if we try the apply
function instead of bind
, we'll get:
Function.prototype.apply was called on undefined, which is a undefined and not a function
And the same will happen with call
:
undefined is not a function
These errors demonstrate that all restricted functions were invoked, but with unexpected parameters. The Array.map
and Array.filter
functions will invoke the callback
function with three parameters (the element, its index, and the original array). They also accept additional parameters.
We can also consider an example of an expression successfully leveraging call
to invoke an arbitrary function (such as String.toUpperCase
) on arbitrary elements:
{{ ["a","b","c"].map([].constructor.call, "".toUpperCase) }}
This will return:
["A","B","C"]
While interesting, I didn't find a way to get anything useful from invoking functions this way, mostly because I didn't have a clear goal. Nevertheless, in other contexts or in combination with other techniques, invoking restricted and arbitrary functions might be a useful approach to consider.
Bypassing the SAFE_IDENTIFIER
check
In principle, the following code taken from target versions (v1.5.9-v1.5.11) looked safe to me, and I saw no trivial way to bypass it:
nonComputedMember: function(left, right) { var SAFE_IDENTIFIER = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/; var UNSAFE_CHARACTERS = /[^$_a-zA-Z0-9]/g; if (SAFE_IDENTIFIER.test(right)) { return left + '.' + right; } else { return left + '["' + right.replace(UNSAFE_CHARACTERS, this.stringEscapeFn) + '"]'; } }
What if we use the same techniques used in previous exploits? Can we replace some built-in JavaScript functions like String.replace
to avoid the encoding of special characters? At this point, there's no way to override the String
prototype or any of its functions, so this doesn't look like a viable path.
However, among the many built-in objects checked into the ensureSafe*
functions, AngularJS doesn't prevent us from overwriting the RegExp
prototype and, therefore, it's theoretically possible to replace the RegExp.test
with a function which returns true
. To verify this assumption, we can paste the following code in the browser's console and then paste the payload into AngularJS v1.5.9 with the ensureSafeAssignContext
function disabled:
RegExp.prototype.test = function() {return true}
This works, so the assumption is correct, but the ensureSafeAssignContext
function won't be disabled in a real-life scenario, so we need to figure out how to reach the same outcome using an AngularJS expression.
Getting access to a RegExp
object
Unlike all the other basic types which are checked (Integer
, Boolean
, String
, Object
and Array
), we can't create a RegExp
object within an expression. While JavaScript supports creating an object by simply surrounding an expression with forward slashes (e.g. /abc/
), this syntax isn't supported by the limited expressions we can use in AngularJS, so we'll have to find another way.
I explored different options, like attempting to access the caller
or arguments
properties of an invocation, but this isn't allowed in strict mode where our code runs. I also tried to see if any function we're allowed to call returns a RegExp
object, but was unsuccessful. Since we can create and access strings, the closest attempt was to call String.matchAll
:
{{ "abc".matchAll("(?<myGroup>.*)").toString() }}
This returns a RegExp String Iterator
object:
[object RegExp String Iterator]
Unfortunately, this object only has references to arrays, integers, Booleans and strings, not to any regular expression. Also, its constructor is Object
, so it doesn't open the door to manipulating additional types.
Maybe there's a way to access a RegExp
object from the built-in objects. We can't create or access a RegExp
object from the limited DOM-based scenario but, for now, let's assume that the scope object had a reference to a regular expression. We can explore what we could do from this scenario. It's unlikely that we would find it in a real application, but it's still an interesting exercise from a research perspective, so I modified my testing environment to pass a regular expression as an argument.
return $interpolate(input)(/test/);
From now on, if we access this
, it will contain a reference to a RegExp
object:
{{ this.constructor.toString() }}
Returns:
function RegExp() { [native code] }
Overwriting the RegExp.test
function
Assuming that we can access a regular expression object, we can try to exploit the issue we described earlier, attempting to assign an existing function that we can access from our limited context. Our goal is to override the test
function with another function accepting arguments which must be compatible with the original one, and which always returns true
.
I gathered a list of candidates from the AngularJS documentation, and after some trial and error I found one which did exactly what we wanted: Array.push
. Well, maybe not exactly, because this function doesn't return true
as a Boolean, but instead returns an integer greater than zero. This is good enough in our case, since the condition doesn't enforce any specific type, so the result will be casted and other values like [1]
(as an array) or "1"
(as a string) would also pass the test. This would have been different if the code had a stricter condition, using defensive coding practices, like this:
if (SAFE_IDENTIFIER.test(right) === true) {...}
At this point, assuming that we could bypass the ensureSafeAssignContext
check, and that we got access to a regular expression object, this payload will work in the target versions (v1.5.9-v1.5.11):
{{ this.constructor.prototype.test = [].push; {y:''.constructor.prototype}.y.charAt = [].join; [1]|orderBy:'x=alert(1)' }}
Messing with the RegExp
prototype
We just described a way to bypass one of the two additional checks enforced in the target versions, which may be a useful part of a more complex chain under specific conditions. Before we continue exploring this path, let's take a step back and see what else we can do if we can access a RegExp
object from an AngularJS expression.
By inspecting the source code, we can find multiple references to regular expressions in AngularJS, so if we replace functions of the RegExp
prototype we can probably find something useful.
Out of memory errors
As soon as we replace the RegExp.exec
function with another function with compatible arguments, it's highly likely to enter an infinite loop or recursion, causing the browser to consume a full CPU core and quickly allocate several gigabytes of RAM, ultimately throwing an out of memory
error. For instance, this is exactly what will happen if we try the following payload:
{{ this.constructor.prototype.exec = [].valueOf }}
Others like this one will have a similar effect, but the memory grows in a slower fashion:
{{ this.constructor.prototype.exec = [].constructor }}
While this can be considered as a client-side denial-of-service condition, rather than being something useful, in our case this behaviour may prevent us from taking advantage of the function's replacement, probably rendering this technique useless. However, if we save a reference to the original function and we restore it before ending our interpolated expression, we can still use the poisoned function in between. This won't cause an infinite loop:
{{ originalExec = this.constructor.prototype.exec; this.constructor.prototype.exec = [].constructor; /* Do something here */ this.constructor.prototype.exec = originalExec; }}
Invoking disallowed functions using the Date
filter
Analysing the new v1.5.9 attack surface to see if it was possible to take advantage of replacing functions from the RegExp
prototype, I found that the built-in Date
filter uses regular expressions to match date formats, which will be replaced by their corresponding parts (https://github.com/angular/angular.js/blob/v1.5.9/src/ng/filter/filters.js#L626)
var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG' ... / function dateFilter($locale) { ... return function(date, format, timezone) { ... while (format) { match = DATE_FORMATS_SPLIT.exec(format); ... }
Since we can reassign the RegExp.exec
function and we also have control over the format
parameter, we can do something like the following to call disallowed functions like bind
:
{{ ex = this.constructor.prototype.exec; this.constructor.prototype.exec = [].constructor.bind; 1|date:"".constructor.toString; this.constructor.prototype.exec = ex; }}
This will trigger the Bind must be called on a function
error, meaning that the disallowed function was invoked, but since the left part of the inner expression (the this
argument) will be a regular expression object (DATE_FORMATS_SPLIT
) rather than a function, this call doesn't make much sense. At least it gives an idea on what could be done in other similar scenarios with better conditions.
Exploring other approaches
The AngularJS versions we're targeting are enough to consider different approaches, such as relying on features which changed over the years and probably broke some of the assumptions made at that time. In general, these may include changes in any of the other components involved, such as web browsers, the JavaScript language specification, or even underlying protocols. Not only recent changes should be considered, but also existing or uncommon features which probably weren't taken into account. In this case, I didn't spend time analysing changes in web browsers or protocols. I just focused on JavaScript capabilities.
JavaScript features
I reviewed what was changed in the latest versions of JavaScript and other aspects which were there already, including objects like Reflect
or Symbol
. Getting access to these objects would allow to invoke things like Reflect.setPrototypeOf
, or overwrite the RegExp.replace
function (which unlike others, requires access to Symbol.replace
in order to do so), but I found no way to get references to any of these static objects from the context of AngularJS interpolated expressions.
Other things I considered were related to getting access to primitive types using syntaxes which are supported in JavaScript (for instance, 123n
is a BigInt
and /abc/
is a RegExp
), but these weren't accepted by the AngularJS lexer either, so it wasn't possible to leverage any of them.
I also reviewed all operators supported by JavaScript, to see if there was a way to get something useful from any of them. One of the most interesting ones was probably the spread
operator, which allows to perform a shallow copy of an object with the following syntax:
copied = { ...original }
Maybe this could be used to get references to restricted objects or functions without being caught by the checks performed by AngularJS. However, the syntax itself wasn't supported by the lexer, so that wasn't an option. Similarly, the rest
parameters and destructuring
assignments weren't supported either, since they also use the ellipsis (...
) syntax.
Conclusion
This blog series describes the process I undertook to prepare a simple application, test and adapt known sandbox bypasses with previous versions of AngularJS, and develop a slightly improved payload to allow an empty scope. This was submitted to PortSwigger's XSS cheat sheet where it was quickly accepted and merged.
We describe how it's possible to invoke restricted or disallowed functions like call
, apply
or bind
using indirect calls like the ones offered by Array.map
or Array.filter
. We also demonstrate the impact of having access to a regular expression object (RegExp
) in our scope. This allows us to bypass certain checks and alter the normal behaviour of AngularJS by replacing built-in functions, which can lead to infinite loops which increases CPU and RAM usage or potential sandbox bypasses, among other challenges.
Other approaches which led to nothing useful were also discussed to give additional context on the thought processes behind the research and provide ideas on further research on this or similar topics.
Even though the sandbox was completely removed in the most recent versions of AngularJS, it's still relatively common to find applications using target versions (1.5.9-1.5.11) which still featured a sandbox. I hope you enjoyed reading this blog series and found it useful.
About the Author
Daniel Kachakil, Principal Security Engineer, leads the Anvil Secure application and cloud security groups. He works with the COO to grow Anvil Secure's client base and with the CTO to oversee and direct security research. He is a speaker and published author on topics including the Android vulnerabilities he discovered, cryptography, web hacking, and SQL injection. He is also an ethical hacking instructor. He holds a Master's Degree in Information Systems Management from Polytechnic University of Valencia in addition to a five-year degree in Computer Science Engineering with a concentration in Hardware Engineering.