Attempting to Bypass the AngularJS Sandbox from a DOM-Based Context in versions 1.5.9-1.5.11 (Part 2)

By Daniel Kachakil
Attempting to Bypass the AngularJS Sandbox from a DOM-Based Context in versions 1.5.9-1.5.11 (Part 2)

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.