Detecting Successful XSS Testing with JS Overrides

Do you know when an attacker or security researcher successfully finds a Cross-site Scripting (XSS) vulnerability in your web application?

This blog post will demonstrate a proof of concept that uses ModSecurity to add defensive Javascript to response pages that will identify when web browsers execute certain code and then will send back a beacon alert to the web server.

Detecting XSS Attacks

Speaking quite candidly, from a web defender's perspective, dealing with XSS attacks and vulnerabilities is extremely challenging. From an attack view, if your web application functionality does not allow users to submit any html/code to the application then it is possible for a WAF to block it using blacklist input filtering. The challenges come when websites need to allow users to post html/dynamic content. How do you distinguish benign from malicious html code? Applying blacklist filtering in this situation is frought with both false negatives and false positives. There are a myriad of obfuscation techniques, browser quirks and best-fit mappings that web applications apply to inbound data which means that blacklist filtering is doomed to fail. So, what else can we do to identify successful XSS attacks?

This brings us to a fundamental issue with the way that most organizations are utilizing WAFs, which is strictly as "Attack Detection Systems." If you are only focusing on inbound data to identify attacks, you are not factoring in any vulnerability context to the equation.

Detecting Successful XSS Attacks

There is critical data which can be gained when you fully understand the attack sequences used during both reconnaissance and exploitation phases of XSS attacks. Remember, the interpreter which is being targeted in XSS attacks is the client's web browser. This means that if you are looking only at the initial inbound request data in a reflected XSS attack, you do not have enough information at that time about the application's susceptibility to this attack. Does the web application properly handle user-supplied data? Does it apply the correct, context-aware output encoding/escaping of user content when it is used in response data? We can not answer that question until we inspect the outbound data leaving the web application as it is being sent to the client (or in this case the reflected XSS victim).

In the case of XSS (and also many drive-by-download attack payloads), even inspecting HTML response bodies may be challenging to properly apply plaintext/string searches for malicious keywords. The key here is that we want to know when a client's particular web browser actually executes specific code. In order to accomplish this task, we need to move our detection logic from server-side (where the WAF essentially conducts static analysis of the html data) to client-side where we can utilize defensive Javascript within the client's web browser. The goal is to have our JS code execute first within the client's web browser where it will monitor the DOM for execution of specific functions that are often used during XSS probes and attacks:

  • alert()
  • prompt()
  • confirm()
  • unescape
  • document.write
  • String.fromCharCode

We can then "tripwire" these JS function calls so that if they ever execute, we can then send a beacon request using XML HTTP Request (XHR) back to our web server to report this issue.

JS Overrides with ModSecurity

SpiderLabs Research has created a js-overrides.js file and added it to our OWASP ModSecurity CRS GitHub repository. Here are the contents (thanks to @kkotowicz for improving the script):

