Real World .NET, C#, and Silverlight: Indispensible Experiences from 15 MVPs (2012)
Chapter 3
Ethical Hacking of ASP.NET
by György Balássy
Web applications have at least three important characteristics that are absolutely independent of the technology they are built on: They must be user-friendly, fast, and secure. As a web developer, you are probably sensitive to these aspects, and keep them on high priority, because you know that they heavily determine the success of your website.
When you design the user interface and the architecture of your application, you can constantly focus on these three facets. You can try to find the best arrangement of the user interface (UI) elements, and try to fit the components to get the best performance results. However, even with your best efforts, you have an important component in your system that can help you to reach (or keep you from reaching) your goals: the web platform.
If you have an inherently slow base technology, you cannot boost it and make it fast, not even with your hardest work. Your application can be profoundly optimized for performance, but if the underlying platform is inherently slow, you are doomed — the speed of the underlying platform is the key to your success. You can have a cheetah, but if it travels on the back of a turtle, it takes a long time to reach its destination.
The same is also (or even more) true for security. Web applications today do not handle web protocols at the low level because the programming frameworks provide a high level of abstraction that increases developer productivity. This abstraction also means that the security aspects of a website are tightly coupled with the security features of the programming platform. When you build your web application, you no longer must find out how to implement authentication, or you do not have to invent a way to manage sessions, because they are provided out-of-the-box by the framework. Because these solutions are standard parts of the framework, it is expected that your application uses these standard solutions instead of a custom approach.
Your website and the developer framework you use are tightly chained together. And the old saying is still true: “A chain is only as strong as its weakest link.”
You most likely have written code for the web for a while, and you already know much about SQL injection, cross-site scripting (XSS), and buffer overflows. They are dangerous threats, and it is mostly up to the application developer to prevent them. A single website that is vulnerable to SQL injection can lead to a compromised database, or even to a compromised server. A website that has a single XSS vulnerability can transform a client computer into a zombie member of a botnet. Application developers learned these lessons, and you most likely also know how to write code to protect your site, your server, and your visitors against these attacks.
But what about the code that was not written by you? Or what about the code that you utilize as a basis to build your own code on top of it? Can you be absolutely sure that the framework you use is secure? Of course, you naturally trust it because most frameworks are usually provided by big vendors and have a long history. But should you really trust them?
You can have a house, a castle, or even a well-protected fortress, but if you build it on a swamp, sooner or later it will go down. That's a serious risk you have to deal with. You must know the strengths and the weaknesses of your favorite developer platform, and you must eliminate the threats that the platform brings into your application. You build not only an independent application, but also a complete solution, and the platform is an integral part of it. If the built-in features are not secure enough, it is your responsibility to enhance them to provide a rock-hard solution to your customers.
This chapter guides you through the built-in security-related features of the ASP.NET platform. You see how they work, what threats they can handle, and what their weak spots are. You also see solutions that you can use in your own code to further enhance the security of your ASP.NET web application.
Ethical Hacking — Is That an Oxymoron?
Online news sites and traditional newspapers like screamers because the big thrilling headlines attract the readers. The word hacking often appears in those headlines (did you notice the title of this chapter?) because it has some mysterious meaning almost as alluring as the magic world of Harry Potter.
Contrary to the expectations of most people, The Jargon File (which is the online glossary of hacker slang and an authoritative source) defines hacker in a positive way:
hacker /n./ […] 1. A person who enjoys exploring the details of programmable systems and how to stretch their capabilities, as opposed to most users, who prefer to learn only the minimum necessary. 2. One who programs enthusiastically (even obsessively) or who enjoys programming rather than just theorizing about programming. […] 4. A person who is good at programming quickly….
— The Jargon File
This definition says nothing more than hackers are computer and network gurus. It does not say anything about evil intent to rule the world. The expression “ethical hacking” (or “white hat hacking”) pushes this positive side further. Ethical hackers are security experts who specialize in penetration testing to ensure that IT systems are secure. Ethical hackers are the good guys who attack systems on behalf of their owners, seeking vulnerabilities that a malicious user could exploit.
It was almost certainly Sir Arthur Conan Doyle who first stated in his Sherlock Holmes stories that a detective must know the criminal's way of thinking and the criminal's tools to effectively hunt crime. The same is also true for cybercrime. You must be aware of the latest attacks and use the latest tools to find out how to protect your information systems against them.
In this chapter, you learn about many of those attacks, and about some useful tools that you should use to test your own applications. Did you notice the emphasis on the word “own”? It is important that these are real attacks and real tools, and they can be used for good or can be used for bad. I trust that you will use them only with good intent, to produce better applications and make our online world more secure.
Filling Your Toolbox
To be an effective ethical hacker, you need two things: knowledge and tools. The first one is about theory, and the second one is about practice. Although there is nothing more practical than a good theory, to try out the concepts discussed in this chapter, you need some tools. This part of the chapter introduces you to some of the most useful web developer tools, and tools that you should definitely try. They can be used not only for security testing, but also for other tasks during web application development — such as fixing Cascading Style Sheet (CSS) code or debugging JavaScript code.
Fiddler
First, you need a web debugging proxy, which is an application that stands between your browser and your website. I emphasized the words “debugging” and “your” because this is not a network sniffing tool and does not provide you with access to someone else's network traffic. It can intercept the communication between an application (typically a browser) on your computer and a remote server, acting as a network proxy.
Several web debugging proxy tools are available on the Internet. I prefer to use Eric Lawrence's Fiddler, which is freeware you can download from http://fiddler2.com. With it, you can log all HTTP(S) traffic between your computer and the Internet. You can use Fiddler to inspect the communication, set breakpoints, and modify the outgoing HTTP requests or the incoming responses. It has an event-based scripting subsystem, but the most unique feature is that Fiddler can be extended using any .NET language.
After you install and start Fiddler, it tries to register itself in the operating system as a network proxy that runs on 127.0.0.1:8888. As a consequence, applications that rely on the operating system's proxy settings automatically forward their traffic to Fiddler, and it logs the communication. For this reason, Microsoft Internet Explorer and Google Chrome require no additional configuration, but for Firefox, you may need to manually set the proxy.
Follow these steps to peek into the communication between the browser and the server:
1. Start Fiddler from the Windows Start menu.
2. Start Internet Explorer and open the http://fiddler2.com website.
3. Fiddler automatically captures the traffic as it is indicated on the first pane of the status bar (see Figure 3.1), so you can inspect the communication straight away.
4. On the left pane, you can see the various sessions (HTTP request-response pairs) sent by your browser, and on the right pane, you can find the tool windows. Select a session on the left and the Inspectors tab on the right to display the details of the selected session.
5. The upper part of the Inspectors tab details the HTTP request, and the lower part displays the HTTP response. Both the upper and lower panes enable you to view the traffic in different modes. For example, you can click the Headers button to view only the header fields in logically grouped sections, or you can click the Raw button to examine the full traffic byte by byte.
Figure 3.1 Browser traffic inspection with Fiddler
Usually, a single web page involves multiple sessions to download the full content because separate requests are required for every single image and script file the page links to. This characteristic of the HTTP traffic can quickly lead to a long list of sessions on the left pane that hides the important details. Because of this, it is a good practice to filter the traffic using the features in the Rules menu and the Filters tab. When you finish logging the sessions you want to inspect, stop capturing with the Capture Traffic menu item in the File menu to avoid getting flooded with the additional requests.
To modify the network traffic, you can set breakpoints using the Automatic Breakpoints menu item in the Rules menu. You can set Before Requests and After Responses type breakpoints, which pause the execution before the request is sent to the server, or after the response is received by the proxy, but before it is forwarded to the browser. When these breakpoints are reached, Fiddler pauses, and you can manipulate the traffic in the Inspectors tab.
If you want to construct an HTTP request from scratch and examine how the server processes that request, you can use the Request Builder tab on the right pane.
Fortunately, the most frequently used features of Fiddler are also accessible with hotkeys, as shown in Table 3.1.
Table 3.1 Frequently Used Hotkeys of Fiddler
Hotkey |
Function |
F12 |
Start or stop capturing. |
F11 |
Set a Before Requests breakpoint. |
ALT+F11 |
Set an After Responses breakpoint. |
Shift+F11 |
Disable breakpoints. |
F6 |
Open or close the session list pane. |
F8 |
Jump to the Inspector tab. |
CTRL+A |
Select all sessions. |
DEL |
Remove the selected session. |
CTRL+X |
Remove all sessions. |
Firebug
Another widely used tool for monitoring HTTP traffic is Firebug, which is an Open Source and free add-on for Firefox that you can download from http://getfirebug.com. Firebug fully integrates with Firefox, as you can see in Figure 3.2.
After you install Firebug, you can press F12 to open the tool window in the lower pane. To monitor the HTTP traffic, hover over the Net tab, click the down arrow, and then click Enabled to turn on capturing. From now on, all communication produced by the page you currently visit in the main browser window is automatically logged, and you can inspect it in the lower pane. If you do not need to monitor all types of traffic, you can apply a content type filter by clicking the buttons for HTML, CSS, JS, XHR, and so on below the Net tab.
Figure 3.2 The Firebug add-on for Firefox
You can click the + sign in the row header to scrutinize the details of a single request and its corresponding response. The expanding pane shows the query string parameters, the request and response headers, the response body, and cache statistics. You can also add a Cookies tab here by installing the Firecookie add-on from the add-on gallery that further extends the capabilities of Firebug.
Firebug is a powerful tool with many hidden treasures. I strongly recommend that you visit its online documentation page and experiment using it in your daily work.
Internet Explorer 9 Developer Toolbar
Although, as a web developer, you probably already have Firefox installed on your machine, if you still prefer to use Internet Explorer, you can perform the same network monitoring tasks in your favorite browser, too. Internet Explorer 7 introduced the Developer Toolbar feature that incorporates similar features that Firebug provides, with the only major exception being the previously mentioned features of the Net tab. However, with Internet Explorer 9 (IE9), you do not have to miss this feature any more because Microsoft added a new Network tab to the Developer Toolbar, as shown inFigure 3.3.
Figure 3.3 Network monitoring with the IE9 Developer Toolbar
The new Network tab in IE9 provides similar functionality that Firebug provides, with the only major difference being the UI layout.
Lens
The tools that you have learned about thus far provide general traffic-monitoring features that you can use to inspect any website independently from the technology they are built on. However, every technology has specialties, and you need special tools to test them. With its custom authentication, session, and state management, ASP.NET is no exception.
Lens is a free tool you can download from http://ethicalhackingaspnet.codeplex.com that you can use to test only ASP.NET applications. Lens is not only a penetration-testing tool, but it was also written with educational and demonstrational purposes in mind. For this reason, it contains links to additional online learning resources.
You can see the main (and only) window of Lens in Figure 3.4. To test a website, enter its URL into the Target URL textbox, and then choose a tab that corresponds to the feature you want to test. For example, in the ViewState tab, you can download, extract, and decode the ViewState of the given page (as you will see in more detail later in this chapter).
Figure 3.4 Lens, the ASP.NET penetration-testing tool
In addition to being a free tool, Lens is also Open Source, so you can download the full source code from the CodePlex site. It is written in .NET 4 with a Windows Presentation Foundation (WPF) graphical user interface (GUI) and a modular architecture, which enables quick extensibility.
Understanding Session Management
Now that you have a handful of tools in your toolbox that you can use to test your solution, you need to understand how things work to know what and how you should test.
One of the most important parts of a web application is session management. The basic problem is that web pages have no memories, so when you visit a page on a website, the site always treats you as a completely new visitor. Accordingly, it doesn't know anything about your previous actions, even if you logged in before, or put something into your shopping basket a minute ago. The underlying protocol provides no solution for this problem, so from the HTTP perspective, with every new request, you are a brand new guest of the site, who just entered the front door.
But, of course, visitors need this kind of functionality, so they don't get asked again and again for the same information. In other words, they need a session that connects their movements from page to page, and because HTTP is stateless, it is implemented in a higher abstraction level, mostly by cookies.
Session Management in HTTP
Cookies are small pieces of data transmitted with the HTTP requests and responses. They are created on the server and sent to the client with an HTTP response. The browser stores them, and when the next request goes to the same server, the browser adds the same cookies to the request and sends its data back to the server. When the same cookie value is received with two separate requests by the server, the server treats them as part of the same session.
Cookies travel in the header section of the HTTP traffic. As a result, they have a limited size. As a consequence, most web applications do not store the full user state in the cookie, but instead they store it on the server side, give this property bag a session ID, and then store this small session ID in the session cookie, as shown in Figure 3.5.
Figure 3.5 How cookies travel in HTTP headers
This cookie exchange can be perfectly caught on the act by Fiddler. Just visit a website (for example, http://codeplex.com) and look for a Set-Cookie header in the first response, as shown in Figure 3.6.
Figure 3.6 Inspecting the Set-Cookie header in Fiddler
Then, select a subsequent HTTP request that targets the same domain, and you see a Cookie header in the request that contains the same value.
Session Management in ASP.NET
In ASP.NET, you have two kinds of sessions: simple sessions and login sessions. The first one is mostly called just “session.” (I added the “simple” prefix to it to help you to differentiate it from the second one.)
Simple sessions have nothing to do with authentication. When you use them, it does not matter whether your user is logged in. These types of sessions are just there to solve the problem detailed earlier in this chapter, and you use simple sessions to connect HTTP request and response pairs together that originate from the same client. You can use the Session object in ASP.NET to create a multipage form, or to implement a web shopping basket that does not require authentication.
On the other hand, login sessions are closely related to authentication. A login session starts when a user signs in and lasts until the same user signs out from your application. The main purpose of a login session is to track who exactly sends the requests to your application and to relay authorization decisions on that information. The login controls and the membership providers implement login sessions in ASP.NET.
Although there are these two kinds of sessions, both lead back to the same problem. There is no connection between the request-response pairs at the HTTP level. Both types of sessions are implemented using the same cookie-based concept; although, they use different cookies and have different strengths and weaknesses.
Attacking the ASP.NET Authentication
The most attractive target for a hacker in a website is the authentication. If the authentication fails and enables an unauthorized user to log in to the application, that can have unforeseeable consequences.
Before you can perform a penetration test on the authentication of your ASP.NET website, you must understand how the built-in authentication features are implemented in ASP.NET.
Deep Dive into ASP.NET Authentication
When you use the built-in login controls and the out-of-the-box membership provider, all you see is that the authentication “just works,” and all the magic is done by ASP.NET in the background. The following is happening behind the curtains:
1. The Login control calls the ValidateUser method of the configured MembershipProvider that checks the user's login credentials.
2. If the credentials are invalid, the login process terminates, and an error message displays for the user.
3. If the credentials are valid, the Login control calls the SetAuthCookie method of the FormsAuthentication class that reads the settings from the web.config file and then creates the authentication cookie and attaches it to the HttpResponse.
4. The user is redirected with a HTTP 302 status code to the original page. The response contains the Set-Cookie HTTP header with a default cookie name, .ASPXAUTH (the name starts with a dot).
5. The cookie is received by the browser and stored in its cookie store. When the browser must send a request later to the same domain, the cookie is attached to the Cookie header of the request.
6. The cookie is received by the FormsAuthenticationModule on the server. This HTTP module checks the cookie and, if the cookie is valid, it creates a new FormsIdentity and a GenericPrincipal object and assigns them to the current HttpContext.
The most important part is no doubt how the authentication cookie is created and what it contains. The SetAuthCookie method is responsible for creating the authentication cookie, and it puts a FormsAuthenticationTicket object into it. Figure 3.7 shows the structure of the FormsAuthenticationTicket class.
Figure 3.7 The FormsAuthenticationTicket class
The ticket contains the following property values:
· CookiePath — The directory where the cookie is active. So, if you want the cookie to be only sent to pages in the Members directory, set the CookiePath to /Members.
· Expiration — The expiration date and time of the ticket.
· Expired — A Boolean value that indicates whether the ticket is expired.
· IsPersistent — A Boolean value that indicates whether the authentication cookie is persistent.
· IssueDate — The date and time when the ticket is issued.
· Name — The login name of the user.
· Version — A numeric constant that defines the algorithms used by the framework to build the cookie. This version number is 2 in ASP.NET 4 (since ASP.NET 2.0).
The UserData property is empty by default and can be used for custom purposes.
By default (you can configure it in the web.config file), the ticket is protected with a hash then it is encrypted and converted to a hex string before it is added to the authentication cookie.
Stealing the Ticket
As you saw earlier in this chapter, the heart of the authentication is not the user's password, but the ticket that sits inside the cookie. If you can get a valid ticket, you no longer need the original password of the user because it is the ticket that is validated at every single request, and it directly contains the unique login name of the user. If malicious users can successfully attack the ticket, they can reach that goal.
One method for the attacker to get the ticket is to use XSS. As you probably know, the possibilities of XSS extend much further than just capturing a cookie. XSS can even help an attacker take total control of the victim's computer. However, it is still a classic and widely used way to steal your cookie with your login ticket in it. Fortunately, ASP.NET, by default, creates HttpOnly cookies, and that greatly reduces the risk of this attack.
HttpOnly is a single flag in the cookie, which signals to the browser that the cookie should not be accessed by any script code on the page, and that it can be used only for HTTP transfers. If you do not want to access the cookie value in JavaScript, it is recommended that you set this flag to mitigate script-based attacks against the cookie.
The ticket is transmitted in the header of the HTTP traffic so that it travels in clear text on the wire, (refer to Figure 3.6). Although the ticket itself is encrypted, as you see later, the attacker does not need to decrypt it to maliciously use it. Having the ticket is enough for the attacker to act on your behalf. For this reason, it is crucial that you protect the ticket in transmit at least as seriously as the password, and you have to use Secure Sockets Layer (SSL) for that.
You have probably heard many times that encryption is extremely CPU-intensive, and that you must minimize HTTPS to free up resources on your server. That was probably true once, but not today. In January 2010, Gmail switched to using HTTPS for everything by default. Of course, the more-secure protocol had its overhead, but guess how much? Less than 1 percent! You should read the case study about it at www.imperialviolet.org/2010/06/25/overclocking-ssl.html.
By default, ASP.NET authentication does not force you to use HTTPS, but it is recommended that you change this setting in the web.config file by setting the requireSSL attribute of the forms tag to true, as shown here:
<authentication mode="Forms">
<forms requireSSL="true" ... />
</authentication>
With this change, you can force ASP.NET to set the Secure flag of the cookie to true, which mandates that the browser transmits the cookie only over HTTPS and never on plain HTTP.
You can test your configuration with Fiddler. By default, Fiddler does not decrypt HTTPS traffic, so you see what an attacker could see from your encrypted traffic. If you turn on the Decrypt HTTPS Traffic option in the Fiddler Options dialog, Fiddler creates self-signed certificates to intercept the encrypted traffic. If you can read the traffic without turning on this option, that means it goes on HTTP and not on HTTPS. If you allow Fiddler to decrypt HTTPS traffic, but you don't see any change in the browser (not even a warning dialog for self-signed certificates), then you are not ready yet.
Do not use self-signed certificates in production. They are open doors for man-in-the-middle attacks because the attacker can easily replace them and then sniff the network traffic between the browser and the server.
Tampering with the Ticket
After stealing your authentication cookie, the attacker will try to see what you have in it. Fortunately, by default, the ticket is encrypted with the machine key, so the attacker cannot read the values in the cookie.
The next question is whether the attacker can alter the cookie. The structure of the cookie is public, and a finite number of encryption algorithms are used by the .NET Framework. Can attackers modify the cookie, or can they create a nice-looking cookie from scratch? Fortunately, ASP.NET also protects your application against this kind of attack by adding a server-side secret value to the ticket hash. Even if the attackers know all values and algorithms, they can't bake a fresh cookie without knowing this server-side secret.
So, by default, you are safe from these two types of cookie-manipulation attacks. However, you can turn off these protections (but you should never do so) in the protection attribute of the forms tag in the web.config file. This setting is inherited from the machine-wide settings. Hence, if you want to ensure that your application uses encryption and validation independently from other applications on the server, you should set this explicitly in your local web.config file, as shown here:
<authentication mode="Forms">
<forms protection="All" ... />
</authentication>
I recommend using this setting explicitly in your web.config file because it makes your application independent from server-level settings, and it is easy to check during a code review.
Hijacking the Login Session
Earlier in this chapter, you saw that the key to a successful authentication is not the user's password, but the authentication ticket. If attackers can get a valid authentication cookie that contains the ticket, and can send it back to the server, and the application accepts it, they can act on behalf of the original user. In other words, attackers can take control of the login session, and, for this reason, this kind of attack is called the login session hijacking.
Unfortunately, the built-in forms authentication mechanism in ASP.NET is vulnerable to this type of session hijacking. You can test it by following these steps:
1. Start Fiddler to capture all traffic that runs between your browser and the website.
2. Open your favorite browser, and sign in to an ASP.NET website you want to test.
3. Inspect the HTTP 302 Redirect response in Fiddler that contains a Set-Cookie HTTP header. If the name of the cookie is .ASPXAUTH, the site most likely uses the default forms authentication implementation.
4. Copy the value of the cookie (the long unreadable numbers after the = sign) to the clipboard. Now you have the authentication cookie that you have just captured from your own traffic. Malicious attackers must sniff the network to get this cookie, but finally they have the same data you have just copied to the clipboard. In the following steps, you test whether the application accepts this cookie if it is sent back from a different client.
5. Start Lens and enter the URL of the website in the Target URL textbox.
6. Switch to the Session Fixation tab that provides various session-related tests. Enter the name of the cookie (most likely .ASPXAUTH) into the Cookie Name textbox, and paste the value from the clipboard in the Cookie Value field, as shown in Figure 3.8.
7. Click any of the enabled Save To buttons to create a session cookie with the given value directly in the local cookie store of the browser you selected. This cookie attaches to the request the next time you open the website in that browser. To successfully test your application against session hijacking, you must select a different browser than what you used in Step 2. In this way, you can simulate that the cookie is sent back from a different location.
The term location at this point means a different browser, and it is a valid test scenario because the cookie stores of the browsers are fully isolated. If you want to test for a different client IP address, you can start Lens on a different computer and follow the same steps to create the authentication cookie on that machine.
8. Open the browser that you chose in Step 7, and simply visit the website you are testing. The browser automatically attaches the previously stored cookie to the request, and, if the website accepts it, you will be immediately signed in as the original user you used in Step 2. That means that your application is vulnerable to login session hijacking. If the website does not treat you as a signed-in user, or displays an error message, your website is safe from session hijacking.
Figure 3.8 Creating an authentication cookie in Lens
By default, all ASP.NET applications that use the built-in authentication implementation with the default configuration are vulnerable to this attack.
Protecting Your Application Against Login Session Hijacking
Unfortunately, ASP.NET does not provide a configuration setting or any built-in magic switch to protect your application against login session hijacking. Although you can mitigate the risk using HTTPS encrypting, the traffic does not solve the original problem. You must write some code to strengthen your application.
The original problem is that the cookie is not bound to a single client. If the website can detect that the cookie is sent back from a different client, the site is safe. Standard cookies do not provide any way to check the original owner of them, so the only option is to add some additional information to the content of the cookie.
First, you must determine what data can fulfill your needs. For example, you can use the User-Agent header that identifies the browser, and also the IP address of the client for this purpose.
Both the User-Agent header and the IP address can help you raise the barrier that attackers meet when they attack your application. However, you should know that, unfortunately, none of these solutions are perfect in all circumstances. The User-Agent header can change if the users mask their browser for compatibility reasons. (This happens quite often among advanced Opera users.) The IP address of the client may also change, if it has a dynamic address, or switches networks — for example, a mobile computer, virtual private network (VPN), or wireless network. Even so, I still recommend that you use these values, but be prepared with a friendly error message to the users because there can be valid actions that modify these values.
As you saw in the “Deep Dive into ASP.NET Authentication” section earlier in this chapter, the cookie contains an encrypted ticket, and you also learned about the structure of the ticket. Luckily, the FormsAuthenticationTicket class provides a standard way to add additional information to it in theUserData property. This is a simple string property, and you can store anything in it because it is not used by the ASP.NET runtime. Just like any other property value of the ticket, it will be encrypted, and also be protected from tampering.
First, you must override the default cookie-creation mechanism of ASP.NET, and that requires you to add some custom code to the Login control, and omit the RedirectFromLoginPage method of the FormsAuthentication class.
To create a custom cookie, start by asking ASP.NET to create a standard one on your login page, just after you validate the user's credentials:
HttpCookie cookie = FormsAuthentication.GetAuthCookie
( userName, chkRememberMe.Checked );
Code file [Authentication\Login.aspx.cs] available for download at Wrox.com.
This cookie contains the ticket that you can access by decrypting the cookie value:
FormsAuthenticationTicket oldTicket = FormsAuthentication.Decrypt( cookie.Value );
Code file [Authentication\Login.aspx.cs] available for download at Wrox.com.
With the properties of the original ticket, you can create a new one, with the only difference being that you set the UserData property this time:
FormsAuthenticationTicket newTicket = new FormsAuthenticationTicket(
oldTicket.Version, oldTicket.Name, oldTicket.IssueDate,
oldTicket.Expiration, oldTicket.IsPersistent, userData );
Code file [Authentication\Login.aspx.cs] available for download at Wrox.com.
The userData variable can contain any additional information you want to embed into the ticket, such as the User-Agent and the IP address of the client. Besides the values you add for security reasons, you can embed additional values you need for every request, but for performance reasons you do not want to store or look up on the server every time. For example, the following code adds the user's role, unique identifier, and e-mail address to the cookie:
string userAgent = HttpContext.Current.Request.UserAgent;
string clientIP = HttpContext.Current.Request.UserHostAddress;
string userData = String.Format( CultureInfo.InvariantCulture,
"{0}|{1}|{2}|{3}|{4}", clientIP, userAgent, role, userId, email );
Code file [Authentication\Login.aspx.cs] available for download at Wrox.com.
In the next step, you encrypt the new ticket:
cookie.Value = FormsAuthentication.Encrypt( newTicket );
And, finally, you add the new encrypted ticket to the HTTP response:
Response.Cookies.Add( cookie );
Code file [Authentication\Login.aspx.cs] available for download at Wrox.com.
To redirect the user to the original page, do not call the RedirectFromLoginPage method because it always creates the standard authentication cookie. You should do that in two steps:
string redirectUrl = FormsAuthentication.GetRedirectUrl( userName, false );
Response.Redirect( redirectUrl );
Code file [Authentication\Login.aspx.cs] available for download at Wrox.com.
With these steps, you have created a standard authentication cookie that behaves just like the original one, but it contains additional information about the client. The next step is to validate this information when the cookie is sent back by a client with the next HTTP request.
The best place to check the UserData is the Application_PostAuthenticateRequest event in the global.asax file. This event handler is called right after the FormsAuthenticationModule validated the other fields of the ticket, and here you can access its results. You can start by getting the identity of the user if the user was successfully authenticated by the forms authentication module:
IPrincipal user = HttpContext.Current.User;
if( user.Identity.IsAuthenticated &&
user.Identity.AuthenticationType.Equals( "Forms",
StringComparison.OrdinalIgnoreCase ) )
{
FormsIdentity formsIdentity = HttpContext.Current.User.Identity as FormsIdentity;
// Next code lines come here…
}
Code file [Authentication\Global.asax] available for download at Wrox.com.
The FormsIdentity class enables you access the ticket and the UserData value submitted by the client:
string userData = formsIdentity.Ticket.UserData;
Code file [Authentication\Global.asax] available for download at Wrox.com.
The userData field contains the IP address and the User-Agent of the client who originally requested the cookie. All you must do now is to compare these values with the values of the current client:
string[] userDataParts = userData.Split( ‘|’ );
string clientIP = userDataParts[ 0 ];
string userAgent = userDataParts[ 1 ];
PortalRole role = (PortalRole) Enum.Parse( typeof( PortalRole ),
userDataParts[ 2 ] );
int userId = Int32.Parse( userDataParts[ 3 ] );
string email = userDataParts[ 4 ];
HttpRequest req = HttpContext.Current.Request;
if( !req.UserHostAddress.Equals( clientIP, StringComparison.OrdinalIgnoreCase ) )
throw new SecurityException();
if( !req.UserAgent.Equals( userAgent, StringComparison.OrdinalIgnoreCase ) )
throw new SecurityException();
Code file [Authentication\Global.asax] available for download at Wrox.com.
For simplicity, the previous code snippet just throws a SecurityException, but you have to be sure that these events are logged and a friendly error message is displayed to the user. But be careful not to provide too much information about the error that can help the attacker.
The previous code snippet demonstrates how you can store any value in the login cookie. For example, because ASP.NET gives you access only to the login name of the current user, it can be also useful to cache the user's internal ID in the cookie to simplify database operations. You can also create custom identity and principal classes that implement the IIdentity and the IPrincipal interfaces, and you can make these values available to every part of your website by assigning them to the current HTTP context and the current thread:
MyPortalIdentity portalIdentity = new MyPortalIdentity( user.Identity.Name,
userId, role, email );
MyPortalPrincipal portalPrincipal = new MyPortalPrincipal( portalIdentity );
HttpContext.Current.User = portalPrincipal;
Thread.CurrentPrincipal = portalPrincipal;
Code file [Authentication\Global.asax] available for download at Wrox.com.
After you add this workaround to your code, do not forget to retest your solution with Fiddler and Lens, as described in detail earlier in this chapter.
Cross-Site Request Forgery
As a security-conscious web developer, you already know about XSS attacks that rely on the user's trust for the website. In this section, you learn about another type of attack, which is the inverse of XSS, which exploits that the website trusts the user.
Most websites protect sensitive operations by requiring authentication. For example, if you have an online banking site, it can provide the following URL that enables the transferring of money:
http://bank.example.com/Transfer?To=111-222-333&Amount=1000
As you can see, the query string contains the target bank account number (the To field) and the amount of money to transfer (the Amount field). You can send an HTTP GET request to this URL, but, of course, the website checks that you have logged in before and processes the request only if you are successfully authenticated.
But how does the website know that you are already signed in? It checks if the request contains a login cookie. Therefore, if you send an HTTP GET request to this URL and attach a valid login cookie to the request, the bank completes the transfer. Accordingly, if attackers can send an HTTP GETrequest to a similar URL that contains their bank account number as the target and attach your login cookie to the request, your money will be stolen by the bad guys.
If you read the previous sections of this chapter, you already know how to protect your login cookie from being stolen and sent back from another location. So, the only option left for the attacker is to force you to send a request to a maliciously crafted URL and attach your login cookie to it. You would be shocked at how easy it is. All the attackers need is a webpage where they she can embed a simple image tag:
<img src="http://bank.example.com/Transfer?To=666-666-666&Amount=9999" />
This forces your browser to send an HTTP GET request to the bank's website asking for a transfer to the attacker's bank account. And here comes the best part — if your browser has a cookie for bank.example.com, the browser will be more than happy to attach it to the request, and the website sees it as an authenticated request from you.
You probably noticed the Remember Me check box on the login page of many websites. If you check this option, it asks the website to create a persistent cookie. This type of cookie is stored not only in the browser's memory, but also on disk, so it survives browser or system restarts. The website remembers you because the browser remembers the cookie. So, if you logged in to your bank site a week ago and checked the Remember Me check box, your authentication cookie still sits on your disk and will be sent with your request to save you from re-authenticating yourself.
If you have a previously stored persistent login cookie and visit a website that contains the image tag just described, you will be the victim of this attack without even noticing it. A single click can be enough, and that's why this kind of attack is often called a one-click attack, or, more formally,cross-site request forgery (CSRF).
There are two things to consider here. Firstly, although the name one-click attack is widely used for CSRF attacks, using them as synonyms is technically not absolutely correct. One-click attacks are actually a subset of CSRF. Secondly, this example used an HTTP GET target, but the attack does not bound to GET requests. POST requests are also vulnerable; although, they require a bit more coding to exploit.
Protecting Against CSRF Attacks
As you saw earlier, persistent cookies facilitate CSRF, so your first idea could be to completely disable persistent cookies to protect your application. Although it actually mitigates this attack, my experience is that users like and use this feature, especially in websites they frequently access.
The root problem with CSRF is that the website trusts the user. If a valid request is received from the client, the website treats it as if it were intentionally sent by the user. The question is how the website can differentiate between intentional requests and requests sent by the browser without the user's consent. That's a difficult problem, because, from the server's standpoint, an intentional and a malicious request look absolutely the same.
The other characteristic of CSRF is that the attacker can build a fully valid request beforehand, which means that the attacker formerly knows every single value that the server expects in a request. By making all requests a bit different with values that the attacker can't find out, you can mitigate this attack.
One common part of every POST request in an ASP.NET website is the view state. ASP.NET provides a built-in mechanism, the ViewStateUserKey property, to add additional protection to the view state by adding any arbitrary value to it that is checked when the integrity of the view state is validated by the runtime. If you add an extra value that ties the view state to the current user or to the current session, you can raise the barrier for a successful CSRF attack.
The ViewStateUserKey property can be set only in the Page_Init phase of the page life cycle, as you can see in the following code snippet:
protected override void OnInit( EventArgs e )
{
base.OnInit( e );
if( this.User.Identity.IsAuthenticated )
{
this.ViewStateUserKey = this.Session.SessionID;
}
}
Although this setting raises the barrier for CSRF attacks, it does not protect you in all situations:
· The ViewStateUserKey protects only pages that have view state enabled.
· The ViewStateUserKey is not checked when view state Method Authentication Code (MAC) validation is turned off.
· The view state MAC (and the ViewStateUserKey with it) is checked only for POST requests. Therefore, you must ensure that all your GET requests are idempotent.
· Unfortunately, ASP.NET ignores HTTP verbs when processing form values, so the ViewState can be posted back also in a query string parameter to completely bypass the MAC validation.
What's more, when the user's session times out, the runtime throws a view state MAC validation error that you must gracefully handle.
Additional Protection Against CSRF
The anti-CSRF solution you learned earlier heavily relies on the ASP.NET view state with its advantages and disadvantages. However, there are situations in which you do not want to use view state, or you cannot use view state.
For example, there is no view state in the ASP.NET Model-View-Controller (MVC). Fortunately, the MVC library out-of-the-box provides a mechanism to protect your application against CSRF via the AntiForgeryToken helper.
In this case, you can use any other parts of the HTTP traffic to make your requests and responses unique. For example, you can add a random value to a hidden field on the page and store the same value in the session on the server side. Later, when a request comes in, you can check that these two values are equal. This approach is implemented as a free downloadable HttpModule in the .NET CSRF Guard project of Open Web Application Security Project (OWASP) from www.owasp.org/index.php/.Net_CSRF_Guard.
However, if your application does not rely on sessions, turning it on only for anti-CSRF is a big overhead. The AntiCSRF module (also downloadable for free from http://anticsrf.codeplex.com) solves this problem by storing the session-specific secret in a form field and in a session cookie, and mandating that the two values be the same for every request.
If you implement a secret-based protection like the .NET CSRF Guard or the AntiCSRF module, ensure that your application is free from XSS vulnerabilities, and that your GET requests are idempotent.
Attacking the ASP.NET Session
At the beginning of this chapter, you learned about the two kinds of sessions ASP.NET supports: the classic session and the login session. After learning about the login session, it's now time to learn about various attacks against the simple session.
ASP.NET Session Under the Covers
The ASP.NET session is managed by an HTTP module (called the SessionStateModule) that works independently from the authentication modules. When a new client connects to your website, this module generates a new session identifier, and stores this ID in an HTTP cookie or in the URL, related to the cookieless attribute of the sessionState tag in the web.config file. This section focuses on the default cookie-based sessions.
In contrast to the login sessions, the value stored in the cookie is not encrypted and not protected with a hash. It is just the raw session ID. ASP.NET first generates a 15-byte random ID and then encodes it to a 24-character string that can be used directly as a cookie value.
Besides this, there are many similarities between the authentication and the session, and, therefore, similar attacks can target them.
Guessing the Session ID
As you have seen, the heart of the session is the session ID. If the attackers can get the session ID, they can build a session cookie and can send it to the website to steal the session. In most web platforms, the session ID is an integer number. If the session ID numbers are created sequentially or with a simple random-number generator, the attackers can look up their own session ID and have a fairly good guess about the IDs currently used by other users of the website.
Fortunately, ASP.NET does not use incremental session numbers but generates the ID with the .NET Framework's built-in RNGCryptoServiceProvider. This class produces cryptographically strong random numbers that are really, really difficult to guess. The number is 15-bytes long, so it would also take a long time to find an existing session ID by probing.
Stealing the Session Cookie
If attackers can't guess your session ID, they must steal your session cookie to access the ID. Just like with the login cookie, a classic method to grab it is XSS. Fortunately (again, just like with the login cookie), ASP.NET by default adds the HttpOnly attribute to the cookie. Therefore, it cannot be accessed by the webpage, which makes it difficult to steal via XSS.
Another way the attacker can access your session cookie is by sniffing the network. If you connect to the website through nonsecure HTTP, the requests and the responses are sent in clear text, so any access to the communication can reveal your session cookie.
If you use sessions in your ASP.NET application, the first response after you store any value in the Session dictionary contains your session cookie. From that time, your browser attaches the cookie to every HTTP request it sends to the server. Because the cookie is almost always on the wire, the attackers can capture it even if they have access to the HTTP traffic for a limited time.
Unfortunately, ASP.NET does not provide any built-in configuration feature to force the browser to transmit the session cookie only through HTTPS, and never on HTTP. It is up to you, the developer, to architect your application to secure the session cookie because out-of-the-box, every ASP.NET application is vulnerable to session hijacking.
Testing Your Application Against Session Hijacking
As you have learned, neither the cookie nor the server stores any additional information about the client. The session ID alone is enough to get access to anyone's session. So, if attackers possess the cookie, they can send it back to the website from any other computer because the session is not bound to any particular client.
Follow these steps to test your web application against session hijacking:
1. Start Fiddler to capture all traffic that runs between your browser and the website. Do some actions on the site that store some value in the session (for example, put an item in your shopping basket if the site is for web shopping).
2. Study the communication log, and look for a Set-Cookie HTTP header in the response, or Cookie header in the request that has the name ASP.NET_SessionId.
3. Copy the 24-character value of the cookie to the clipboard.
4. Start Lens, and enter the URL of the website in the Target URL textbox.
5. Switch to the Session Fixation tab that provides various session-related tests. By default, the Cookie Name textbox contains ASP.NET_SessionId, so most likely, you won't have to change it. Paste the value from the clipboard to the Cookie Value field
6. Click any of the enabled Save To buttons to create a session cookie with the given value directly in the local cookie store of the browser you selected. This cookie will be attached to the request the next time you open the website in that browser. To simulate that you submit the session cookie from a different location, select a different browser from the one you used in Step 1 to generate the traffic.
7. Open the browser that you have chosen in the previous step, and simply visit the website you're testing. The browser automatically attaches the previously stored cookie to the request. If the website accepts it, you see the same state you created in the first browser. (For example, the item will be in your basket.) This means your application is vulnerable to session hijacking. If the website does not show your original state, or displays an error message, your website is safe from session hijacking.
Protecting Your Website Against Session Hijacking
The root problem of session hijacking is that the cookie is not bound to a particular client. This was also the root problem for the login session hijacking, and you have already learned how you can add additional protection by storing the IP address and the User-Agent of the client in the cookie. The same method works here, too. However, the implementation is completely different because the built-in SessionStateModule does not provide the level of extensibility you have with the FormsAuthenticationModule.
To fix the session management, you can create a new HTTP module that sits in the ASP.NET pipeline in front of the SessionStateModule. When the website first generates the session cookie, this new module modifies the session cookie and attaches some additional information to it, just before the cookie is sent to the client. When the browser submits the cookie back to the website, it is first examined by the new module, and the additional information is checked. If the extra data is valid, it is removed, and the original session cookie value is passed to the SessionStateModule. If the validation fails, the new front-end module terminates the execution. The additional data can be any information that identifies the original client. (I recommend that you to use the IP address and the User-Agent of the browser.)
Figure 3.9 shows the architecture of the solution with the custom module highlighted:
Figure 3.9 The custom SecureSessionModule in the ASP.NET pipeline
To implement this solution, first you must create a new class that implements the IHttpModule interface. In the Init phase of the execution, you must subscribe to the BeginRequest and EndRequest events to tamper with the request and the response.
public class SecureSessionModule : IHttpModule
{
public void Init( HttpApplication context )
{
context.BeginRequest += new EventHandler( this.OnBeginRequest );
context.EndRequest += new EventHandler( this.OnEndRequest );
}
}
Code file [Session\SecureSessionModule.cs] available for download at Wrox.com.
The first time the website creates a session cookie, it will be sent to the browser in a HTTP response. In the OnEndRequest handler, you can access this cookie and add a MAC to it:
public void OnEndRequest( object sender, EventArgs e )
{
HttpCookie cookie = this.GetCookie( HttpContext.Current.Response.Cookies );
if( cookie != null )
{
string mac = this.GenerateMac( cookie.Value, HttpContext.Current.Request );
cookie.Value += mac;
}
}
Code file [Session\SecureSessionModule.cs] available for download at Wrox.com.
The previous code uses two helper methods. The first one is GetCookie that is used to find the ASP.NET session cookie in the HTTP response:
private HttpCookie GetCookie( HttpCookieCollection cookies )
{
for( int i = 0; i < cookies.Count; i++ )
{
HttpCookie cookie = cookies[ i ];
if( cookie.Name.Equals( "ASP.NET_SessionId",
StringComparison.OrdinalIgnoreCase ) )
{
return cookie;
}
}
return null;
}
Code file [Session\SecureSessionModule.cs] available for download at Wrox.com.
At first, it may seem to be a bit lengthy, but believe me, it is necessary. In this phase of the execution, using the indexer of the HttpCookieCollection may have side effects, and using a foreach loop instead of the for loop may throw exceptions.
The second helper function is the GenerateMac private method. This method is responsible for generating a Hash-based Message Authentication Code (HMAC) that contains the sensitive properties of the client. Because, unlike the authentication ticket, the session cookie is not encrypted and not signed, it is up to you to ensure that the values cannot be tampered with by the attacker. An HMAC is a kind of hash, which uses a server-side secret to protect the data from regeneration by a malicious user. The GenerateMac method utilizes the built-in System.Security.Cryptography.HMACSHA512 class of the .NET Framework to calculate the HMAC.
private string GenerateMac( string sessionID, HttpRequest request )
{
string content = String.Format( CultureInfo.InvariantCulture, "{0}|{1}",
request.UserHostAddress, request.UserAgent );
byte[] key = Encoding.UTF8.GetBytes( "Any server side secret..." );
using( HMACSHA512 hmac = new HMACSHA512( key ) )
{
byte[] hash = hmac.ComputeHash( Encoding.UTF8.GetBytes( content ) );
return Convert.ToBase64String( hash );
}
}
Code file [Session\SecureSessionModule.cs] available for download at Wrox.com.
After the HMAC containing the IP address and the User-Agent of the client is calculated, it is concatenated to the original cookie value in the OnEndRequest handler. This new value is sent to the client and is sent back to the server with the next HTTP request.
When a new request comes in, the OnBeginRequest handler is executed. This handler examines the request, and if it contains a session cookie, it validates the HMAC by generating a new HMAC, and comparing it with the values passed by the client. If the values differ, it may indicate a session-hijacking attack. Finally, if the HMAC is valid, the HMAC is removed from the cookie before the ASP.NET session module uses it. The implementation of the OnBeginRequest method is shown in the following code snippet:
public void OnBeginRequest(object sender, EventArgs e)
{
HttpRequest request = HttpContext.Current.Request;
HttpCookie cookie = this.GetCookie(request.Cookies);
if( cookie != null )
{
string value = cookie.Value;
if(!String.IsNullOrEmpty(value))
{
string sessionID = value.Substring(0, 24);
string clientMac = value.Substring(24);
string serverMac = this.GenerateMac(sessionID, request);
if( !clientMac.Equals(serverMac, StringComparison.OrdinalIgnoreCase) )
{
throw new SecurityException("Hack!");
}
// Remove the MAC from the cookie before ASP.NET uses it.
cookie.Value = sessionID;
}
}
}
Code file [Session\SecureSessionModule.cs] available for download at Wrox.com.
After you create the new SecureSessionModule, open your web.config file, and add it to the ASP.NET request-processing pipeline:
<configuration>
<system.web>
<httpModules>
<add name="SecureSessionModule" type="SecureSessionModule"/>
</httpModules>
</system.web>
</configuration>
As mentioned during the discussion of the login session hijacking, using the IP address and the User-Agent header can help to raise the security barrier, but they are not perfect solutions in all circumstances.
Session Fixation
If attackers can't discover your session ID after you first have it, they still have the option to discover the session ID even before you use it. In other words, if attackers can force a user's browser to use a pre-set session ID determined by the attackers, the attackers have no need to steal the session cookie anymore — they already knows what is in it!
This kind if attack is called session fixation, and Figure 3.10 shows how it works:
1. A malicious user, Mallory, finds a way (for example, through a browser vulnerability or external application) to store a new cookie for a website in the cookie store of Alice's browser, with the name of ASP.NET_SessionId and with the value of 123451234513245123451234.
2. The next time Alice visits the website, the browser attaches the cookie from the cookie store.
3. Mallory stores the same session cookie in her own browser, and when she visits the website, she can connect to the same session that Alice uses.
Figure 3.10 Session fixation
Unfortunately, by default, ASP.NET is vulnerable to session-fixation attacks. But examining what makes the platform susceptible to this kind of attack can help you to mitigate it.
Protecting Your Application Against Session Fixation Attacks
The first problem is that the attacker knows everything about the cookie: its name and the format of the value it should contain. The first step to raise the barrier is to change the name of the cookie from the default ASP.NET_SessionId to something unique in the web.config file:
<sessionState cookieName="MyCookieName" />
The second step is to generate a custom cookie value. By default, ASP.NET accepts any 24-character-length cookie value that contains characters from “a” to “z” and “0” to “5.” If you want to generate a custom cookie value, you can do it by creating a new session ID manager that implements the System.Web.SessionState.ISessionIDManager interface.
The built-in implementation is the SessionIDManager class that takes care of even the cookieless operation. Therefore, if you want to change only the way the session ID is created, you can derive directly from the SessionIDManager base class and override the CreateSessionID method:
public class MySessionIdManager : System.Web.SessionState.SessionIDManager
{
public override string CreateSessionID( System.Web.HttpContext context )
{
// Custom session ID creating goes here...
}
public override bool Validate( string id )
{
// Validation logic goes here...
}
}
Obviously, if you change how you generate the session ID, you must change how the incoming values are validated. In the previous code, you can see the Validate method, which can also be overridden to serve this purpose. The platform implementation of the Validate method checks only the format of the session cookie value. But if you want to protect your application against session fixation, you must add an additional layer of security. You must check that the session ID submitted by the client was previously created by the CreateSessionID method for the current client.
After you have your custom session ID manager implementation, you must configure it in the web.config file:
<configuration>
<system.web>
<sessionState sessionIDManagerType="MySessionIdManager" />
</system.web>
</configuration>
So far, you were dealing with only cookie-based sessions, but ASP.NET also supports cookieless sessions. The support for cookieless sessions was added to the platform to support clients that disable cookies, or cannot manage cookies at all (for example, early mobile browsers), but thankfully that era has ended. Because cookieless sessions store the session identifier in the URL (and that makes the attacker's job easier), I strongly recommend that you to work only with cookie-based sessions and completely disable the cookieless fallback in the web.config file:
<sessionState cookieless="UseCookies" />
To protect your application against session fixation and session hijacking, you can combine the methods described in the “Protecting Your Website Against Session Hijacking” section earlier in this chapter with a custom session ID manager.
Hacking the View State
One of the biggest advantages of ASP.NET over other web platforms is that web controls automatically preserve their state across postbacks. This feature, which gives tremendous productivity for developers, is provided by the view state. As an ASP.NET developer, you have probably already learned that the view state is actually a property bag that stores the state of the controls on the page, which is later serialized by the runtime into the hidden _VIEWSTATE field. You also probably know what impact the view state has on the page life cycle. In this section, you learn about the security-related side effects of using the view state.
Peeking into the View State
The view state is automatically maintained by the ASP.NET runtime. Web controls and web pages, by default, store arbitrary values in the ViewState and ControlState properties, and the runtime takes care of serializing and deserializing these properties to the hidden _VIEWSTATE field during the page life cycle. Because the values are stored in a form field (which travels to the client as part of the HTML code of the page), you must treat the field as public information for your visitors. Because ASP.NET developers store and retrieve values from the ViewState property conveniently in server-side code, they tend to forget that any data placed in the view state is actually publicly visible on the client.
When you look into the generated HTML markup of your page, you can see that the serialized view state looks something like this:
<input type="hidden" name="_VIEWSTATE" id="_VIEWSTATE" value=
"/wEPDwUKLTkzNjEzMDI5NA9kFgJmD2QWAmYPZBYEZg9kFgICAQ8WAh4EaHJlZgU4
aHR0cDovL2kxLmNvZGVwbGV4LmNvbS9jc3MvdjE3NDUwL2k5NDIxMS9TdHlsZVNoZ
WV0LmFzaHhkAgEPZBYIAgEPZBYCZg8PFgIeCEltYWdlVXJsBTJodHRwOi8vaTEuY2
9kZXBsZXguY29tL0ltYWdlcy92MTc0NTAvbG9nby1ob21lLnBuZ2RkAgIPFgIeBFR
leHQFSjxzY3JpcHQgc3JjPSIvc2l0ZS9saWJyYXJ5L3N2eS9icm9rZXIuanMiIHR5
cGU9InRleHQvamF2YXNjcmlwdCI+PC9zY3JpcHQ+ZAIDDw8WAh8BBS5odHRwOi8va
TMuY29kZXBsZXguY29tL0ltYWdlcy92MTc0NTAvYmxhbmsucG5nFgIeBm9ubG9hZA
UZc2VsZi5sb2dvSW1hZ2VMb2FkZWQ9dHJ1ZWQCBA8WAh4HVmlzaWJsZWdkZNaxicg
/ZsYrWPagVP35MS3HEdiV" />
Although it looks cryptic, it is not encrypted at all. To serialize the objects in the ViewState property, ASP.NET uses the LosFormatter (Limited Object Serialization) class, which is designed for highly compact, ASCII format serialization. Internally, it uses the ObjectStateFormatter class that is capable of serializing any object graph. However, by default, it does not encrypt the output, but only converts it to Base64 to make it suitable for storing in a HTML form field. Because these serializer classes are public, a malicious user can deserialize and decode your view state and peek into its content. If you (or any control you use) store some sensitive data in the view state or in the control state, it may lead to information disclosure, which is an often underestimated but dangerous threat.
You can set the EnableViewState attribute of the @Page directive or the <pages> tag in the web.config file, or any single control to false to completely turn off view state. Turning off the view state is a good idea if you don't need it, but if you do, you must ensure you use it securely. You can also use the ViewStateMode property in conjunction with EnableViewState to accomplish the same.
Testing Your View State Against Information Disclosure
You can use Lens to test your pages against information disclosure in the view state by following these steps:
1. Start Lens, and enter the full URL of the page you want to test into the Target URL textbox. You can click the Open button to verify the URL you entered.
2. Go to the ViewState tab, and click the Extract button to download the page. Snip out the content of the _VIEWSTATE hidden field. The Output pane shows the length of the extracted value, as shown in Figure 3.11.
3. Click the Decode button to decode the downloaded content. This changes the lower pane to a tree view that displays the decoded content. However, you can always switch between the original and the decoded view by clicking the Extracted String and the Decoded Content radio buttons.
4. If you are just looking for a particular string in the view state, you can enter it into the Keyword field, and Lens automatically highlights the tree node that contains the text with red as you type. Figure 3.12 shows the tree view and the keyword highlighting.
Figure 3.11 Using Lens to extract the ViewState from a page
Figure 3.12 The decoded ViewState and keyword search in Lens
The view state you downloaded corresponds with the current state of the current page. You must test every page of your website independently for information disclosure. To test every page state, you can also directly paste the serialized view state value into the “Extracted String view of Lens,” and then click the Decode button to decode it.
Encrypting Your View State
Fortunately, ASP.NET provides a built-in mechanism to encrypt the content of the ViewState property via the ViewStateEncryptionMode property of the Page. This property can have one of the following three values:
· Auto — The view state is encrypted only if a control requests encryption by calling the RegisterRequiresViewStateEncryption method of the Page class. This is the default value. That also implies that if you develop a control that stores sensitive information in the ViewState property, you can call this method to encrypt the value.
· Always — The view state information is always encrypted, regardless of the sensitivity of its content. If you utilize third-party controls that may have access to sensitive object values, you should use this option.
· Never — The view state information is never encrypted, even if a control requests encryption.
You can enable view state encryption in the @Page directive or in the <pages> section of the web.config file.
Some built-in web controls, by default, may request the page to encrypt the ViewState. If you have a FormView, a ListView, a GridView, or a DetailsView on the page, and you set the DataKeyNames property to a non-null value, it asks the page for view state encryption. The reason for this is that these controls store the key field values in the control state, which is being serialized with the view state to the same hidden field.
You can use Lens to test if the view state is encrypted on the page. After extracting the _VIEWSTATE value, when you click the Decode button, an error message in the Output pane displays if the view state cannot be decoded because it is encrypted.
Tampering with the View State
View state has an important role not only in persisting control state, but also in event processing. Change events (for example, the TextChanged event of the TextBox control) don't cause immediate postback, but instead their event handlers are executed later when the form is submitted to the server, based on the content of the view state. If attackers can maliciously tamper with the view state data, they can not only change the state of the controls on the page, but can also even influence which event handlers are executed on the server.
Fortunately, by default, ASP.NET protects the view state against client-side modification with an HMAC. The method is similar to one you saw in the “Protecting Your Website Against Session Hijacking” section earlier in this chapter, using the keys configured in the validationKey attribute of themachineKey section of the web.config file.
Although view state MAC protection can be turned off, I strongly recommend you never do so because it can have serious security consequences. Ensure that the EnableViewStateMac property of the @Page directive and the enableViewStateMac attribute of the <pages> section in the web.config is always set totrue, as shown here:
<pages enableViewStateMac="true" ... />
MAC validation is enforced only on HTTP POST requests, so if the request is not a postback, the ViewState MAC is never checked.
Reposting the View State
When the ASP.NET runtime processes the object graph in the ViewState property, it serializes the type and value of the objects. However, it does not store two additional, important properties of the view state:
· The timestamp when the view state is created or any expiration date to it
· The identifier of the session or the user who visits the page
Because of the lack of this information, an attacker can capture another user's valid view state and can post it back to the server at any later time. In this way, a malicious user can attack your website or abuse the victim's data in the view state. To mitigate this threat, you can use the ViewStateUserKeyproperty of the page, which you already learned about in the “Cross-Site Request Forgery” section earlier in this chapter.
However, if you enable ViewStateUserKey, be aware that the number of view state MAC validation errors may increase on your site, and the following exception will be thrown several times:
HttpException (0x80004005): Validation of viewstate MAC failed. If this
application is hosted by a Web Farm or cluster, ensure that
<machineKey> configuration specifies the same validationKey and
validation algorithm. AutoGenerate cannot be used in a cluster.
In my experience, the most common reason for this exception is that users tend to submit pages after session timeout or login session timeout. In these cases, the view state is encrypted with a key based on the user's session ID that expires by the time the postback occurs, and the runtime throws the exception. Two workarounds may help you to handle this problem:
· Apply the Post/Redirect/Get (PRG) pattern to avoid re-posts when the user refreshes the page in the browser.
· Maintain a JavaScript timer on the client that notifies the user before the session times out to avoid posting back to an expired session.
You must also be sure that view state MAC exceptions are gracefully handled, and a friendly error page is displayed to the visitors of your site. Unfortunately, this error is a general HttpException, but the inner exception is a ViewStateException, and you can check for that type in the Application_Error event handler in the global.asax file of your application:
private void Application_Error( object sender, EventArgs e )
{
Exception ex = this.Server.GetLastError();
if( ex is HttpException && ex.InnerException != null &&
ex.InnerException is ViewStateException )
{
// TODO: Log the error here...
// Clear the error.
this.Server.ClearError();
// TODO: Redirect the user to a friendly error page here...
}
}
Tricking Event Handlers
Earlier in this chapter, you learned how a malicious attacker can exploit the weaknesses of the ASP.NET authentication, session, and view state mechanisms. The last part of this chapter discusses another feature of ASP.NET used on almost every single web page: event handling.
Web controls and event-based programming are probably the most important features of ASP.NET that promoted the success of this platform, by hiding the low-level protocol details, and making web development as simple as desktop application development. To provide this level of abstraction, ASP.NET creates one big hidden form on the page, which is submitted to the server when a postback event occurs on the client. The way the form is submitted depends on the control that causes the postback:
· If the control is a Button or an ImageButton, the form is submitted by the browser when the button is activated. These controls, by default (when the UseSubmitBehavior property is true), generate standard input elements with type=”submit” or type=”image” attributes.
· If the control that triggers the postback is a LinkButton, a CheckBox, or a RadioButton with AutoPostBack=”true” attribute, the form is submitted by a tiny client-side JavaScript generated by the ASP.NET runtime. This JavaScript function (called _doPostBack) fills two hidden form fields (the _EVENTTARGET and the_EVENTARGUMENT fields) with the ID of the control that caused the postback, and with any additional event parameters, respectively.
If you capture the HTTP POST request generated by the browser, you can clearly see these values in the HTTP body (irrelevant header lines omitted):
POST http://localhost:1124/Default.aspx HTTP/1.1
Host: localhost:1124
_EVENTTARGET=LinkButton1&_EVENTARGUMENT=&_VIEWSTATE=%2FwEPDwUKMTQ1OTQ0MTY
yOWRknEdK5e5oeG%2BwlkzU3XqnsLvH%2FZSyW6p6j%2FcIsjMRJMo%3D&_EVENTVALIDATION=
%2FwEWAgKT14CuCgLM9PumD6UtsOk9JGNhr%2FHBC0YgaY3a5ZVBFa5QvTO1fI%2BOTzKp
Because a client-side script is responsible for filling these form fields, the values can be manipulated by a malicious user before they are sent to the server. Luckily, ASP.NET provides a built-in mechanism, called event validation, that provides some level of protection against this type of modification.
Event Validation Internals
When a web control is rendered, it may call the RegisterForEventValidation method of the ClientScriptManager class. This method computes the hash code of the client ID of the control and the hash code for the event argument, and then XORs these values. The resulting hash is added to an internal ArrayListthat is serialized to the hidden _EVENTVALIDATION form field on the page when the page is rendered. The serialization is performed by the ObjectStateFormatter class, the same class used by the ViewState serialization. This also means that the event-validation data is also protected with an HMAC, and when theViewState is encrypted, the _EVENTVALIDATION field also gets encrypted.
When the form data is posted back to the server, the runtime calls the ValidateEvent method of the ClientScriptManager that decodes the _EVENTVALIDATION field content and ensures that the postback was triggered by a valid control with a valid argument value.
Turning Off Event Validation
A common scenario for developers confronted with event validation is client-side scripting. For example, if you create an empty DropDownList control, and then load the items with JavaScript, the server may throw an “Invalid postback or callback argument” exception, because the items added on the client side are missing from the _EVENTVALIDATION field.
An easy workaround is to turn off event validation, but, unfortunately, this cannot be done on the control level, but only on the page level. Because this is an open call for hackers, I strongly do not recommend doing so, and because of that, I won't even show you how to do it. Although there are perfectly valid times when it is okay to turn it off, you should think twice before you even search for the corresponding property.
On the other hand, you can also call the Page.ClientScript.RegisterForEventValidation method to register your dynamically created controls or values.
Hacking Event Validation
The algorithm described earlier looks practical, and it helps to protect your application against malformed POST attacks. Unfortunately, there are two problems with the implementation:
· The implementation is not consistent across the various controls. For example, the Button control enables postback even when it is disabled.
· Just like the _VIEWSTATE, the _EVENTVALIDATION state is not bound to a particular page state.
Both of these problems can have serious consequences.
Pushing the Disabled Button
If you look into the source code of the AddAttributesToRender method of the Button class, you can see that the RegisterForEventValidation method is called independently from the value of the Enabled property of the control. That means the _EVENTVALIDATION field contains a valid reference even for disabled buttons. Therefore, a malicious user can create a request that triggers the Click event handler, even if the button is disabled.
You can try this behavior for yourself by following these steps:
1. Create an ASPX page with a disabled Button and a Label:
<asp:Button ID="Button1" runat="server" Enabled="False"
onclick="Button1_Click" Text="Button" />
<asp:Label ID="Label1" runat="server" Text="Label" />
2. Create an event handler for the Button1 to indicate that the Click event is triggered:
protected void Button1_Click( object sender, EventArgs e )
{
this.Label1.Text = "It works!";
}
3. Start Fiddler, and click the Request Builder tab. Create the following POST request that triggers a postback on the Button1 control:
POST http://localhost:11124/Default.aspx HTTP/1.1
Host: localhost:1124
Content-Length: 196
Content-Type: application/x-www-form-urlencoded
_VIEWSTATE=%2FwEPDwULLTE0MDUxNTAzOTRkZJN4%2FA8LlvP81wb0Bq7KNKVhfUIP3arRKvj7
WYWbp7H1&_EVENTVALIDATION=%2FwEWAwLM68mpDAKM54rGBgK7q7GGCNCQWa9IGCZDaMDYCo2
BAx%2BtsDa1ROsmqXSp7AVvc%2FX4&Button1=Button
The values of the _VIEWSTATE and _EVENTVALIDATION fields in the POST body may be different in your environment, but you can get those values by visiting the page and inspecting the generated HTML markup.
4. Click the Execute button in Fiddler. Then select the newly created session on the left, and switch to the Inspectors tab. If the “It Works!” text displays in the HTTP result, then you successfully pushed a disabled button!
This scenario is simple, but you can use the same method to test your pages against this POST attack. Naturally, fully populated pages produce much more complex POST requests, but you don't have to build them from scratch. You can capture them, and then use the Replay function of Fiddler.
Pushing the Invisible Button
Contrary to the Enabled property, web controls handle the Visible property correctly when they register for event validation. However, there are scenarios in which a malicious user can still trigger invisible controls.
As you learned earlier, event validation is based on the content of the _EVENTVALIDATION hidden form field, which, in turn, is bound to the current page, but not to the current rendered HTML markup. Because of this, a malicious user can capture a generated _EVENTVALIDATION value and post it back with another HTTP POST request.
For example, you can have a Product.aspx page on your website that displays the details of a single product based on an id value in the query string. There is a Discount Order button on your page, but it is visible only for certain products. Because the same page is displayed for all products, attackers can take the _EVENTVALIDATION field from a discounted product, and send it back with a POST request that points to the URL of a full-priced item, and they can order it at the lower price.
Another example of when an attacker can circumvent event validation occurs when the same page is displayed for the user once with controls visible and at other times with controls hidden. Because event validation does not contain any timestamp, a malicious user can save the value of the_EVENTVALIDATION field when the button is visible and post it back later when the button is hidden to trigger the Click event handler of the button.
Both examples are based on the internal behavior of the event-handling and event-validation features in ASP.NET. Because they work the same way in all ASP.NET web pages, an attacker can have a fairly good chance of successfully exploiting them.
Protecting Your Site Against POST Attacks
The farthest a POST attack can get is executing an event handler on the server. It cannot do anything wrong in itself; it can just run code you do not want to execute. Therefore, if you want to protect your web pages against POST attacks, you must add additional layers of protection between the UI and the sensitive code.
If you see some code like this, you should be suspicious of the page being vulnerable to POST attacks:
// If the user is not admin, disable a feature.
if( !this.User.IsInRole( "Admin" ) )
{
this.MyButton.Enabled = false;
}
To strengthen this code, you can not only disable the button, but you can also unsubscribe from the Click event handler using the -= operator:
// If the user is not admin, disable a feature.
if( !this.User.IsInRole( "Admin" ) )
{
this.MyButton.Enabled = false;
this.MyButton.Click -= this.MyButton_Click;
}
To add further protection, you can insert additional checks at later phases of the execution. For example, in addition to disabling the UI, you can run similar validation (in this case, an authorization check) within the event handler:
protected void MyButton_Click( object sender, EventArgs e )
{
if( this.User.IsInRole( "Admin" ) )
{
// Do something serious here...
}
}
If the code in the event handler executes a stored procedure, you can perform a validation check there, too, just before the data is updated in the database.
This kind of approach is called the defense in depth strategy, in which multiple layers of countermeasures protect the integrity of the data on your website.
Summary
In this chapter, you learned about various attacks that can target your website. Even if you write the most secure code on the planet, because your application is built on top of a particular web platform, your site inherits its security strengths and weaknesses.
Just like any other web platform, ASP.NET also has effective built-in protection against the most common web-based attacks. However, ASP.NET is also susceptible to weaknesses because it cannot cover the full range of attacks. Although the ASP.NET platform is robust, malicious users can find ways to exploit its weaknesses. As an ASP.NET developer, you must know about these threats and the appropriate countermeasures to protect your application.
This chapter examined attacks against the ASP.NET authentication, session, ViewState, and event handlers. This chapter has not presented the full picture. These are only the most important pieces of the puzzle. The ASP.NET platform gains new features in every release, and those features may come with their own secure and unsecure parts.
While you have been reading these chapter, security experts around the world are working hard to find new vulnerabilities to exploit your website, or to find ways to protect against them. Remember, security is not a state or a product, but rather a constant process you can repeat over and over.
About the Author
György Balássy teaches web portal development as a lecturer at the Budapest University of Technology and Economics. He is a founding member of the local MSDN Competence Center (MSDNCC), having an important role in evangelizing the .NET platform as a speaker, book author, and consultant. Balássy provided leadership in the foundation of the Hungarian .NET community as a key evangelist on Microsoft events and technical forums, and as the head of the Portal Technology Group in the MSDNCC. He is a regular speaker on academic and industrial events, presenting in-depth technical sessions on .NET, ASP.NET, Microsoft Office development, and ethical hacking, with which he won the Best Speaker and the Most Valuable Professional (MVP) awards in SharePoint and ASP.NET multiple times, and was selected to be a member of the ASPInsiders group. Since 2005, Balássy has been the Microsoft Regional Director in Hungary. You can visit his blog at http://gyorgybalassy.wordpress.com, or reach him at balassy@aut.bme.hu.