Qualys validaterequest ‘finding’ is an Annoying PCI Problem

Uh oh. A post about compliance. That means it’s a rant, because I think compliance is dumb. I love parts of the security community, like Defcon/Bsides/CTF/the movie hackers and stuff like that, but I also hate more security people than not. Sorry.

A lot of sites use qualys scans as part of PCI, so doubtless I’m not the only person who has run into this. If you’re running ASP.net 3 you’ll get the error here: https://community.qualys.com/docs/DOC-3495

Some excerpts are

What versions of Microsoft ASP.NET are vulnerable?
Microsoft has confirmed that ASP.NET versions 1 and 2 are both vulnerable.

Additionally, Qualys has confirmed that ASP.NET version 3 is also vulnerable, as it includes the vulnerable component from version 2 by default. We have tested this in our Labs and confirmed the exploit works on a fully patched version 3.

What versions of Microsoft ASP.NET are not vulnerable?
ASP.NET version 4 is not vulnerable, as it does not use the vulnerable ‘ValidateRequest’ Filter.

Applications that have been securely coded, and have custom filtering in place above and beyond the ValidateRequest Filter, may not be vulnerable.

Since this is being detected based upon the .NET Framework Version, shouldn’t this be reported as a Potential Vulnerability?
After an in-depth investigation, including discussions with the original publisher, the vendor, and a thorough review of the two published CVE’s, we have decided that QID 90780 would be better represented as a Potential Vulnerability, and so we have reclassified it as such.

Our detection is based on the remote capability to identify the active framework running on the system. While this does accurately validate the framework version, it does not accurately confirm the presence of XSS, which applies to a higher layer and would be dependent upon several other factors such as web application coding practices, input sanitization, form submissions, etc.

In many cases, although someone may be running the vulnerable framework, they may have additional custom built filters in place which mitigate the risk and ensure that XSS is not possible on the target system.

In summary, the presence of the Vulnerable Framework actively running is the basis of this vulnerability, which could potentially allow additional attacks such as cross-site scripting. However, this detection is not actively confirming the presence of cross-site scripting, and so we believe this is most accurately marked as a Potential Vulnerability.

As a side note, since PCI Requires that both Actual & Potential Vulnerabilities be remediated the same, this is still a PCI Failing Vulnerability.

So while upgrading versions of .net is a great idea, especially to 4.5 which has some awesome security improvements, this qualys scan issue seems totally bogus.

  1. I talk about how validaterequest works here. It is not meant to stop all flavors of xss. However, even if validaterequest completely failed and was bypassable in all cases (which it’s not afaik) why should this by itself cause a site to fail PCI, when most other frameworks do not have this sort of WAF in place at all? If validaterequest were always bypassable, this means the web application is put on the same footing as other frameworks. Should all Java sites fail because they don’t have something like validaterequest built in?
  2. So, Qualys labs has determined version 3 is vulnerable but version 4 is not, huh? It turns out that right now, when I put all versions in ilspy, from .net 2 to .net 4.5, validaterequest has not changed. At all. Here is what it looks like for all versions of .net. Maybe the problem is somewhere else, but I’m skeptical.
  3. // System.Web.CrossSiteScriptingValidation
    internal static bool IsDangerousString(string s, out int matchIndex)
    {
    	matchIndex = 0;
    	int startIndex = 0;
    	while (true)
    	{
    		int num = s.IndexOfAny(CrossSiteScriptingValidation.startingChars, startIndex);
    		if (num < 0)
    		{
    			break;
    		}
    		if (num == s.Length - 1)
    		{
    			return false;
    		}
    		matchIndex = num;
    		char c = s[num];
    		if (c != '&')
    		{
    			if (c == '<' && (CrossSiteScriptingValidation.IsAtoZ(s[num + 1]) || s[num + 1] == '!' || s[num + 1] == '/' || s[num + 1] == '?'))
    			{
    				return true;
    			}
    		}
    		else
    		{
    			if (s[num + 1] == '#')
    			{
    				return true;
    			}
    		}
    		startIndex = num + 1;
    	}
    	return false;
    }
    
    

If I were to guess, I’d say this scan check is just checking the HTTP headers and we have no way to know what the root issue really is. This is one of my biggest issues with scanners in general.

More than once, working with multiple groups within multiple companies, there have been scrambles to upgrade .NET versions to address this “potential vulnerability”. Pentest reports have been delivered where this was reported as the #1 issue with an important rating and the impact spelled out like it was xss. Drives me crazy. I even tweeted about it and everything. End rant.

