Detecting Malice with ModSecurity: CSRF Attacks

This week's installment of Detecting Malice with ModSecurity will discuss how to detect and prevent Cross-Site Request Forgery (CSRF) Attacks.

Example CSRF Section of Robert "Rsnake" Hansen's book "Detecting Malice" -

One form of attack that is widely found to be present on most websites is cross site request forgery (CSRF). Basically, an attacker can force a victim's browser to connect to your site, and perform functions, like change password, change email address, and so on, unless you have some protection in place to defend yourself. The reason why CSRF is so difficult is because the Internet was designed to allow a user to connect to anything they wanted to from anywhere they wanted to, given there was a routable address. Unfortunately that leads to the possibility of attack. Here's the process flow:

1. Attacker visits a website "site1.com" that he wants to steal user accounts from.

2. Attacker realizes that the website's change password function is vulnerable to CSRF which will allow him to reset people's passwords.

3. Attacker also knows that the administrator of site1.com visits a web-board on "site2.com" which will allow the attacker to insert references to images.

4. Attacker inserts an image tag into site2.com's web-board similar to <img src="http://site1.com/passwordchange?pass=1234&pass2=1234">

5. The administrator of site1.com visits the site2.com's web-board and his browser is immediately redirected back to his own website where he is authenticated.

6. The attacker logs into site1.com with the administrator's username and the password "1234" successfully.

This is only one of dozens of ways to perform a CSRF attack, and to be clear, although the name specifically says "cross" site, it doesn't necessarily have to involve two websites for this same attack to work, if the original site also had a web-board, for instance, that is vulnerable to HTML injection.

Request Validation Tokens

So how can we mitigate CSRF attacks? Detecting Malice suggests the following:

Note: Although it is possible to catch CSRF attacks by observing the logs carefully, as demonstrated above, your site should not allow such attacks to work in the first place. If they do, that only means the site wasn't coded properly, and you should generally seek to determine if it is possible to fix the problem in the code. The preferable method of prevention is to use one time tokens or "nonces" that are unique to every page view, and not known to an attacker. These nonces are sent only once, and verified by the server to ensure that the user could have been the only one who had access to that page. Of course if the page suffers from other attacks that enable an attacker to read the nonce, like XSS, that's another issue.

OWASP has a great project called CSRFGuard which implements a framework for using request validation tokens. I am using the an older version of the JS file in these examples. The main limitation is that the older csrf JS injection code does not update AJAX calls. I haven't had a chance yet to review the new v3 release yet.

OWASP ModSecurity CRS - CSRF Mitigations

The OWASP ModSecurity CRS Project includes an optional rules file that will implement request validation tokens. The file is called modsecurity_crs_43_csrf_protection.conf. Before injecting or validating CSRF tokens, you first need to activate another file called - modsecurity_crs_16_session_hijacking.conf. This is a helper file whose main purpose is to create a Session-based persistent collection using the application's Set-Cookie SessionID data as a key. It will then store the csrf token value in the collection for use in the Content Injection action in the csrf protection file.

## This rule will identify the outbound Set-Cookie SessionID data and capture it in a setsid#SecRule RESPONSE_HEADERS:/Set-Cookie2?/ "(?i:(j?sessionid|(php)?sessid|(asp|jserv|jw)?session[-_]?(id)?|cf(id|token)|sid)=([^\s]+)\;\s?)"      "chain,phase:3,t:none,pass,nolog,capture,setsid:%{TX.6},setvar:session.sessionid=%{TX.6},setvar:tx.ip=%{remote_addr},setvar:tx.ua=%{request_headers.user-agent},setvar:session.valid=1"        SecRule SESSION:SESSIONID "(.*)" "t:none,t:sha1,t:hexEncode,capture,setvar:session.csrf_token=%{TX.1}"

As a demonstration, I setup my local ModSecurity system to be a forward proxy so that I could browse through it to any website. I decided to test out the CSRF Protections against Amazon's site. When I made an initial request for the ModSecurity Handbook page, this transactional data was logged in the ModSecurity audit log file:

