What is the best way to respond to suspicious transactions within your web application? The answer is that it depends in the circumstances and it is certainly not a "One Size Fits All" approach. The reality is that most external web application defensive tools (such as web application firewalls) have a very limited set of response actions. Whether it is a technical limitation of the tool or if it is the end user's specific desire, most WAFs will simply terminate malicious transactions with an abrupt HTTP deny response such as a 403 Forbidden response status code.
This single response action approach is not very flexible and is not desireable from a detectability perspective - see WAF fingerprinting tools such as WAFW00F. It is highly recommended that a WAF's response actions be thoroughly reviewed and ideally tightly integrated with how the application itself should respond to such attacks. Perhaps malicious requests should be met with 302 redirections back to the home page for example. Below is a table listing various Response Actions from the OWASP AppSensor Project:
In the remainder of this blog post - we will show how implement the following AppSensor Response Action:
id |
ASR-J |
title |
Account Logout |
classifications |
Logging, notifying (sometimes), disrupting and blocking | One user | Instantaneous |
category |
Active |
description |
The current session is terminated on the server, and the user is logged out. |
consideration |
Often undertaken in conjunction with process termination (ASR-G) and sometimes lockout (ASR-K). |
examples |
Example 1: Session terminated and user redirected to logged-out message page Example 2: Session terminated only (no redirect) |
code |
- |
This type of response action is ideal for post-authentication scenarios where malicious transactions are identified such as suspected Session Hijacking attacks. By implementing a forces application logout from a WAF - the application SessionID would become invalid and the attacker would then have to reinitiate their attack to obtain a new SessionID from a victim.
There are a variety of response actions with which we could possible use in ModSecurity beyond the standard deny or block. The top candidates for use in a forced application logout scenario are the following.
This action would allow us to issue an HTTP redirect response back to the client and specify a Location reponse header that points to the application's logout URL. While this action may work, the limitation is that if the malicious client itself doesn't honor HTTP redirect responses (such as when they are non-browsers) then this action will not work for our needs.
The proxy action would be ideal is it can intercept the request in transit and act as a traffic cop and send the request to a new location. This happens server-side so the client does not see anything. The advantage with this approach is that the malicious request will already be sending the proper Cookie/SessionID data in the request so by proxying it to the application's logout URL, it is quite easy to force a logout.
While this approach works in theory - there currently exists an Apache mod_proxy bug that may prevent you from using it.
The exec action allows you to externally execute any local script that you would like when a rule matches. With this approach, we can rather easily fire off a curl command from the local Apache host to spoof an application logoff request. This is the action we will demonstrate today.
In order to use an external curl request, you must first profile what an actual application logout request looks like. This is easy enough to do if you have the proper ModSecurity audit logging enabled. For demonstration purposes, we will be using the OWASP WebGoat application as our target.
For this demo, I setup Apache+ModSecurity to be a reverse proxy in front of the OWASP WebGoat application. This ensures that I will have to go through ModSecurity in order to interact with WebGoat. After logging into WebGoat with Basic Auth, I receive the following HTTP response:
HTTP/1.1 200 OKDate: Tue, 21 Jun 2011 18:12:58 GMTServer: Apache-Coyote/1.1Pragma: No-cacheCache-Control: no-cacheExpires: Wed, 31 Dec 1969 19:00:00 ESTContent-Type: text/html;charset=ISO-8859-1Content-Length: 3774Set-Cookie: JSESSIONID=7D955FFF8A5853CACF50BFEBE870BAE4; Path=/webgoat
Notice the application issued a Set-Cookie JSESSIONID that the application now uses to track me. I am then presented with a screen similar to the following:
I can now interact with WebGoat. If I want to issue an application Logout, I would go to this URL -
http://localhost:8080/webgoat/attack?Logout=true
When this happens, my browser sends the following HTTP Request:
GET /webgoat/attack?Logout=true HTTP/1.1Host: localhost:8080User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1Accept: 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-aliveCookie: JSESSIONID=7D955FFF8A5853CACF50BFEBE870BAE4DNT: 1Authorization: Basic Z3Vlc3Q6Z3Vlc3Q=
And then the application will actually issue a new Set-Cookie SessionID.
HTTP/1.1 200 OKDate: Tue, 21 Jun 2011 18:17:16 GMTServer: Apache-Coyote/1.1Pragma: No-cacheCache-Control: no-cacheExpires: Wed, 31 Dec 1969 19:00:00 ESTContent-Type: text/html;charset=ISO-8859-1Content-Length: 3774Set-Cookie: JSESSIONID=381733FF3EDCC311163871059DD73C8F; Path=/webgoat
So, for our scenario, we need to create an external script that ModSecurity can use to send an HTTP request to the application that has the correct URL, Cookie and Authorization data highlighted above.
We can use another ModSecurity action called setenv to export the current transactional request data into Apache ENV data that can then be used from within our logout script. We already have the following from the OWASP ModSecurity Core Rule Set's modsecurity_crs_49_inbound_blocking.conf file:
# Alert and Block based on Anomaly Scores#SecRule TX:ANOMALY_SCORE "@gt 0" \ "chain,phase:2,id:'981176',t:none,deny,log,msg:'Inbound Anomaly Score Exceeded (Total Score: %{TX.ANOMALY_SCORE}, SQLi=%{TX.SQL_INJECTION_SCORE}, XSS=%{TX.XSS_SCORE}): Last Matched Message: %{tx.msg}',logdata:'Last Matched Data: %{matched_var}',setvar:tx.inbound_tx_msg=%{tx.msg},setvar:tx.inbound_anomaly_score=%{tx.anomaly_score}" SecRule TX:ANOMALY_SCORE "@ge %{tx.inbound_anomaly_score_level}" chain SecRule TX:ANOMALY_SCORE_BLOCKING "@streq on" chain SecRule TX:/^\d/ "(.*)"
This ruleset will currently initiate a deny when the transactional anomaly score is above the threshold specified by the ModScurity Admin and the Anomlay Scoring Blocking variable is set. We can externally modify this ruleset using the SecRuleUpdateActionbyId directive to add in the following actions:
SecRuleUpdateActionById 981176:3 "setenv:request_cookies=%{request_headers.cookie},setenv:basic=%{request_headers.authorization}"SecRuleUpdateActionById 981176:3 "exec:/usr/local/apache/conf/logout.sh"
These directives have the effect of transforming the original ruleset to this:
# Alert and Block based on Anomaly Scores#SecRule TX:ANOMALY_SCORE "@gt 0" \ "chain,phase:2,id:'981176',t:none,deny,log,msg:'Inbound Anomaly Score Exceeded (Total Score: %{TX.ANOMALY_SCORE}, SQLi=%{TX.SQL_INJECTION_SCORE}, XSS=%{TX.XSS_SCORE}): Last Matched Message: %{tx.msg}',logdata:'Last Matched Data: %{matched_var}',setvar:tx.inbound_tx_msg=%{tx.msg},setvar:tx.inbound_anomaly_score=%{tx.anomaly_score}" SecRule TX:ANOMALY_SCORE "@ge %{tx.inbound_anomaly_score_level}" chain SecRule TX:ANOMALY_SCORE_BLOCKING "@streq on" chain SecRule TX:/^\d/ "(.*)" "setenv:request_cookies=%{request_headers.cookie},setenv:basic=%{request_headers.authorization},exec:/usr/local/apache/conf/logout.sh"
With this ruleset updated, we can how create the logout.sh script.
This is a very simply shell script that will execute a curl request to the WebGoat URL and use the ENV data exported by the previous rules to add in the appropriate Authorization and Cookie data.
#!/bin/sh/opt/local/bin/curl -s -H "Authorization: $basic" -b "$request_cookies" "http://localhost:8080/webgoat/attack?Logout=true" > /tmp/logout.output echo "0"exit
If your application uses POST request data to pass parameters to the application then you would need to update the curl request flags to properly mimic a browser request.
When the ruleset has identified a high anomaly score, the final rule will initiate the execution of the logout.sh script. Here is an example from the ModSecurity debug log of the final rule processing:
Recipe: Invoking rule 1016cdb38; [file "/usr/local/apache/conf/crs/activated_rules/modsecurity_crs_49_inbound_blocking.conf"] [line "29"].Rule 1016cdb38: SecRule "TX:/^\\d/" "@rx (.*)" "setenv:request_cookies=%{request_headers.cookie},setenv:basic=%{request_headers.authorization},exec:/usr/local/apache/conf/logout.sh"Expanded "TX:/^\d/" to "TX:958052-WEB_ATTACK/XSS-ARGS:Username|TX:958051-WEB_ATTACK/XSS-ARGS:Username|TX:973300-WEB_ATTACK/XSS-ARGS:Username|TX:973307-WEB_ATTACK/XSS-ARGS:Username|TX:0|TX:973331-WEB_ATTACK/XSS-ARGS:Username".Transformation completed in 0 usec.Executing operator "rx" with param "(.*)" against TX:958052-WEB_ATTACK/XSS-ARGS:Username.Target value: "alert("Ignoring regex captures since "capture" action is not enabled.Operator completed in 8 usec.Setting env variable: request_cookies=%{request_headers.cookie}Resolved macro %{request_headers.cookie} to: JSESSIONID=0572B09F434440A3C0334CB5F11176BFSet env variable "request_cookies" to: JSESSIONID=0572B09F434440A3C0334CB5F11176BFSetting env variable: basic=%{request_headers.authorization}Resolved macro %{request_headers.authorization} to: Basic Z3Vlc3Q6Z3Vlc3Q=Set env variable "basic" to: Basic Z3Vlc3Q6Z3Vlc3Q=Exec: /usr/local/apache/conf/logout.shExec: First line from script output: "0"Resolved macro %{TX.ANOMALY_SCORE} to: 25Resolved macro %{TX.XSS_SCORE} to: 25Resolved macro %{tx.msg} to: IE XSS Filters - Attack DetectedResolved macro %{matched_var} to: alert(Warning. Pattern match "(.*)" at TX:958052-WEB_ATTACK/XSS-ARGS:Username. [file "/usr/local/apache/conf/crs/activated_rules/modsecurity_crs_49_inbound_blocking.conf"] [line "26"] [id "981176"] [msg "Inbound Anomaly Score Exceeded (Total Score: 25, SQLi=, XSS=25): Last Matched Message: IE XSS Filters - Attack Detected"] [data "Last Matched Data: alert("]
I simultaneously had ngrep monitoring the traffic and this is how the logout.sh scripts curl request looked:
T 127.0.0.1:58958 -> 127.0.0.1:8080 [AP]GET /webgoat/attack?Logout=true HTTP/1.1.User-Agent: curl/7.21.1 (x86_64-apple-darwin10.4.0) libcurl/7.21.1 OpenSSL/1.0.0d zlib/1.2.5 libidn/1.22.Host: localhost:8080.Accept: */*.Cookie: JSESSIONID=839DD9399E3C573C7005A64C3354F946.Authorization: Basic Z3Vlc3Q6Z3Vlc3Q=..
This request successfully invalidated the current SessionID and logged me out and I was presented with the original homepage screen from above.
While this implementation works most of the time, we have run into a rather common defect with the way in which applications handle an application logout. If the application only attempts to invalidate client-side Cookie data by issue new Set-Cookies in the response headers, this approach may not work. If the application does not also invalidate the SessionID data server-side, then there is nothing preventing attackers from re-using their Cookie data and still getting authenticated access back into the application. It is highly recommended that you test out this scenario to confirm that the application is properly terminating the SessionIDs within the application itself.
If you do run into this defect, then it is possible to simply tag the SessionID as invalid within ModSecurity's local Session persistent storage data. The OWASP ModSecurity Core Rule Set also has a Session Hijacking rule set called modsecurity_crs_16_session_hijacking.conf that verifies that SessionIDs submitted by clients are valid. First it saves the SessionID data within the ModSecurity session collection and marks it as valid:
## 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,id:'981062',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}"
And then it validates the SessionIDs submitted:
SecRule REQUEST_COOKIES:'/(j?sessionid|(php)?sessid|(asp|jserv|jw)?session[-_]?(id)?|cf(id|token)|sid)/' ".*" "chain,phase:1,id:'981054',t:none,block,log,msg:'Invalid SessionID Submitted.',setsid:%{matched_var},setvar:tx.sessionid=%{matched_var},skipAfter:END_SESSION_STARTUP" SecRule SESSION:VALID "!@eq 1" "t:none,setvar:tx.anomaly_score=+%{tx.critical_anomaly_score},setvar:tx.%{rule.id}-WEB_ATTACK/INVALID_SESSIONID-%{matched_var_name}=%{tx.0}"
This rule's main purpose is to identify when clients are sending bogus SessionIDs that the application itself never issued from within Set-Cookie response headers. This mechanism could be leveraged however to also invalidate a SessionID in a scenario where the application itself wasn't properly killing it server-side. We can simply update our previous SecRuleUpdateActionById directive to also invalidate the local SessionID like this:
SecRuleUpdateActionById 981176:3 "setenv:request_cookies=%{request_headers.cookie},setenv:basic=%{request_headers.authorization}"SecRuleUpdateActionById 981176:3 "setvar:!session.valid,exec:/usr/local/apache/conf/logout.sh"
This has essentially the same effect as an application logout as ModSecurity will now block any requests that use that SessionID.