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.

DPAPI Primer for Pentesters

Update 6 July 2013 – fixed info when user store may decrypt on separate machine

Understanding DPAPI is not that complicated, although the amount of the documentation can be daunting. There is a lot of excellent “under the hood” DPAPI stuff available (e.g. Stealing Windows Secrets Offline http://www.blackhat.com/html/bh-dc-10/bh-dc-10-briefings.html) But is it easier to steal these secrets online? The answer is yes, probably.

DPAPI’s purpose in life is to store secrets. These are frequently symmetric crypto keys used to encrypt other things. For example, typical use cases for these protected keys are for them to encrypt anything from saved passwords in an RDP connection manager on a Desktop to encrypting sensitive info in a database (e.g. bank account numbers). Using DPAPI to store sensitive info (or store keys that encrypt sensitive info) is good practice.

There are a few concepts to understand before using DPAPI

  • You can encrypt/decrypt the secrets using either a “user store” or a “machine store”. This is where the entropy comes from. What this means is:
    • If you use the user store, then this secret may only be read by this user on this machine. In the testing I’ve done, it cannot be read by the same domain user on a different machine either.
    • If you use the machine store, any user on the machine is able to decrypt the secrets – including Network User (e.g. IIS), Guest, etc. In the testing I’ve done, this is certainly less restrictive/secure than the user store (user store takes into account the machine also).
  • Secondary Entropy: One argument to the DPAPI calls is the secondary entropy argument. Using this, an application needs to know this secret before the data is decrypted.

A few common misconceptions

  • I’ve heard from several people how the user’s login password is used for entropy. This does not really tell the whole story.  An attacker does not need to know the password to retrieve DPAPI, they just need to be executing with the account/machine. This is often an easier problem than retrieving a password.
  • It can be easy to mix up DPAPI and other things that make use of DPAPI. For example, the credential store is an API that uses DPAPI

A pretend good setup

If things are done basically correct, then a good scheme might be something like this:

  • There’s a server database that stores encrypted bank account numbers 
    • It needs to decrypt these for regular use
    • The encryption/integrity of this data uses a good authenticated encryption scheme, say AES/GCM.
  • The question is, where do we store the key?
    • We use DPAPI in user mode to encrypt the data
    • The user used that can access the data is a locked down domain service account with limited permissions. For example, the service account can’t log in to the box, and is a different user than the database runs as.
    • The DPAPI encrypted blob is stored in an ACLed part of the registry so only the service account has access

An overly simplified example might be the following, which runs at deployment time.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Win32;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Security.AccessControl;

namespace encrypt_dpapi
{
    class Program
    {
        private const string MASTER_REG_KEY_PATH = @"SOFTWARE\Contoso\dbapp";
        private const string MASTER_REG_KEY_NAME = @"bankkey";
        private const string SERVICE_ACCT = @"EVIL\_mservice";

        public static void WriteMachineReg(string path, string valuename, string value)
        {
            RegistryKey bank_reg = Registry.LocalMachine.CreateSubKey(MASTER_REG_KEY_PATH);

            //set the ACLs of the key so only service account has access
            RegistrySecurity acl = new RegistrySecurity();
            acl.AddAccessRule(new RegistryAccessRule(SERVICE_ACCT, RegistryRights.FullControl, AccessControlType.Allow));
            acl.SetAccessRuleProtection(true, false);

            bank_reg.SetAccessControl(acl);

            //write the key
            bank_reg.SetValue(MASTER_REG_KEY_NAME, value);

            bank_reg.Close();


        }

        //we want the symmetric key to be randomly generated and no one to even know it!
        public static byte[] genSymmetricKey(int size)
        {
            RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
            byte[] buf = new byte[size];
            rng.GetBytes(buf);
            return buf;
        }

        static void Main(string[] args)
        {
            //check that we're running as the correct service account
            string user = WindowsIdentity.GetCurrent().Name;
            if (!user.Equals(SERVICE_ACCT, StringComparison.OrdinalIgnoreCase))
            {
                Console.WriteLine("Error: must run as " + SERVICE_ACCT + " Account");
                Console.WriteLine(user);
                Console.WriteLine("Exiting program");
                return;
            }


            //generate a random key we'll use to encrypt bank accounts
            byte[] key = genSymmetricKey(256);
            Console.WriteLine("Key " + Convert.ToBase64String(key));

            byte[] additional_entropy = {0x41, 0x41, 0x41, 0x41};

            //dpapi encrypt the key
            byte[] enc_key = ProtectedData.Protect(key, additional_entropy, DataProtectionScope.CurrentUser);

            //dpapi encrypted key is saved in base64
            WriteMachineReg(MASTER_REG_KEY_PATH, MASTER_REG_KEY_NAME, Convert.ToBase64String(enc_key));


        }
    }
}

If I run this, I get

>encrypt_dpapi.exe
Key 721HLUm5n9/0hpAFtBl3Jvn2jJ+KM3z4mPKfyLCHOAZyx/JUP6qs+DCVpwWCqbmB3CZc+o6qXeY4
T+ivRkgn6ZLUSTInuhIh96qRPC9DXZD/ALUg5NTdoWtxYaSq4uOeF6ywh1hRyLyVKSopdHkR4ZycFKV9
KIIX+O5pKK/sYBRwvkhnIwpbLO3Qps7FK5x3wNlj5OwLOfl31bs8rE0Qk/yzvhzT5+zF7BJx/j/qUCGa
g8f8PGOBhi/Ch0lGDWW203rbwdfMC8fmHAYfR4FdlU2L90lmEOCY8Mgjno4ScAGgKPyFS74TLaufLKiz
tjBaAKt89JGaNHOizWvdIGsoMw==

This is the base64 version of the key used to decrypt bank account numbers – so ultimately what we as attackers want. But if I look in the registry (where this info is stored) I get something completely different

dpapi_registry

 

So how can we get decrypted Bank Account Numbers?

Say we’re an attacker and we have system privs on the box above. How can we decrypt the bank account numbers?

There’s probably more than one way. One method may be to scan memory and extract the key from memory (but if it uses safe memory protection it may not be in there long…). Another method may be to attach a debugger to the app and extract it that way. For a production pentest, one of the most straightforward ways just to use DPAPI again to decrypt the data.

using System;
using System.Text;
using System.Runtime.InteropServices;
using System.ComponentModel;
using Microsoft.Win32;
using System.Security.Cryptography;



namespace decrypt_dpapi_reg
{

    public class registry
    {
        public static string ReadMachineReg(string path, string valuename)
        {
            return (string)Registry.GetValue(path, valuename, "problem");
        }
    }

    class Program
    {

        private const string MASTER_REG_KEY_PATH = @"SOFTWARE\Contoso\dbapp";
        private const string MASTER_REG_KEY_NAME = @"bankkey";
        private const string SERVICE_ACCT = @"EVIL\_mservice";   


        public static byte[] UnprotectUser(string data)
        {
            try
            {
                byte[] additional_entropy = { 0x41, 0x41, 0x41, 0x41 };
                byte[] encryptData = Convert.FromBase64String(data);
                return ProtectedData.Unprotect(encryptData, additional_entropy, DataProtectionScope.CurrentUser);
                
            }
            catch (CryptographicException e)
            {
                Console.WriteLine("Data was not decrypted. An error occurred.");
                Console.WriteLine(e.ToString());
                return null;
            }
        }


        static void Main(string[] args)
        {
            //must be run as a user with access on a machine with access
            string s = registry.ReadMachineReg(@"HKEY_LOCAL_MACHINE\" + MASTER_REG_KEY_PATH, MASTER_REG_KEY_NAME);

            Console.WriteLine("DPAPI encrypted key: " + s);
            Console.WriteLine();

            Console.WriteLine("DPAPI encrypted key: " + BitConverter.ToString(Convert.FromBase64String(s)));
            Console.WriteLine();

            byte[] decryptedbytes = UnprotectUser(s);
            Console.WriteLine(Convert.ToBase64String(decryptedbytes));
            
        }
    }

}

If we try to run this as another user, what happens? It won’t work. We’ll get an exception like this:

System.Security.Cryptography.CryptographicException: Key not valid for use in specified state.

   at System.Security.Cryptography.ProtectedData.Unprotect(Byte[] encryptedData,Byte[] optionalEntropy, DataProtectionScope scope)
   at decrypt_dpapi_reg.Program.UnprotectUser(String data)

DPAPI uses the user entropy to encrypt the data, so we need to compromise the user. But what happens if we copy out the registry value to another machine and try to DPAPI to decrypt the secrets on another box as the EVIL\_mservice account, what happens?

It turns out in my testing this does not work, and I got the same exception as a bad user on the same machine. I needed to run on the same machine that encrypted the key. UPDATE However, there are several reasons it may not work, but it should work overall. From http://support.microsoft.com/kb/309408#6

DPAPI works as expected with roaming profiles for users and computers that are joined to an Active Directory directory service domain. DPAPI data that is stored in the profile acts exactly like any other setting or file that is stored in a roaming profile. Confidential information that the DPAPI helps protect are uploaded to the central profile location during the logoff process and are downloaded from the central profile location when a user logs on.
For DPAPI to work correctly when it uses roaming profiles, the domain user must only be logged on to a single computer in the domain. If the user wants to log on to a different computer that is in the domain, the user must log off the first computer before the user logs on to the second computer. If the user is logged on to multiple computers at the same time, it is likely that DPAPI will not be able to decrypt existing encrypted data correctly.
DPAPI on one computer can decrypt the master key (and the data) on another computer. This functionality is provided by the user’s consistent password that is stored and verified by the domain controller. If an unexpected interruption of the typical process occurs, DPAPI can use the process described in the “Password Reset” section later in this article.
There is a current limitation with roaming profiles between Windows XP-based or Windows Server 2003-based computers and Windows 2000-based computers. If keys are generated or imported on a Windows XP-based or Windows Server 2003-based computer and then stored in a roaming profile, DPAPI cannot decrypt these keys on a Windows 2000-based computer if you are logged on with a roaming user profile. However, a Windows XP-based or Windows Server 2003-based computer can decrypt keys that are generated on a Windows 2000-based computer.

Blackbox Detection

DPAPI can be really tough to do with a complete blackbox. As part of an engagement, if I’ve compromised this far, I usually go hunting for the source which is frequently less protected than the DPAPI protected asset. But one giveaway that you’re even dealing with DPAPI is if a blob has a structure similar to the following:

dpapi_format

We can be a bit more scientific about this. Comparing two runs of the same program above gives the following bytes that are the same (note that since the key itself is random but a constant length, this reveals a bit about the structure). This is documented better elsewhere I’m sure, but if something looks quite a bit like this, it should give you a quick idea if you’re dealing with a dpapi encrypted blob.

01-00-00-00-D0-8C-9D-DF-01-15-D1-11-8C-7A-00-C0-4F-C2-97-EB
-01-00-00-00-08-DC-D9-58-94-2E-C9-4A-9C-59-12-F1-60-EA-6C-56-00-00-00-00-02-00-0
0-00-00-00-03-66-00-00-C0-00-00-00-10-00-00-00-xx-xx-xx-xx-xx-xx-xx-xx-xx-xx-xx-
xx-xx-xx-xx-xx--00-00-00-00-04-80-00-00-A0-00-00-00-10-00-00-00-xx.....