Securing your Google Apps SHA1 password hashes
For me, one of the advantages of Google Apps over Live@Edu is the simplicity of the tool used to automatically provision user accounts based on your Active Directory. While Live@Edu requires you to use the complicated Identity Lifecycle Manager (which consumes significant server resources), Google Apps Directory Sync can be run on the bare minimum of hardware and takes only a very short time to set up.
When used in conjunction with the sha1hexfltr program, you can not only synchronise the user account data, but also the user’s password, ensuring the user does not have to remember two different passwords for their standard computer logon and their Google Apps account. Unfortunately, the way it does so is not the exactly most secure method in the world.
The basis of the system is that when a user changes their password, a SHA1 hash of that password is recorded in an attribute in their Active Directory account. When Google Apps Directory Sync runs, it reads this hash and sends it to the Google Apps server. The problem with this is twofold:
- The attribute it uses (the division attribute) is not private in any way. Any authenticated user can read the division attribute of any other user. This wouldn’t be a huge problem, if not for the fact that:
- The hash is a basic unsalted hash, and advancements in computing power mean it is not infeasible for SHA1 hashes to be brute-force decoded in a short amount of time by a knowledgeable user.
This was brought up by a commenter a few months back when I talked about 2-legged OAuth, and has also been discussed on the project page for sha1hexfltr, but there’s no way of making it use a different attribute. A newer filter DLL, hashing-password-filter, does use a different attribute, but it requires AD schema changes and is no less insecure.
Instead, I went looking for a way to secure the data that sha1hexfltr produces. I haven’t come up with a cast-iron solution, but I have a workaround that significantly reduces the vulnerability window.
The principal problem is that by default, almost all Active Directory LDAP attributes are readable by anyone who can connect to the directory. There are ways of altering these default permissions, but you stand a decent chance of screwing your domain functionality by doing so.
However, Windows 2003 SP1 introduced the concept of confidential attributes. There are attributes that are not readable even if you have the ‘read all properties’ right on an AD object, unless you also have the extra CONTROL_ACCESS right. This is normally only granted as part of ‘full access’ rights, which regular users do not possess – only administrators get them.
My first thought was “great, I’ll just mark the division attribute as confidential’. Unfortunately, you can’t mark LDAP base schema attributes as confidential – and division is one of them.
Instead, I implemented a workaround – a VB script that will run on a schedule and move any data it finds in the division attribute of a user account into a different attribute that is already marked as confidential. No schema change required!
The attribute I settled on was unixUserPassword. Normally this is used to store a password for use by Windows Services for UNIX, but since I don’t use that on my domain, it was going spare. If it’s already in use on your domain, you’ll need to identify a different spare confidential attribute (unfortunately I couldn’t find a list anywhere). If all else fails, you could take one of the more obscure non-base attributes in AD and modify it to be confidential. I would suggest you are probably not already using the utterly ridiculous drink attribute.
Nice and simple:
Const ADS_PROPERTY_CLEAR = 1 Set oConn = CreateObject("ADODB.Connection") oConn.Open "Provider=ADsDSOObject;" Set oCmd = CreateObject("ADODB.Command") oCmd.ActiveConnection = oConn oCmd.CommandText = "<LDAP://DC=angrytech,DC=internal>;(&(objectClass=user)(division=*));ADsPath;subtree" Set oRecords = oCmd.Execute Do Until oRecords.EOF strADsPath = oRecords.Fields("ADsPath") Set oUser = GetObject(strADsPath) oUser.Put "unixUserPassword", oUser.Division oUser.PutEx ADS_PROPERTY_CLEAR, "division", 0 oUser.SetInfo oRecords.MoveNext Loop oConn.Close
In a nutshell, this queries the current logon DC for every user account that currently has something in the division attribute (line 8). It then goes through each result and copies the value from division to unixUserPassword, then erases the content of division.
Obviously, you’ll need to alter DC=angrytech,DC=internal on line 8 to the root of your domain. When testing, I suggest you comment out line 16 so that you don’t obliterate the SHA1 data until you are confident it is being stored correctly.
This script is pretty efficient and should execute very quickly. The first time I ran it in production, it modified 257 accounts, and took around 5 seconds on a member server. Subsequent runs are quicker still, because it doesn’t need to process each account again until the next time the password changes. This means you can schedule this script to run practically as frequently as you need to. By running it hourly, for example, you reduce the exposure window of the hashed passwords to a maximum of the first hour after the password is changed. That’s a significant improvement over having the data almost permanently accessible.
You will need to run this script with a highly-privileged account such as a Domain Admin. This is because the sha1hexfltr DLL doesn’t just store password hashes for the users you are syncing to Google Apps – it stores it for every user, including your administrator accounts. The Domain Admins group (along with a few others) is a service administration group in Active Directory (otherwise known as a ‘protected group’), and a standard account won’t be able to modify the attributes of any account that is a member of such a group even if you set an explicit Allow rule in the ACL. Only other admin accounts can do this. So, if you run the above script with a standard account, you will get access denied errors if it needs to modify an admin account, even if you have delegated modify privileges.
Accessing the userUnixPassword
Obviously, once the data is securely stored, Google Apps Directory Sync still needs to be able to access it. If you use the same account to schedule GADS as you do the above script, that won’t be a problem. If you use a lesser account, you will need to give it that special CONTROL_ACCESS right on the required container(s) in Active Directory.
Bear in mind that although I recommend using a non-admin account for GADS, you won’t be able to if you need to sync an account to Google Apps that is a member of any of those service administration groups we just talked about. For the same reason as above, confidential attributes of any accounts in a privileged group will not be able to be read by any account other than a Domain Admin. Personally I do not recommend syncing the details of a highly-priveleged account to a cloud service at all, but that’s up to you.
Adding the CONTROL_ACCESS right for a standard account
Helpfully, you can’t do this using the standard Active Directory Users & Computers snap-in, or even ADSIEdit. KB922836 described a method for doing it, but I found the instructions to be incomplete, hard to follow, and not at all up to date. So, here is a blow-by-blow description of adding the appropriate permission to an entire OU so that GADS can read the details of every user inside it:
12. Lastly, click Update.
Reconfigure Google Apps Directory Sync
Don’t forget to change your configuration XML for GADS to reflect that it should now look in the unixUserPassword attribute instead of division.
And you’re done
With the correct scheduling, this should ensure that your password hashes are only visible for a very short time before being protected by Active Directory.
It’s worth remembering that even when the hashes are visible, they aren’t exactly broadcast to every user who logs on. They need to know what to look for, and be able do a bit of scripting to retrieve them all. And while it is feasible today to bruteforce SHA1 passwords, it’s not as trivial as some would have you believe. At the time of writing, you can’t Google for “brute force SHA1” and download a GUI tool from the first hit you find.