ValidateRequest should probably still be Enabled

I noticed this post on reddit a couple weeks back, and it’s called “new .net xss bypass”. I look at .net apps more than anything else right now as part of my day job, so this new bypass is something I was already aware of. There are quite a few comments I think are a bit off, and the article calls this a “vulnerability”. This is an interesting subject, and like a lot of things, people say things that can be misleading or not tell the entire story…

What is validateRequest?

validateRequest kind of works like a WAF, and it’s main purpose is to mitigate reflected XSS. It’s enabled by default, and you can see it in action on most .NET websites. For example, check out http://windows.microsoft.com/en-US/windows/home. If you send it http://windows.microsoft.com/en-US/windows/home?myparam=aaa or http://windows.microsoft.com/en-US/windows/home?myparam=aaa%3c it’s fine, but then if you send it http://windows.microsoft.com/en-US/windows/home?myparam=aaa%3ca it gives an error (note the only difference between the last two is < vs <a). What's going on here?

Well, on the server, the stack trace probably looks something like this (this is what it looks like in my test application).

[HttpRequestValidationException (0x80004005): A potentially dangerous Request.QueryString value was detected from the client (test="<a").]
   System.Web.HttpRequest.ValidateString(String value, String collectionKey, RequestValidationSource requestCollection) +9665149
   System.Web.<>c__DisplayClass5.<ValidateHttpValueCollection>b__3(String key, String value) +18
   System.Web.HttpValueCollection.EnsureKeyValidated(String key) +9664565
   System.Web.HttpValueCollection.GetValues(Int32 index) +29
   System.Web.HttpValueCollection.ToString(Boolean urlencoded, IDictionary excludeKeys) +206
   System.Web.UI.Page.get_ClientQueryString() +411
   System.Web.UI.HtmlControls.HtmlForm.GetActionAttribute() +259
   System.Web.UI.HtmlControls.HtmlForm.RenderAttributes(HtmlTextWriter writer) +724
   System.Web.UI.HtmlControls.HtmlControl.RenderBeginTag(HtmlTextWriter writer) +41
   System.Web.UI.HtmlControls.HtmlContainerControl.Render(HtmlTextWriter writer) +20
   System.Web.UI.HtmlControls.HtmlForm.Render(HtmlTextWriter output) +53
   System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter) +57
   System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter) +100
   System.Web.UI.HtmlControls.HtmlForm.RenderControl(HtmlTextWriter writer) +40
   System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +128
   System.Web.UI.Control.RenderChildren(HtmlTextWriter writer) +8
   System.Web.UI.Control.Render(HtmlTextWriter writer) +10
   System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter) +57
   System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter) +100
   System.Web.UI.Control.RenderControl(HtmlTextWriter writer) +25
   System.Web.UI.Control.RenderChildrenInternal(HtmlTextWriter writer, ICollection children) +128
   System.Web.UI.Control.RenderChildren(HtmlTextWriter writer) +8
   System.Web.UI.Page.Render(HtmlTextWriter writer) +29
   System.Web.UI.Control.RenderControlInternal(HtmlTextWriter writer, ControlAdapter adapter) +57
   System.Web.UI.Control.RenderControl(HtmlTextWriter writer, ControlAdapter adapter) +100
   System.Web.UI.Control.RenderControl(HtmlTextWriter writer) +25
   System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +6704
   System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +245
   System.Web.UI.Page.ProcessRequest() +72
   System.Web.UI.Page.ProcessRequestWithNoAssert(HttpContext context) +21
   System.Web.UI.Page.ProcessRequest(HttpContext context) +58

Tracing down the line from the validateString method, eventually it calls this for all values (not keys).

// System.Web.CrossSiteScriptingValidation
internal static bool IsDangerousString(string s, out int matchIndex)
{
	matchIndex = 0;
	int startIndex = 0;
	while (true)
	{
		int num = s.IndexOfAny(CrossSiteScriptingValidation.startingChars, startIndex);
		if (num < 0)
		{
			break;
		}
		if (num == s.Length - 1)
		{
			return false;
		}
		matchIndex = num;
		char c = s[num];
		if (c != '&')
		{
			if (c == '<' && (CrossSiteScriptingValidation.IsAtoZ(s[num + 1]) || s[num + 1] == '!' || s[num + 1] == '/' || s[num + 1] == '?'))
			{
				return true;
			}
		}
		else
		{
			if (s[num + 1] == '#')
			{
				return true;
			}
		}
		startIndex = num + 1;
	}
	return false;
}

