Public/Get-LAPS.ps1
function Get-LAPS { <# .Synopsis This module will query Active Directory for the hostname, LAPS (local administrator) stored password, and password expiration for each computer account. .DESCRIPTION This module will query Active Directory for the hostname, LAPS (local administrator) stored password, and password expiration for each computer account. The script filters out disabled domain computers. LAPS password storage can be identified by querying the (domain user available) ms-MCS-AdmPwdExpirationTime attribute. If the attribute (timestamp) exists, LAPS is in use for local administrator passwords. Access to ms-MCS-AdmPwd attribute should be restricted to privileged accounts. Also, since the script uses data tables to output affected systems the results can be easily piped to other commands such as test-connection or a Export-Csv. .EXAMPLE The example below shows the standard command usage. Disabled system are excluded by default. If your user doesn't have the rights to read the password, then it will show 0 for Readable. PS C:\> Get-LAPS -DomainController 192.168.1.1 -Credential demo.com\administrator | Format-Table -AutoSize Hostname Stored Readable Password Expiration -------- ------ -------- -------- ---------- WIN-M8V16OTGIIN.test.domain 0 0 NA WIN-M8V16OTGIIN.test.domain 0 0 NA ASSESS-WIN7-TES.test.domain 1 1 $sl+xbZz2&qtDr 6/3/2015 7:09:28 PM .EXAMPLE The example below shows how to write the output to a csv file. PS C:\> Get-LAPS -DomainController 192.168.1.1 -Credential demo.com\administrator | Export-Csv c:\temp\output.csv -NoTypeInformation .LINK https://blog.netspi.com/running-laps-around-cleartext-passwords/ https://github.com/kfosaaen/Get-LAPS https://technet.microsoft.com/en-us/library/security/3062591 .NOTES Author: Karl Fosaaen - 2015, NetSPI Version: Get-LAPS.psm1 v1.0 Comments: The technique used to query LDAP was based on the "Get-AuditDSComputerAccount" function found in Carlos Perez's PoshSec-Mod project. The general idea is based off of a Twitter conversation with @_wald0. The bones of this were borrowed (with permission) from Scott Sutherland's Get-ExploitableSystems function. #> [CmdletBinding()] Param( [Parameter(Mandatory=$false, HelpMessage="Credentials to use when connecting to a Domain Controller.")] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory=$false, HelpMessage="Domain controller for Domain and Site that you want to query against.")] [string]$DomainController, [Parameter(Mandatory=$false, HelpMessage="Maximum number of Objects to pull from AD, limit is 1,000.")] [int]$Limit = 1000, [Parameter(Mandatory=$false, HelpMessage="scope of a search as either a base, one-level, or subtree search, default is subtree.")] [ValidateSet("Subtree","OneLevel","Base")] [string]$SearchScope = "Subtree", [Parameter(Mandatory=$false, HelpMessage="Distinguished Name Path to limit search to.")] [string]$SearchDN ) Begin { if ($DomainController -and $Credential.GetNetworkCredential().Password) { $objDomain = New-Object System.DirectoryServices.DirectoryEntry "LDAP://$($DomainController)", $Credential.UserName,$Credential.GetNetworkCredential().Password $objSearcher = New-Object System.DirectoryServices.DirectorySearcher $objDomain } else { $objDomain = [ADSI]"" $objSearcher = New-Object System.DirectoryServices.DirectorySearcher $objDomain } } Process { # Status user Write-Verbose "[*] Grabbing computer accounts from Active Directory..." # Create data table for hostnames, and passwords from LDAP $TableAdsComputers = New-Object System.Data.DataTable $TableAdsComputers.Columns.Add('Hostname') | Out-Null $TableAdsComputers.Columns.Add('Stored') | Out-Null $TableAdsComputers.Columns.Add('Readable') | Out-Null $TableAdsComputers.Columns.Add('Password') | Out-Null $TableAdsComputers.Columns.Add('Expiration') | Out-Null # ---------------------------------------------------------------- # Grab computer account information from Active Directory via LDAP # ---------------------------------------------------------------- $CompFilter = "(&(objectCategory=Computer)(!(CN=SV*))(!(CN=DMZ*))(!(CN=STG*))(!(CN=CNF*))(!(CN=*EDC*)))" $ObjSearcher.PageSize = $Limit $ObjSearcher.Filter = $CompFilter $ObjSearcher.SearchScope = "Subtree" if ($SearchDN) { $objSearcher.SearchDN = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$($SearchDN)") } $ObjSearcher.FindAll() | ForEach-Object { # Setup fields $CurrentHost = $($_.properties['dnshostname']) $CurrentUac = $($_.properties['useraccountcontrol']) $CurrentPassword = $($_.properties['ms-MCS-AdmPwd']) if ($_.properties['ms-MCS-AdmPwdExpirationTime'] -ge 0){$CurrentExpiration = $([datetime]::FromFileTime([convert]::ToInt64($_.properties['ms-MCS-AdmPwdExpirationTime'],10)))} else{$CurrentExpiration = "NA"} $PasswordAvailable = 0 $PasswordStored = 1 # Convert useraccountcontrol to binary so flags can be checked # http://support.microsoft.com/en-us/kb/305144 # http://blogs.technet.com/b/askpfeplat/archive/2014/01/15/understanding-the-useraccountcontrol-attribute-in-active-directory.aspx $CurrentUacBin = [convert]::ToString($CurrentUac,2) # Check the 2nd to last value to determine if its disabled $DisableOffset = $CurrentUacBin.Length - 2 $CurrentDisabled = $CurrentUacBin.Substring($DisableOffset,1) # Set flag if stored password is not available if ($CurrentExpiration -eq "NA"){$PasswordStored = 0} if ($CurrentPassword.length -ge 1){$PasswordAvailable = 1} # Add computer to list if it's enabled if ($CurrentDisabled -eq 0){ # Add domain computer to data table $TableAdsComputers.Rows.Add($CurrentHost,$PasswordStored,$PasswordAvailable,$CurrentPassword, $CurrentExpiration) | Out-Null } } } End { # Display results $TableAdsComputers | Sort-Object {$_.Hostname} } } |