--b48e4530-A--[11/Jan/2011:14:22:18 --0500] TSyt6MCoAWoAAIqiGLMAAAAA 127.0.0.1 54004 127.0.0.1 80--b48e4530-B--GET http://www.amazon.com/ModSecurity-Handbook-Ivan-Ristic/dp/1907117024/ref=sr_1_1?s=books&ie=UTF8&qid=1294773447&sr=1-1 HTTP/1.1Host: www.amazon.comUser-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-us,en;q=0.5Accept-Encoding: gzip,deflateAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7Keep-Alive: 115Proxy-Connection: keep-alive
--b48e4530-F--HTTP/1.1 200 OKx-amz-id-1: 0EAZTWRYA937PQMP6HRDp3p: policyref="http://www.amazon.com/w3c/p3p.xml",CP="CAO DSP LAW CUR ADM IVAo IVDo CONo OTPo OUR DELi PUBi OTRi BUS PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA HEA PRE LOC GOV OTC "x-amz-id-2: 7rwkjLgOBJWUFf4XFksZpyuFePZTQUz9QRD6PsgBkVHAqBCrtC8m4KXCV1JuL9CuVary: Accept-Encoding,User-AgentCneonction: closeContent-Type: text/html; charset=ISO-8859-1Set-cookie: session-id=190-2550070-1804850; path=/; domain=.amazon.com; expires=Tue Jan 01 08:00:01 2036 GMT; HTTPOnlyContent-Length: 381900

You can see the Amazon is issuing a Set-Cookie to hold my SessionID in the session-id cookie name. Our previous rule will now capture this session-id value and use it as a key for the Session collection. We also capture important meta-data including:

  • That this session-id value is in deed valid as the application sent it out.
  • Create the csrf token value

This is how it looks in the debug log when we save this data to the Session collection:

Wrote variable: name "__expire_KEY", value "1294778739".Wrote variable: name "KEY", value "190-2550070-1804850".Wrote variable: name "TIMEOUT", value "3600".Wrote variable: name "__key", value "190-2550070-1804850".Wrote variable: name "__name", value "default_SESSION".Wrote variable: name "CREATE_TIME", value "1294775137".Wrote variable: name "UPDATE_COUNTER", value "1".Wrote variable: name "sessionid", value "190-2550070-1804850".Wrote variable: name "valid", value "1".Wrote variable: name "csrf_token", value "ab411ea78241aae1074a2f7587e270f5648a9fa1".Wrote variable: name "ip", value "a90c80bf0f0fc7f79224cd129783eaa26c358d5b".Wrote variable: name "ua", value "f702ef57b6b7ca1e9382e0b2c2e1399ba54793f9".Wrote variable: name "LAST_UPDATE_TIME", value "1294775139".Persisted collection (name "default_SESSION", key "190-2550070-1804850").

Now that we have a csrf token for each session, we can then use the following csrf rules to inject the CSRFGuard JS data into the response:

## This rule will use Content Injection to append the CSRF Token #SecRule &SESSION:CSRF_TOKEN "@eq 1" \"phase:4,t:none,nolog,pass,append:'<html><script language=\"JavaScript\"> \\var tokenName = \'CSRF_TOKEN\'; \var tokenValue = \'%{session.csrf_token}\'; \\function updateTags() { \\        var all = document.all ? document.all : document.getElementsByTagName(\'*\'); \        var len = all.length; \\        for(var i=0; i<len; i++) { \                var e = all[i]; \                \                updateTag(e, \'src\');\                updateTag(e, \'href\');\        } \} \\--CUT--\updateTags(); \updateForms(); \\</script></html>'"

Notice that the ModSecurity append action is being used to inject the anti-csrf token into the response body. The append action uses macro expansion so that the "%{session.csrf_token}" data is properly expanded to the hash value saved in the collection. When injected into the response body, the resulting HTML ending looks like this:

Screen shot 2011-01-11 at 3.29.04 PM

This JS code will update all links and forms and add in the new CSRF_TOKEN value. Here is another screenshot of an updated link. Notice as I mouse over the link that the URL location at the bottom of the browser window shows the link with the updated CSRF_TOKEN value.

Screen shot 2011-01-11 at 3.35.24 PM

When the user then clicks on links or submits forms, etc... the following ModSecurity CSRF rules can validate that the token is present and that it has the correct value for the SessionID.