Obvious Edge Cases

So clearly, validateRequest makes no attempt to stop most DOM XSS, and there’s no attempt at all with stored XSS. It’s also clearly limited in preventing reflected XSS too. There’s no attempt at detecting encoding, so there’s kind of an assumption that the encoding is UTF-8. Not to mention if there’s an xss in the whole query string, in which case you could just put it in the keys. Not to mention if there’s user input being reflected in the context of an attribute, validateRequest does not stop it. For example:

<img src="" title="USERINPUT">

In this context, validateRequest doesn’t stop reflected XSS. It does, however, try to stop at least this case:

<html><body>
USERINPUT
</body></html>

If you can get script to execute in this context with UTF-8 encoding, you’ve bypassed something validateRequest was meant to prevent.

The bypass I saw on reddit

The bypass looks like this:

‹%tag style="xss:expression(alert(123))" ›

This can make it through validateRequest’s filter because the < is not followed by a bad char (% is fine). The tags execute in IE9 in compatibility mode in the context above (it doesn’t seem to execute in IE 8- or IE 10).

There are some limitations in this attack though. First, remember we’re talking about just IE9. Second is the fact that by default IE9’s xss filter is turned on, and IE 9's xss filter will prevent the above from executing. There might be a generic way to bypass both validateRequest and IE9’s xss filter, but I am not aware of it.

IE_filter

Things I’ve Tried

In the past, I’ve tried a few bypasses of validateRequest myself.

Unit Test with single separators

I’ve tried the following test cases. This does catch the <% bypass with IE9 in compatability mode, but nothing else fires. One thing that I thought was a bypass at first was %00, but validaterequest does actually catch that.

def bad_char(m):
    if (m == "<" or m== "!" or m == "?" or m == "/" or (ord(m) >= 0x41 and ord(m) <= 0x5a) or
        (ord(m) >= 0x61 and ord(m) <= 0x7a)):
        return False
    return True

def singlechars_img(f):
    for i in range(1,256):
        if not bad_char(chr(i)):
            continue
        f.write("<" + chr(i) + "img src='' onerror=alert(" + hex(i) + ") /> <br />\n")

def singlechars_style(f):
    for i in range(1,256):
        if not bad_char(chr(i)):
            continue
        f.write("<" + chr(i) + "tag style=\"xss:expression(open(alert(" + str(i) + ")))\" > <br />\n")

Trying to be clever (overlong UTF-8, etc)

One thing I tried was to backspace one of the tags with the ascii value 0x08: <<0x08script>. This doesn’t execute in any browser tried

I tried the shift JIS prefix code %e0 which should consume the next character as part of a multibyte literal. The idea is that if validaterequest tries to decode any data, it may consume an extra %3c, whereas some browsers (like chrome) do not consume an extra %3c and there would be a mismatch. But validaterequest is much simpler, and just iterates over all the bytes like we can see from the reflected snipped above.

I also tried several overlong UTF-8 combinations, but these don’t execute with content-type UTF-8

#overlong UTF-8
def overlong(f):
    #attempt to delete a <
    f.write("<<\x08script>alert(1)</script>")
    #some overlong UTF
    f.write("<\xFC\x80\x80\x80\x81\xA9\xFC\x80\x80\x80\x81\xAD\xFC\x80\x80\x80\x81\xA7 src='' onerror=alert(2) />")
    f.write("<\xFC\x80\x80\x80\x81\xA9mg src='' onerror=alert(2) />")
    f.write("\xC1\xA9mg src='' onerror=alert(3) />")
    f.write("\xF8\x80\x80\x81\xA9mg src='' onerror=alert(3) />")

Conclusions

The ‹%tag style=”xss:expression(alert(123))” › validaterequest bypass is interesting and something to keep in the back of your pentesting pocket, but if you’re on defense don’t panic. I don’t think this really breaks validaterequest in a meaningful way. Pulling numbers out of my ass, I would say that if 30% of xss were prevented by validaterequest without this, then maybe this knocks the successfully prevented attacks down like a percent or two, to 28% (exploitable against users who use IE9 and have disabled the xss filter (or where you can get around the xss filter)). validaterequest does have it’s problems, and there have been generic bypasses in the past, but I think it’s still worthwhile to enable.