(function() { // don't leak XSSTripwire into global ns  /*  Assumptions:    - we need to run first, before any other attacker script    - we can't prevent tripwire from being detected (e.g. by side effects)  Todo:    - a lot more in lockdown    - protect XHR  */  var XSSTripwire = new Object();  XSSTripwire.report = function() {    // Notify server    var notify = XSSTripwire.newXHR();    // Create a results string to send back    var results;    try {      results = "HTML=" + encodeURIComponent(document.body.outerHTML);    } catch (e) {} // we don't always have document.body    notify.open("POST", XSSTripwire.ReportURL, true);    notify.setRequestHeader("Content-Type","application/x-www-form-urlencoded");    notify.send(results);  }  XSSTripwire.lockdown = function(obj, name) {    if (Object.defineProperty) {      Object.defineProperty(obj, name, {        configurable: false      })    }  }  XSSTripwire.newXHR = function() {    var xmlreq = false;    if (window.XMLHttpRequest) {      xmlreq = new XMLHttpRequest();    } else if (window.ActiveXObject) {      // Try ActiveX      try {        xmlreq = new ActiveXObject("Msxml2.XMLHTTP");      } catch (e1) {        // first method failed        try {          xmlreq = new ActiveXObject("Microsoft.XMLHTTP");        } catch (e2) {          // both methods failed        }      }    }    return xmlreq;  };  XSSTripwire.proxy = function(obj, name, report_function_name, exec_original) {    var proxy = obj[name];    obj[name] = function() {      // URL of the page to notify, in the event of a detected XSS event:      XSSTripwire.ReportURL = "xss-tripwire-report?function=" + encodeURIComponent(report_function_name);      XSSTripwire.report();      if (exec_original) {        return proxy.apply(this, arguments);      }    };    XSSTripwire.lockdown(obj, name);  };  XSSTripwire.proxy(window, 'alert', 'window.alert', true);  XSSTripwire.proxy(window, 'confirm', 'window.confirm', true);  XSSTripwire.proxy(window, 'prompt', 'window.prompt', true);  XSSTripwire.proxy(window, 'unescape', 'unescape', true);  XSSTripwire.proxy(document, 'write', 'document.write', true);  XSSTripwire.proxy(String, 'fromCharCode', 'String.fromCharCode', true);})();

Now that we have the JS tripwire code, we next need to figure out how to add it to response content. ModSecurity has a number of different methods to manipulate the outbound HTTP response body content being sent to end users. We can either prepend/append our data to the response or optionally inject it directly into some part of the response using the @rsub operator. Here are some example rules that accomplish this task:

SecRule RESPONSE_HEADERS:Content-Type "@contains text/html" "id:'999000',chain,phase:4,t:none,nolog,pass"  SecRule &ARGS "@gt 0" "prepend:'<script src="\"/js-overrides.js\"" type="\"text/javascript\""></script>'"SecRule REQUEST_URI "@contains js-overrides.js" "id:'999002',phase:1,t:none,nolog,ctl:ruleEngine=off,ctl:auditEngine=Off"SecRule REQUEST_URI "@contains xss-tripwire-report" "id:'999001',phase:2,t:none,log,pass,msg:'XSS Testing Beacon Report',logdata:'JS Function Detected: %{args.function} in page: %{request_headers.referer}'"

The first rule will prepend our JS code to the top of the response page. The second rule will disable ModSecurity for the request for our JS override code and the final rule will generate an alert if we receive a beacon report from our tripwire code.

Practical Example: XSSED.COM

In order to demonstrate how this code actually works, let's take a look at a practical, real-world example taken from the site xssed.com which provides an archive of XSS vulnerabilities found within public sites. This particular issue was found on the Adobe site and has since been fixed:

http://www.adobe.com/cfusion/tdrc/modal/signin.cfm?loc=en_us&product=""</script><script>alert%28document.cookie%29</script><script>

If a client were to send that request, the "product" parameter data was reflected back within the following section of code in the response body:

<script type="application/javascript" language="javascript"> s2=new Object(); var tmpOmniture=''; s2.pageName ="HTML Trial Download : Login : "+tmpOmniture; s2.channel="Trial Download";  s.t(s2);    createAccountURL = '/cfusion/tdrc/modal/signup.cfm?product=""</script><script>alert(document.cookie)</script><script>&loc=en_us'; </script>

If we utilize our JS tripwire rules, our code is injected at the top of the response page:

<html><script type="text/javascript" src="/js-overrides.js"></script></html><script type="application/javascript" language="javascript"> s2=new Object(); var tmpOmniture=''; s2.pageName ="HTML Trial Download : Login : "+tmpOmniture; s2.channel="Trial Download";  s.t(s2);    createAccountURL = '/cfusion/tdrc/modal/signup.cfm?product=""</script><script>alert(document.cookie)</script><script>&loc=en_us'; </script>

When the web browser executes the XSS alert code, our JS override code for the window.alert function catches it and initiates the following XHR request back to our site:

POST /2012/02/12/www.adobe.com/xss-tripwire-report?function=window.alert HTTP/1.1Host: vuln.xssed.netUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:17.0) Gecko/17.0 Firefox/17.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflateProxy-Connection: keep-aliveContent-Type: application/x-www-form-urlencoded; charset=UTF-8Referer: http://vuln.xssed.net/2012/02/12/www.adobe.com/?loc=en_us&product=%22%22%3C/script%3E%3Cscript%3Ealert%28document.cookie%29%3C/script%3E%3Cscript%3EContent-Length: 492Pragma: no-cacheCache-Control: no-cacheHTML=%3Cbody%3E%0A%3Cscript%20type%3D%22application%2Fjavascript%22%20language%3D%22javascript%22%3E%0A%09s2%3Dnew%20Object()%3B%0A%09var%20tmpOmniture%3D''%3B%0A%09s2.pageName%20%3D%22HTML%20Trial%20Download%20%3A%20Login%20%3A%20%22%2BtmpOmniture%3B%0A%09s2.channel%3D%22Trial%20Download%22%3B%09%0A%09s.t(s2)%3B%09%0A%09%0A%09%09createAccountURL%20%3D%20'%2Fcfusion%2Ftdrc%2Fmodal%2Fsignup.cfm%3Fproduct%3D%22%22%3C%2Fscript%3E%3Cscript%3Ealert(document.cookie)%3C%2Fscript%3E%3C%2Fbody%3E

When this request is received by ModSecurity, our rules generate the following alert:

[Thu Nov 29 11:16:34 2012] [error] [client 192.168.1.101] ModSecurity: Warning. String match "xss-tripwire-report" at REQUEST_URI. [file "/usr/local/apache/conf/crs/base_rules/modsecurity_crs_15_custom.conf"] [line "9"] [id "999001"] [msg "XSS Testing Beacon Report"] [data "JS Function Detected: window.alert in page: http://vuln.xssed.net/2012/02/12/www.adobe.com/?loc=en_us&product=%22%22%3C/script%3E%3Cscript%3Ealert%28document.cookie%29%3C/script%3E%3Cscript%3E"] [hostname "vuln.xssed.net"] [uri "/2012/02/12/www.adobe.com/xss-tripwire-report"] [unique_id "ULeKYsCoC-kAAFhjC7IAAAAA"]

We now have a complete report of the incident including what JS function was executed, the page it was on and the full HTML payload in the ModSecurity audit log file:

--9483ad6a-A--[29/Nov/2012:11:16:34 --0500] ULeKYsCoC-kAAFhjC7IAAAAA 192.168.1.101 62864 192.168.1.101 80--9483ad6a-B--POST http://vuln.xssed.net/2012/02/12/www.adobe.com/xss-tripwire-report?function=window.alert HTTP/1.1Host: vuln.xssed.netUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:17.0) Gecko/17.0 Firefox/17.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflateProxy-Connection: keep-aliveContent-Type: application/x-www-form-urlencoded; charset=UTF-8Referer: http://vuln.xssed.net/2012/02/12/www.adobe.com/?loc=en_us&product=%22%22%3C/script%3E%3Cscript%3Ealert%28document.cookie%29%3C/script%3E%3Cscript%3EContent-Length: 492Pragma: no-cacheCache-Control: no-cache--9483ad6a-C--HTML=%3Cbody%3E%0A%3Cscript%20type%3D%22application%2Fjavascript%22%20language%3D%22javascript%22%3E%0A%09s2%3Dnew%20Object()%3B%0A%09var%20tmpOmniture%3D''%3B%0A%09s2.pageName%20%3D%22HTML%20Trial%20Download%20%3A%20Login%20%3A%20%22%2BtmpOmniture%3B%0A%09s2.channel%3D%22Trial%20Download%22%3B%09%0A%09s.t(s2)%3B%09%0A%09%0A%09%09createAccountURL%20%3D%20'%2Fcfusion%2Ftdrc%2Fmodal%2Fsignup.cfm%3Fproduct%3D%22%22%3C%2Fscript%3E%3Cscript%3Ealert(document.cookie)%3C%2Fscript%3E%3C%2Fbody%3E--9483ad6a-F--HTTP/1.1 404 Not FoundContent-Type: text/html; charset=iso-8859-1Content-Length: 79Vary: Accept-Encoding--9483ad6a-E--404--9483ad6a-H--Message: Warning. String match "xss-tripwire-report" at REQUEST_URI. [file "/usr/local/apache/conf/crs/base_rules/modsecurity_crs_15_custom.conf"] [line "9"] [id "999001"] [msg "XSS Testing Beacon Report"] [data "JS Function Detected: window.alert in page: http://vuln.xssed.net/2012/02/12/www.adobe.com/?loc=en_us&product=%22%22%3C/script%3E%3Cscript%3Ealert%28document.cookie%29%3C/script%3E%3Cscript%3E"]Apache-Handler: proxy-serverStopwatch: 1354205794313596 157225 (- - -)Stopwatch2: 1354205794313596 157225; combined=4495, p1=3489, p2=603, p3=17, p4=363, p5=22, sr=283, sw=1, l=0, gc=0Response-Body-Transformed: DechunkedProducer: ModSecurity for Apache/2.7.0-rc3 (http://www.modsecurity.org/); OWASP_CRS/3.0.0.Server: Apache/2.2.17 (Unix) mod_ssl/2.2.12 OpenSSL/0.9.8r DAV/2Engine-Mode: "DETECTION_ONLY"--9483ad6a-Z--

Benefits

The benfits of this approach should be self evident. Using this approach, you will provided with high fidelity alerts when a successful XSS attack has been executed. This will alert you not only to specific threat agents that are probing your site, but also for identify which application resources are not properly handling user supplied data. This approach gives you the information you need to quickly address XSS flaws identified on your site.

Caveats

As with most mitigation strategies, using JS overriding is not fool-proof. There is nothing preventing attackers from using other html/js tags to test the application. We chose these particular ones as they are the most popular ones and will catch the vast majority of attackers and vulnerability scanning tools. There is also the issue of using JS to delete these functions and then resetting them such as:

<script>delete prompt;prompt(1)</script>

There are improvements that can be made to the JS code to make it more resistent to evasions such as using features of ES5, however total evasion prevention is not the purpose of this code. The goal is to gain an early warning system to know when someone is probing for XSS flaws. Defenders must use many different methods to attempt to mitigate XSS attacks. See my Blackhat presentation "XSS Street-Fight: The only rule is there are no rules" for more ideas.

Thanks

Thanks goes to @mimeframe for inspiring me with this idea from his recent Ruxcon talk and to Zane Lackey for his XSS testing scripts which gave me something to work with. Thanks also goes to @superevr for feedback and a dose of reality with bypasses :)

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.