SecRule &ARGS "@ge 1" "chain,phase:2,t:none,block,msg:'CSRF Attack Detected - Missing CSRF Token.'" SecRule &ARGS:CSRF_TOKEN "!@eq 1" "setvar:'tx.msg=%{rule.msg}',setvar:tx.anomaly_score=+%{critical_anomaly_score},setvar:tx.%{rule.id}-WEB_ATTACK/CSRF-%{matched_var_name}=%{matched_var}"
SecRule &ARGS "@ge 1" "chain,phase:2,t:none,block,msg:'CSRF Attack Detected - Invalid Token.'" SecRule ARGS:CSRF_TOKEN "!@streq %{SESSION.CSRF_TOKEN}" "setvar:'tx.msg=%{rule.msg}',setvar:tx.anomaly_score=+%{critical_anomaly_score},setvar:tx.%{rule.id}-WEB_ATTACK/CSRF-%{matched_var_name}=%{matched_var}"

Inter-Request Latency Thresholds

Use of request validation tokens is the currently accepted mitigation strategy for CSRF flaws, but is it the only way to protect against CSRF? Consider the following common attack scenario where request validation tokens are not used:

  • Attacker identifies a Stored XSS flaw within the target website bank.example.com
  • She sends the following CSRF attack payload -

<img src="http://bank.example.com/withdraw?account=bob&amount=1000000&for=mallory">

  • The victim views then views the page with the Stored CSRF payload
  • The victim's browser processes the HTML IMG tag and immediately sends a GET request back to bank.example.com

How can we identify this type of CSRF attack request? There are some learning aspects that we could leverage (such as learning the expected Request Method) however that is a subject saved for a future blog post. :)

The key transactional element to understand is that normal, non-malicious users use a web application at a certain velocity while automated attacks (such as CSRF payloads) usually trigger immediate follow-up requests.

We can therefore use ModSecurity to alert on quick follow-up requests. It takes real users a bit of time to read a page, decide on search queries, type if form fields, etc... This means that there is normally a several second time delay between "dynamic requests" sent back to the web application. By dynamic, I mean requests that actually carry parameters. As you may have already considered, web browsers almost always send a flood of follow-up requests as the page is being rendered in order to call up CSS, JS and Image files. These types of requests would obviously trigger false positive alerts in this type of monitoring. The OWASP ModSecurity CRS includes an optional rules file to ignore static content requests. Activating this ruleset will not only help with performance, but it will also help to prevent any false positive alerts for this monitoring. Another issue to deal with is that AJAX. Many of today's website use dynamic Web 2.0 technology such as AJAX which will pre-fetch lots of data for users behind the scenes. For example, Amazon makes use of AJAX and will include the following additional Request Header for such requests:

X-Requested-With: XMLHttpRequest

Taking all of these issues into account, we can use the following example rules to help identify these types of basic CSRF attack payloads:

SecAction "phase:4,t:none,nolog,pass,setvar:session.inter-request-latency=1,expirevar:session.inter-request-latency=5"SecRule &SESSION:INTER-REQUEST-LATENCY "@eq 1" "chain,phase:2,t:none,log,deny,msg:'Inter-Request Latency Violation: Potential CSRF Attack.'"        SecRule &REQUEST_HEADERS:X-Requested-With "@eq 0"

The first SecAction directive creates a new variable in the SESSION collection called inter-request-latency, which is set to expire after 5 seconds. The expirevar setting is the key to CSRF detection. 5 seconds is only an initial starting point for testing. This threshold is aimed at identifying follow-up requests that are received in less than 5 seconds from sending out the current response body. The last ruleset is a chained rule. First we check to see if the inter-request-latency variable still exists. If it doesn't, that means that it has been more then 5 seconds since the response body was sent out to this user. If it does still exist, however, then it means that this follow-up request was sent back in very quickly. The 2nd part of the chained rule will then do a check to see if this was an AJAX request by looking for the existence of the X-Requested-With request header. If this request header is missing, it means that this was not an AJAX request and the event should be triggered as a possible CSRF Attack based on the inter-request latency timing violation.

Banking Trojans

It is possible that Inter-Request Latency Monitoring could also provide some protection against banking trojan software such as Zeus in the following scenarios:

  1. When the banking trojan code sends its own requests, behind the scenes, to transfer funds to another account, or
  2. If the banking trojan sends the current SessionID data off to the attacker and he/she starts to simultaneously use the SessionID at the same time.

Keep in mind that this concept is experimental as we have not yet field tested it. It hasn't even made it into the OWASP ModSecurity CRS experimental directory yet! If you do test this out, please let us know how it goes or if you run into any other real-world issues.

Trustwave reserves the right to review all comments in the discussion below. Please note that for security and other reasons, we may not approve comments containing links.