├── README.md └── LAPSToolkit.ps1 /README.md: -------------------------------------------------------------------------------- 1 | # LAPSToolkit 2 | Functions written in PowerShell that leverage PowerView to audit and attack Active Directory environments that have deployed Microsoft's Local Administrator Password Solution (LAPS). It includes finding groups specifically delegated by sysadmins, finding users with "All Extended Rights" that can view passwords, and viewing all computers with LAPS enabled. 3 | 4 | Please submit issues or comments for any problems or performance improvements. This project was created with code from an older version of PowerView. 5 | 6 | For more information on how LAPS works see https://adsecurity.org/?p=1790. 7 | 8 | #### Get-LAPSComputers: 9 | Displays all computers with LAPS enabled, password expriation, and password if user has access 10 | #### Find-LAPSDelegatedGroups: 11 | Searches through all OUs to see which AD groups can read the ms-Mcs-AdmPwd attribute 12 | #### Find-AdmPwdExtendedRights 13 | Parses through ExtendedRights for each AD computer with LAPS enabled and looks for which group has read access and if any user has "All Extended Rights". Sysadmins may not be aware the users with All Extended Rights can view passwords and may be less protected than the users in the delegated groups. An example is the user which adds a computer to the domain automatically receives the "All Extended Rights" permission. Since this function will parse ACLs for each AD computer, this can take very long with a larger domain. 14 | 15 | Special thanks to Sean Metcalf (@pyrotek3), Will Schroeder (@harmj0y), Karl Fosaaen (@kfosaaen), Matt Graeber (@mattifestation) for research and code with LAPS, AD permissions, and offensive PowerShell. 16 | -------------------------------------------------------------------------------- /LAPSToolkit.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 2 2 | 3 | <# 4 | 5 | LAPSToolkit 6 | 7 | Uses many functions from PowerView 8 | URL: https://github.com/PowerShellMafia/PowerSploit/blob/master/Recon/PowerView.ps1 9 | Author: Will Schroeder (@harmj0y) 10 | 11 | Credits: 12 | Will Schroeder (@harmj0y), 13 | Sean Metcalf (@pyrotek3), 14 | Matt Graeber (@mattifestation), 15 | Karl Fosaaen (@kfosaaen) 16 | 17 | 18 | #> 19 | 20 | 21 | ######################################################## 22 | # 23 | # Functions from PowerView 24 | # Author: Will Schroeder (@harmj0y) 25 | # 26 | ######################################################## 27 | 28 | filter Export-PowerViewCSV { 29 | <# 30 | .SYNOPSIS 31 | 32 | This helper exports an -InputObject to a .csv in a thread-safe manner 33 | using a mutex. This is so the various multi-threaded functions in 34 | PowerView has a thread-safe way to export output to the same file. 35 | 36 | Based partially on Dmitry Sotnikov's Export-CSV code 37 | at http://poshcode.org/1590 38 | 39 | .LINK 40 | 41 | http://poshcode.org/1590 42 | http://dmitrysotnikov.wordpress.com/2010/01/19/Export-Csv-append/ 43 | #> 44 | Param( 45 | [Parameter(Mandatory=$True, ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)] 46 | [System.Management.Automation.PSObject[]] 47 | $InputObject, 48 | 49 | [Parameter(Mandatory=$True, Position=0)] 50 | [String] 51 | [ValidateNotNullOrEmpty()] 52 | $OutFile 53 | ) 54 | 55 | $ObjectCSV = $InputObject | ConvertTo-Csv -NoTypeInformation 56 | 57 | # mutex so threaded code doesn't stomp on the output file 58 | $Mutex = New-Object System.Threading.Mutex $False,'CSVMutex'; 59 | $Null = $Mutex.WaitOne() 60 | 61 | if (Test-Path -Path $OutFile) { 62 | # hack to skip the first line of output if the file already exists 63 | $ObjectCSV | ForEach-Object { $Start=$True }{ if ($Start) {$Start=$False} else {$_} } | Out-File -Encoding 'ASCII' -Append -FilePath $OutFile 64 | } 65 | else { 66 | $ObjectCSV | Out-File -Encoding 'ASCII' -Append -FilePath $OutFile 67 | } 68 | 69 | $Mutex.ReleaseMutex() 70 | } 71 | 72 | 73 | filter Convert-SidToName { 74 | <# 75 | .SYNOPSIS 76 | 77 | Converts a security identifier (SID) to a group/user name. 78 | 79 | .PARAMETER SID 80 | 81 | The SID to convert. 82 | 83 | .EXAMPLE 84 | 85 | PS C:\> Convert-SidToName S-1-5-21-2620891829-2411261497-1773853088-1105 86 | #> 87 | [CmdletBinding()] 88 | param( 89 | [Parameter(Mandatory=$True, ValueFromPipeline=$True)] 90 | [String] 91 | [ValidatePattern('^S-1-.*')] 92 | $SID 93 | ) 94 | 95 | try { 96 | $SID2 = $SID.trim('*') 97 | 98 | # try to resolve any built-in SIDs first 99 | # from https://support.microsoft.com/en-us/kb/243330 100 | Switch ($SID2) 101 | { 102 | 'S-1-0' { 'Null Authority' } 103 | 'S-1-0-0' { 'Nobody' } 104 | 'S-1-1' { 'World Authority' } 105 | 'S-1-1-0' { 'Everyone' } 106 | 'S-1-2' { 'Local Authority' } 107 | 'S-1-2-0' { 'Local' } 108 | 'S-1-2-1' { 'Console Logon ' } 109 | 'S-1-3' { 'Creator Authority' } 110 | 'S-1-3-0' { 'Creator Owner' } 111 | 'S-1-3-1' { 'Creator Group' } 112 | 'S-1-3-2' { 'Creator Owner Server' } 113 | 'S-1-3-3' { 'Creator Group Server' } 114 | 'S-1-3-4' { 'Owner Rights' } 115 | 'S-1-4' { 'Non-unique Authority' } 116 | 'S-1-5' { 'NT Authority' } 117 | 'S-1-5-1' { 'Dialup' } 118 | 'S-1-5-2' { 'Network' } 119 | 'S-1-5-3' { 'Batch' } 120 | 'S-1-5-4' { 'Interactive' } 121 | 'S-1-5-6' { 'Service' } 122 | 'S-1-5-7' { 'Anonymous' } 123 | 'S-1-5-8' { 'Proxy' } 124 | 'S-1-5-9' { 'Enterprise Domain Controllers' } 125 | 'S-1-5-10' { 'Principal Self' } 126 | 'S-1-5-11' { 'Authenticated Users' } 127 | 'S-1-5-12' { 'Restricted Code' } 128 | 'S-1-5-13' { 'Terminal Server Users' } 129 | 'S-1-5-14' { 'Remote Interactive Logon' } 130 | 'S-1-5-15' { 'This Organization ' } 131 | 'S-1-5-17' { 'This Organization ' } 132 | 'S-1-5-18' { 'Local System' } 133 | 'S-1-5-19' { 'NT Authority' } 134 | 'S-1-5-20' { 'NT Authority' } 135 | 'S-1-5-80-0' { 'All Services ' } 136 | 'S-1-5-32-544' { 'BUILTIN\Administrators' } 137 | 'S-1-5-32-545' { 'BUILTIN\Users' } 138 | 'S-1-5-32-546' { 'BUILTIN\Guests' } 139 | 'S-1-5-32-547' { 'BUILTIN\Power Users' } 140 | 'S-1-5-32-548' { 'BUILTIN\Account Operators' } 141 | 'S-1-5-32-549' { 'BUILTIN\Server Operators' } 142 | 'S-1-5-32-550' { 'BUILTIN\Print Operators' } 143 | 'S-1-5-32-551' { 'BUILTIN\Backup Operators' } 144 | 'S-1-5-32-552' { 'BUILTIN\Replicators' } 145 | 'S-1-5-32-554' { 'BUILTIN\Pre-Windows 2000 Compatible Access' } 146 | 'S-1-5-32-555' { 'BUILTIN\Remote Desktop Users' } 147 | 'S-1-5-32-556' { 'BUILTIN\Network Configuration Operators' } 148 | 'S-1-5-32-557' { 'BUILTIN\Incoming Forest Trust Builders' } 149 | 'S-1-5-32-558' { 'BUILTIN\Performance Monitor Users' } 150 | 'S-1-5-32-559' { 'BUILTIN\Performance Log Users' } 151 | 'S-1-5-32-560' { 'BUILTIN\Windows Authorization Access Group' } 152 | 'S-1-5-32-561' { 'BUILTIN\Terminal Server License Servers' } 153 | 'S-1-5-32-562' { 'BUILTIN\Distributed COM Users' } 154 | 'S-1-5-32-569' { 'BUILTIN\Cryptographic Operators' } 155 | 'S-1-5-32-573' { 'BUILTIN\Event Log Readers' } 156 | 'S-1-5-32-574' { 'BUILTIN\Certificate Service DCOM Access' } 157 | 'S-1-5-32-575' { 'BUILTIN\RDS Remote Access Servers' } 158 | 'S-1-5-32-576' { 'BUILTIN\RDS Endpoint Servers' } 159 | 'S-1-5-32-577' { 'BUILTIN\RDS Management Servers' } 160 | 'S-1-5-32-578' { 'BUILTIN\Hyper-V Administrators' } 161 | 'S-1-5-32-579' { 'BUILTIN\Access Control Assistance Operators' } 162 | 'S-1-5-32-580' { 'BUILTIN\Access Control Assistance Operators' } 163 | Default { 164 | $Obj = (New-Object System.Security.Principal.SecurityIdentifier($SID2)) 165 | $Obj.Translate( [System.Security.Principal.NTAccount]).Value 166 | } 167 | } 168 | } 169 | catch { 170 | Write-Debug "Invalid SID: $SID" 171 | $SID 172 | } 173 | } 174 | 175 | 176 | filter Convert-ADName { 177 | <# 178 | .SYNOPSIS 179 | 180 | Converts user/group names from NT4 (DOMAIN\user) or domainSimple (user@domain.com) 181 | to canonical format (domain.com/Users/user) or NT4. 182 | 183 | Based on Bill Stewart's code from this article: 184 | http://windowsitpro.com/active-directory/translating-active-directory-object-names-between-formats 185 | 186 | .PARAMETER ObjectName 187 | 188 | The user/group name to convert. 189 | 190 | .PARAMETER InputType 191 | 192 | The InputType of the user/group name ("NT4","Simple","Canonical"). 193 | 194 | .PARAMETER OutputType 195 | 196 | The OutputType of the user/group name ("NT4","Simple","Canonical"). 197 | 198 | .EXAMPLE 199 | 200 | PS C:\> Convert-ADName -ObjectName "dev\dfm" 201 | 202 | Returns "dev.testlab.local/Users/Dave" 203 | 204 | .EXAMPLE 205 | 206 | PS C:\> Convert-SidToName "S-..." | Convert-ADName 207 | 208 | Returns the canonical name for the resolved SID. 209 | 210 | .LINK 211 | 212 | http://windowsitpro.com/active-directory/translating-active-directory-object-names-between-formats 213 | #> 214 | [CmdletBinding()] 215 | param( 216 | [Parameter(Mandatory=$True, ValueFromPipeline=$True)] 217 | [String] 218 | $ObjectName, 219 | 220 | [String] 221 | [ValidateSet("NT4","Simple","Canonical")] 222 | $InputType, 223 | 224 | [String] 225 | [ValidateSet("NT4","Simple","Canonical")] 226 | $OutputType 227 | ) 228 | 229 | $NameTypes = @{ 230 | "Canonical" = 2 231 | "NT4" = 3 232 | "Simple" = 5 233 | } 234 | 235 | if(!$PSBoundParameters['InputType']) { 236 | if( ($ObjectName.split('/')).Count -eq 2 ) { 237 | $ObjectName = $ObjectName.replace('/', '\') 238 | } 239 | 240 | if($ObjectName -match "^[A-Za-z]+\\[A-Za-z ]+$") { 241 | $InputType = 'NT4' 242 | } 243 | elseif($ObjectName -match "^[A-Za-z ]+@[A-Za-z\.]+") { 244 | $InputType = 'Simple' 245 | } 246 | elseif($ObjectName -match "^[A-Za-z\.]+/[A-Za-z]+/[A-Za-z/ ]+") { 247 | $InputType = 'Canonical' 248 | } 249 | else { 250 | Write-Warning "Can not identify InType for $ObjectName" 251 | return $ObjectName 252 | } 253 | } 254 | elseif($InputType -eq 'NT4') { 255 | $ObjectName = $ObjectName.replace('/', '\') 256 | } 257 | 258 | if(!$PSBoundParameters['OutputType']) { 259 | $OutputType = Switch($InputType) { 260 | 'NT4' {'Canonical'} 261 | 'Simple' {'NT4'} 262 | 'Canonical' {'NT4'} 263 | } 264 | } 265 | 266 | # try to extract the domain from the given format 267 | $Domain = Switch($InputType) { 268 | 'NT4' { $ObjectName.split("\")[0] } 269 | 'Simple' { $ObjectName.split("@")[1] } 270 | 'Canonical' { $ObjectName.split("/")[0] } 271 | } 272 | 273 | # Accessor functions to simplify calls to NameTranslate 274 | function Invoke-Method([__ComObject] $Object, [String] $Method, $Parameters) { 275 | $Output = $Object.GetType().InvokeMember($Method, "InvokeMethod", $Null, $Object, $Parameters) 276 | if ( $Output ) { $Output } 277 | } 278 | function Set-Property([__ComObject] $Object, [String] $Property, $Parameters) { 279 | [Void] $Object.GetType().InvokeMember($Property, "SetProperty", $Null, $Object, $Parameters) 280 | } 281 | 282 | $Translate = New-Object -ComObject NameTranslate 283 | 284 | try { 285 | Invoke-Method $Translate "Init" (1, $Domain) 286 | } 287 | catch [System.Management.Automation.MethodInvocationException] { 288 | Write-Debug "Error with translate init in Convert-ADName: $_" 289 | } 290 | 291 | Set-Property $Translate "ChaseReferral" (0x60) 292 | 293 | try { 294 | Invoke-Method $Translate "Set" ($NameTypes[$InputType], $ObjectName) 295 | (Invoke-Method $Translate "Get" ($NameTypes[$OutputType])) 296 | } 297 | catch [System.Management.Automation.MethodInvocationException] { 298 | Write-Debug "Error with translate Set/Get in Convert-ADName: $_" 299 | } 300 | } 301 | 302 | 303 | function ConvertFrom-UACValue { 304 | <# 305 | .SYNOPSIS 306 | 307 | Converts a UAC int value to human readable form. 308 | 309 | .PARAMETER Value 310 | 311 | The int UAC value to convert. 312 | 313 | .PARAMETER ShowAll 314 | 315 | Show all UAC values, with a + indicating the value is currently set. 316 | 317 | .EXAMPLE 318 | 319 | PS C:\> ConvertFrom-UACValue -Value 66176 320 | 321 | Convert the UAC value 66176 to human readable format. 322 | 323 | .EXAMPLE 324 | 325 | PS C:\> Get-NetUser jason | select useraccountcontrol | ConvertFrom-UACValue 326 | 327 | Convert the UAC value for 'jason' to human readable format. 328 | 329 | .EXAMPLE 330 | 331 | PS C:\> Get-NetUser jason | select useraccountcontrol | ConvertFrom-UACValue -ShowAll 332 | 333 | Convert the UAC value for 'jason' to human readable format, showing all 334 | possible UAC values. 335 | #> 336 | 337 | [CmdletBinding()] 338 | param( 339 | [Parameter(Mandatory=$True, ValueFromPipeline=$True)] 340 | $Value, 341 | 342 | [Switch] 343 | $ShowAll 344 | ) 345 | 346 | begin { 347 | # values from https://support.microsoft.com/en-us/kb/305144 348 | $UACValues = New-Object System.Collections.Specialized.OrderedDictionary 349 | $UACValues.Add("SCRIPT", 1) 350 | $UACValues.Add("ACCOUNTDISABLE", 2) 351 | $UACValues.Add("HOMEDIR_REQUIRED", 8) 352 | $UACValues.Add("LOCKOUT", 16) 353 | $UACValues.Add("PASSWD_NOTREQD", 32) 354 | $UACValues.Add("PASSWD_CANT_CHANGE", 64) 355 | $UACValues.Add("ENCRYPTED_TEXT_PWD_ALLOWED", 128) 356 | $UACValues.Add("TEMP_DUPLICATE_ACCOUNT", 256) 357 | $UACValues.Add("NORMAL_ACCOUNT", 512) 358 | $UACValues.Add("INTERDOMAIN_TRUST_ACCOUNT", 2048) 359 | $UACValues.Add("WORKSTATION_TRUST_ACCOUNT", 4096) 360 | $UACValues.Add("SERVER_TRUST_ACCOUNT", 8192) 361 | $UACValues.Add("DONT_EXPIRE_PASSWORD", 65536) 362 | $UACValues.Add("MNS_LOGON_ACCOUNT", 131072) 363 | $UACValues.Add("SMARTCARD_REQUIRED", 262144) 364 | $UACValues.Add("TRUSTED_FOR_DELEGATION", 524288) 365 | $UACValues.Add("NOT_DELEGATED", 1048576) 366 | $UACValues.Add("USE_DES_KEY_ONLY", 2097152) 367 | $UACValues.Add("DONT_REQ_PREAUTH", 4194304) 368 | $UACValues.Add("PASSWORD_EXPIRED", 8388608) 369 | $UACValues.Add("TRUSTED_TO_AUTH_FOR_DELEGATION", 16777216) 370 | $UACValues.Add("PARTIAL_SECRETS_ACCOUNT", 67108864) 371 | } 372 | 373 | process { 374 | 375 | $ResultUACValues = New-Object System.Collections.Specialized.OrderedDictionary 376 | 377 | if($Value -is [Int]) { 378 | $IntValue = $Value 379 | } 380 | elseif ($Value -is [PSCustomObject]) { 381 | if($Value.useraccountcontrol) { 382 | $IntValue = $Value.useraccountcontrol 383 | } 384 | } 385 | else { 386 | Write-Warning "Invalid object input for -Value : $Value" 387 | return $Null 388 | } 389 | 390 | if($ShowAll) { 391 | foreach ($UACValue in $UACValues.GetEnumerator()) { 392 | if( ($IntValue -band $UACValue.Value) -eq $UACValue.Value) { 393 | $ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)+") 394 | } 395 | else { 396 | $ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)") 397 | } 398 | } 399 | } 400 | else { 401 | foreach ($UACValue in $UACValues.GetEnumerator()) { 402 | if( ($IntValue -band $UACValue.Value) -eq $UACValue.Value) { 403 | $ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)") 404 | } 405 | } 406 | } 407 | $ResultUACValues 408 | } 409 | } 410 | 411 | 412 | filter Get-Proxy { 413 | <# 414 | .SYNOPSIS 415 | 416 | Enumerates the proxy server and WPAD conents for the current user. 417 | 418 | .PARAMETER ComputerName 419 | 420 | The computername to enumerate proxy settings on, defaults to local host. 421 | 422 | .EXAMPLE 423 | 424 | PS C:\> Get-Proxy 425 | 426 | Returns the current proxy settings. 427 | #> 428 | param( 429 | [Parameter(ValueFromPipeline=$True)] 430 | [ValidateNotNullOrEmpty()] 431 | [String] 432 | $ComputerName = $ENV:COMPUTERNAME 433 | ) 434 | 435 | try { 436 | $Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('CurrentUser', $ComputerName) 437 | $RegKey = $Reg.OpenSubkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings") 438 | $ProxyServer = $RegKey.GetValue('ProxyServer') 439 | $AutoConfigURL = $RegKey.GetValue('AutoConfigURL') 440 | 441 | $Wpad = "" 442 | if($AutoConfigURL -and ($AutoConfigURL -ne "")) { 443 | try { 444 | $Wpad = (New-Object Net.Webclient).DownloadString($AutoConfigURL) 445 | } 446 | catch { 447 | Write-Warning "Error connecting to AutoConfigURL : $AutoConfigURL" 448 | } 449 | } 450 | 451 | if($ProxyServer -or $AutoConfigUrl) { 452 | 453 | $Properties = @{ 454 | 'ProxyServer' = $ProxyServer 455 | 'AutoConfigURL' = $AutoConfigURL 456 | 'Wpad' = $Wpad 457 | } 458 | 459 | New-Object -TypeName PSObject -Property $Properties 460 | } 461 | else { 462 | Write-Warning "No proxy settings found for $ComputerName" 463 | } 464 | } 465 | catch { 466 | Write-Warning "Error enumerating proxy settings for $ComputerName : $_" 467 | } 468 | } 469 | 470 | 471 | function Get-PathAcl { 472 | <# 473 | .SYNOPSIS 474 | 475 | Enumerates the ACL for a given file path. 476 | 477 | .PARAMETER Path 478 | 479 | The local/remote path to enumerate the ACLs for. 480 | 481 | .PARAMETER Recurse 482 | 483 | If any ACL results are groups, recurse and retrieve user membership. 484 | 485 | .EXAMPLE 486 | 487 | PS C:\> Get-PathAcl "\\SERVER\Share\" 488 | 489 | Returns ACLs for the given UNC share. 490 | #> 491 | [CmdletBinding()] 492 | param( 493 | [Parameter(Mandatory=$True, ValueFromPipeline=$True)] 494 | [String] 495 | $Path, 496 | 497 | [Switch] 498 | $Recurse 499 | ) 500 | 501 | begin { 502 | 503 | function Convert-FileRight { 504 | 505 | # From http://stackoverflow.com/questions/28029872/retrieving-security-descriptor-and-getting-number-for-filesystemrights 506 | 507 | [CmdletBinding()] 508 | param( 509 | [Int] 510 | $FSR 511 | ) 512 | 513 | $AccessMask = @{ 514 | [uint32]'0x80000000' = 'GenericRead' 515 | [uint32]'0x40000000' = 'GenericWrite' 516 | [uint32]'0x20000000' = 'GenericExecute' 517 | [uint32]'0x10000000' = 'GenericAll' 518 | [uint32]'0x02000000' = 'MaximumAllowed' 519 | [uint32]'0x01000000' = 'AccessSystemSecurity' 520 | [uint32]'0x00100000' = 'Synchronize' 521 | [uint32]'0x00080000' = 'WriteOwner' 522 | [uint32]'0x00040000' = 'WriteDAC' 523 | [uint32]'0x00020000' = 'ReadControl' 524 | [uint32]'0x00010000' = 'Delete' 525 | [uint32]'0x00000100' = 'WriteAttributes' 526 | [uint32]'0x00000080' = 'ReadAttributes' 527 | [uint32]'0x00000040' = 'DeleteChild' 528 | [uint32]'0x00000020' = 'Execute/Traverse' 529 | [uint32]'0x00000010' = 'WriteExtendedAttributes' 530 | [uint32]'0x00000008' = 'ReadExtendedAttributes' 531 | [uint32]'0x00000004' = 'AppendData/AddSubdirectory' 532 | [uint32]'0x00000002' = 'WriteData/AddFile' 533 | [uint32]'0x00000001' = 'ReadData/ListDirectory' 534 | } 535 | 536 | $SimplePermissions = @{ 537 | [uint32]'0x1f01ff' = 'FullControl' 538 | [uint32]'0x0301bf' = 'Modify' 539 | [uint32]'0x0200a9' = 'ReadAndExecute' 540 | [uint32]'0x02019f' = 'ReadAndWrite' 541 | [uint32]'0x020089' = 'Read' 542 | [uint32]'0x000116' = 'Write' 543 | } 544 | 545 | $Permissions = @() 546 | 547 | # get simple permission 548 | $Permissions += $SimplePermissions.Keys | % { 549 | if (($FSR -band $_) -eq $_) { 550 | $SimplePermissions[$_] 551 | $FSR = $FSR -band (-not $_) 552 | } 553 | } 554 | 555 | # get remaining extended permissions 556 | $Permissions += $AccessMask.Keys | 557 | ? { $FSR -band $_ } | 558 | % { $AccessMask[$_] } 559 | 560 | ($Permissions | ?{$_}) -join "," 561 | } 562 | } 563 | 564 | process { 565 | 566 | try { 567 | $ACL = Get-Acl -Path $Path 568 | 569 | $ACL.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier]) | ForEach-Object { 570 | 571 | $Names = @() 572 | if ($_.IdentityReference -match '^S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+') { 573 | $Object = Get-ADObject -SID $_.IdentityReference 574 | $Names = @() 575 | $SIDs = @($Object.objectsid) 576 | 577 | if ($Recurse -and (@('268435456','268435457','536870912','536870913') -contains $Object.samAccountType)) { 578 | $SIDs += Get-NetGroupMember -SID $Object.objectsid | Select-Object -ExpandProperty MemberSid 579 | } 580 | 581 | $SIDs | ForEach-Object { 582 | $Names += ,@($_, (Convert-SidToName $_)) 583 | } 584 | } 585 | else { 586 | $Names += ,@($_.IdentityReference.Value, (Convert-SidToName $_.IdentityReference.Value)) 587 | } 588 | 589 | ForEach($Name in $Names) { 590 | $Out = New-Object PSObject 591 | $Out | Add-Member Noteproperty 'Path' $Path 592 | $Out | Add-Member Noteproperty 'FileSystemRights' (Convert-FileRight -FSR $_.FileSystemRights.value__) 593 | $Out | Add-Member Noteproperty 'IdentityReference' $Name[1] 594 | $Out | Add-Member Noteproperty 'IdentitySID' $Name[0] 595 | $Out | Add-Member Noteproperty 'AccessControlType' $_.AccessControlType 596 | $Out 597 | } 598 | } 599 | } 600 | catch { 601 | Write-Warning $_ 602 | } 603 | } 604 | } 605 | 606 | 607 | filter Get-NameField { 608 | <# 609 | .SYNOPSIS 610 | 611 | Helper that attempts to extract appropriate field names from 612 | passed computer objects. 613 | 614 | .PARAMETER Object 615 | 616 | The passed object to extract name fields from. 617 | 618 | .PARAMETER DnsHostName 619 | 620 | A DnsHostName to extract through ValueFromPipelineByPropertyName. 621 | 622 | .PARAMETER Name 623 | 624 | A Name to extract through ValueFromPipelineByPropertyName. 625 | 626 | .EXAMPLE 627 | 628 | PS C:\> Get-NetComputer -FullData | Get-NameField 629 | #> 630 | [CmdletBinding()] 631 | param( 632 | [Parameter(ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] 633 | [Object] 634 | $Object, 635 | 636 | [Parameter(ValueFromPipelineByPropertyName = $True)] 637 | [String] 638 | $DnsHostName, 639 | 640 | [Parameter(ValueFromPipelineByPropertyName = $True)] 641 | [String] 642 | $Name 643 | ) 644 | 645 | if($PSBoundParameters['DnsHostName']) { 646 | $DnsHostName 647 | } 648 | elseif($PSBoundParameters['Name']) { 649 | $Name 650 | } 651 | elseif($Object) { 652 | if ( [bool]($Object.PSobject.Properties.name -match "dnshostname") ) { 653 | # objects from Get-NetComputer 654 | $Object.dnshostname 655 | } 656 | elseif ( [bool]($Object.PSobject.Properties.name -match "name") ) { 657 | # objects from Get-NetDomainController 658 | $Object.name 659 | } 660 | else { 661 | # strings and catch alls 662 | $Object 663 | } 664 | } 665 | else { 666 | return $Null 667 | } 668 | } 669 | 670 | 671 | function Convert-LDAPProperty { 672 | <# 673 | .SYNOPSIS 674 | 675 | Helper that converts specific LDAP property result fields. 676 | Used by several of the Get-Net* function. 677 | 678 | .PARAMETER Properties 679 | 680 | Properties object to extract out LDAP fields for display. 681 | #> 682 | param( 683 | [Parameter(Mandatory=$True, ValueFromPipeline=$True)] 684 | [ValidateNotNullOrEmpty()] 685 | $Properties 686 | ) 687 | 688 | $ObjectProperties = @{} 689 | 690 | $Properties.PropertyNames | ForEach-Object { 691 | if (($_ -eq "objectsid") -or ($_ -eq "sidhistory")) { 692 | # convert the SID to a string 693 | $ObjectProperties[$_] = (New-Object System.Security.Principal.SecurityIdentifier($Properties[$_][0],0)).Value 694 | } 695 | elseif($_ -eq "objectguid") { 696 | # convert the GUID to a string 697 | $ObjectProperties[$_] = (New-Object Guid (,$Properties[$_][0])).Guid 698 | } 699 | elseif( ($_ -eq "lastlogon") -or ($_ -eq "lastlogontimestamp") -or ($_ -eq "pwdlastset") -or ($_ -eq "lastlogoff") -or ($_ -eq "badPasswordTime") ) { 700 | # convert timestamps 701 | if ($Properties[$_][0] -is [System.MarshalByRefObject]) { 702 | # if we have a System.__ComObject 703 | $Temp = $Properties[$_][0] 704 | [Int32]$High = $Temp.GetType().InvokeMember("HighPart", [System.Reflection.BindingFlags]::GetProperty, $null, $Temp, $null) 705 | [Int32]$Low = $Temp.GetType().InvokeMember("LowPart", [System.Reflection.BindingFlags]::GetProperty, $null, $Temp, $null) 706 | $ObjectProperties[$_] = ([datetime]::FromFileTime([Int64]("0x{0:x8}{1:x8}" -f $High, $Low))) 707 | } 708 | else { 709 | $ObjectProperties[$_] = ([datetime]::FromFileTime(($Properties[$_][0]))) 710 | } 711 | } 712 | elseif($Properties[$_][0] -is [System.MarshalByRefObject]) { 713 | # try to convert misc com objects 714 | $Prop = $Properties[$_] 715 | try { 716 | $Temp = $Prop[$_][0] 717 | Write-Verbose $_ 718 | [Int32]$High = $Temp.GetType().InvokeMember("HighPart", [System.Reflection.BindingFlags]::GetProperty, $null, $Temp, $null) 719 | [Int32]$Low = $Temp.GetType().InvokeMember("LowPart", [System.Reflection.BindingFlags]::GetProperty, $null, $Temp, $null) 720 | $ObjectProperties[$_] = [Int64]("0x{0:x8}{1:x8}" -f $High, $Low) 721 | } 722 | catch { 723 | $ObjectProperties[$_] = $Prop[$_] 724 | } 725 | } 726 | elseif($Properties[$_].count -eq 1) { 727 | $ObjectProperties[$_] = $Properties[$_][0] 728 | } 729 | else { 730 | $ObjectProperties[$_] = $Properties[$_] 731 | } 732 | } 733 | 734 | New-Object -TypeName PSObject -Property $ObjectProperties 735 | } 736 | 737 | 738 | filter Get-DomainSearcher { 739 | <# 740 | .SYNOPSIS 741 | 742 | Helper used by various functions that takes an ADSpath and 743 | domain specifier and builds the correct ADSI searcher object. 744 | 745 | .PARAMETER Domain 746 | 747 | The domain to use for the query, defaults to the current domain. 748 | 749 | .PARAMETER DomainController 750 | 751 | Domain controller to reflect LDAP queries through. 752 | 753 | .PARAMETER ADSpath 754 | 755 | The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 756 | Useful for OU queries. 757 | 758 | .PARAMETER ADSprefix 759 | 760 | Prefix to set for the searcher (like "CN=Sites,CN=Configuration") 761 | 762 | .PARAMETER PageSize 763 | 764 | The PageSize to set for the LDAP searcher object. 765 | 766 | .PARAMETER Credential 767 | 768 | A [Management.Automation.PSCredential] object of alternate credentials 769 | for connection to the target domain. 770 | 771 | .EXAMPLE 772 | 773 | PS C:\> Get-DomainSearcher -Domain testlab.local 774 | 775 | .EXAMPLE 776 | 777 | PS C:\> Get-DomainSearcher -Domain testlab.local -DomainController SECONDARY.dev.testlab.local 778 | #> 779 | 780 | param( 781 | [Parameter(ValueFromPipeline=$True)] 782 | [String] 783 | $Domain, 784 | 785 | [String] 786 | $DomainController, 787 | 788 | [String] 789 | $ADSpath, 790 | 791 | [String] 792 | $ADSprefix, 793 | 794 | [ValidateRange(1,10000)] 795 | [Int] 796 | $PageSize = 200, 797 | 798 | [Management.Automation.PSCredential] 799 | $Credential 800 | ) 801 | 802 | if(!$Credential) { 803 | if(!$Domain){ 804 | $Domain = (Get-NetDomain).name 805 | } 806 | elseif(!$DomainController) { 807 | try { 808 | # if there's no -DomainController specified, try to pull the primary DC 809 | # to reflect queries through 810 | $DomainController = ((Get-NetDomain).PdcRoleOwner).Name 811 | } 812 | catch { 813 | throw "Get-DomainSearcher: Error in retrieving PDC for current domain" 814 | } 815 | } 816 | } 817 | elseif (!$DomainController) { 818 | try { 819 | $DomainController = ((Get-NetDomain -Credential $Credential).PdcRoleOwner).Name 820 | } 821 | catch { 822 | throw "Get-DomainSearcher: Error in retrieving PDC for current domain" 823 | } 824 | 825 | if(!$DomainController) { 826 | throw "Get-DomainSearcher: Error in retrieving PDC for current domain" 827 | } 828 | } 829 | 830 | $SearchString = "LDAP://" 831 | 832 | if($DomainController) { 833 | $SearchString += $DomainController 834 | if($Domain){ 835 | $SearchString += "/" 836 | } 837 | } 838 | 839 | if($ADSprefix) { 840 | $SearchString += $ADSprefix + "," 841 | } 842 | 843 | if($ADSpath) { 844 | if($ADSpath -like "GC://*") { 845 | # if we're searching the global catalog 846 | $DN = $AdsPath 847 | $SearchString = "" 848 | } 849 | else { 850 | if($ADSpath -like "LDAP://*") { 851 | if($ADSpath -match "LDAP://.+/.+") { 852 | $SearchString = "" 853 | } 854 | else { 855 | $ADSpath = $ADSpath.Substring(7) 856 | } 857 | } 858 | $DN = $ADSpath 859 | } 860 | } 861 | else { 862 | if($Domain -and ($Domain.Trim() -ne "")) { 863 | $DN = "DC=$($Domain.Replace('.', ',DC='))" 864 | } 865 | } 866 | 867 | $SearchString += $DN 868 | Write-Verbose "Get-DomainSearcher search string: $SearchString" 869 | 870 | if($Credential) { 871 | Write-Verbose "Using alternate credentials for LDAP connection" 872 | $DomainObject = New-Object DirectoryServices.DirectoryEntry($SearchString, $Credential.UserName, $Credential.GetNetworkCredential().Password) 873 | $Searcher = New-Object System.DirectoryServices.DirectorySearcher($DomainObject) 874 | } 875 | else { 876 | $Searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]$SearchString) 877 | } 878 | 879 | $Searcher.PageSize = $PageSize 880 | $Searcher 881 | } 882 | 883 | 884 | filter Get-NetDomain { 885 | <# 886 | .SYNOPSIS 887 | 888 | Returns a given domain object. 889 | 890 | .PARAMETER Domain 891 | 892 | The domain name to query for, defaults to the current domain. 893 | 894 | .PARAMETER Credential 895 | 896 | A [Management.Automation.PSCredential] object of alternate credentials 897 | for connection to the target domain. 898 | 899 | .EXAMPLE 900 | 901 | PS C:\> Get-NetDomain -Domain testlab.local 902 | 903 | .EXAMPLE 904 | 905 | PS C:\> "testlab.local" | Get-NetDomain 906 | 907 | .LINK 908 | 909 | http://social.technet.microsoft.com/Forums/scriptcenter/en-US/0c5b3f83-e528-4d49-92a4-dee31f4b481c/finding-the-dn-of-the-the-domain-without-admodule-in-powershell?forum=ITCG 910 | #> 911 | 912 | param( 913 | [Parameter(ValueFromPipeline=$True)] 914 | [String] 915 | $Domain, 916 | 917 | [Management.Automation.PSCredential] 918 | $Credential 919 | ) 920 | 921 | if($Credential) { 922 | 923 | Write-Verbose "Using alternate credentials for Get-NetDomain" 924 | 925 | if(!$Domain) { 926 | # if no domain is supplied, extract the logon domain from the PSCredential passed 927 | $Domain = $Credential.GetNetworkCredential().Domain 928 | Write-Verbose "Extracted domain '$Domain' from -Credential" 929 | } 930 | 931 | $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $Domain, $Credential.UserName, $Credential.GetNetworkCredential().Password) 932 | 933 | try { 934 | [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) 935 | } 936 | catch { 937 | Write-Warning "The specified domain does '$Domain' not exist, could not be contacted, there isn't an existing trust, or the specified credentials are invalid." 938 | $Null 939 | } 940 | } 941 | elseif($Domain) { 942 | $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $Domain) 943 | try { 944 | [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) 945 | } 946 | catch { 947 | Write-Warning "The specified domain '$Domain' does not exist, could not be contacted, or there isn't an existing trust." 948 | $Null 949 | } 950 | } 951 | else { 952 | [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() 953 | } 954 | } 955 | 956 | 957 | filter Get-NetForest { 958 | <# 959 | .SYNOPSIS 960 | 961 | Returns a given forest object. 962 | 963 | .PARAMETER Forest 964 | 965 | The forest name to query for, defaults to the current domain. 966 | 967 | .PARAMETER Credential 968 | 969 | A [Management.Automation.PSCredential] object of alternate credentials 970 | for connection to the target domain. 971 | 972 | .EXAMPLE 973 | 974 | PS C:\> Get-NetForest -Forest external.domain 975 | 976 | .EXAMPLE 977 | 978 | PS C:\> "external.domain" | Get-NetForest 979 | #> 980 | 981 | param( 982 | [Parameter(ValueFromPipeline=$True)] 983 | [String] 984 | $Forest, 985 | 986 | [Management.Automation.PSCredential] 987 | $Credential 988 | ) 989 | 990 | if($Credential) { 991 | 992 | Write-Verbose "Using alternate credentials for Get-NetForest" 993 | 994 | if(!$Forest) { 995 | # if no domain is supplied, extract the logon domain from the PSCredential passed 996 | $Forest = $Credential.GetNetworkCredential().Domain 997 | Write-Verbose "Extracted domain '$Forest' from -Credential" 998 | } 999 | 1000 | $ForestContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Forest', $Forest, $Credential.UserName, $Credential.GetNetworkCredential().Password) 1001 | 1002 | try { 1003 | $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ForestContext) 1004 | } 1005 | catch { 1006 | Write-Warning "The specified forest '$Forest' does not exist, could not be contacted, there isn't an existing trust, or the specified credentials are invalid." 1007 | $Null 1008 | } 1009 | } 1010 | elseif($Forest) { 1011 | $ForestContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Forest', $Forest) 1012 | try { 1013 | $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ForestContext) 1014 | } 1015 | catch { 1016 | Write-Warning "The specified forest '$Forest' does not exist, could not be contacted, or there isn't an existing trust." 1017 | return $Null 1018 | } 1019 | } 1020 | else { 1021 | # otherwise use the current forest 1022 | $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() 1023 | } 1024 | 1025 | if($ForestObject) { 1026 | # get the SID of the forest root 1027 | try { 1028 | $ForestSid = (New-Object System.Security.Principal.NTAccount($ForestObject.RootDomain,"krbtgt")).Translate([System.Security.Principal.SecurityIdentifier]).Value 1029 | $Parts = $ForestSid -Split "-" 1030 | $ForestSid = $Parts[0..$($Parts.length-2)] -join "-" 1031 | $ForestObject | Add-Member NoteProperty 'RootDomainSid' $ForestSid 1032 | } 1033 | catch { 1034 | Write-Verbose "Couldn't translate SID for Forest" 1035 | $ForestSid = "" 1036 | } 1037 | $ForestObject 1038 | } 1039 | } 1040 | 1041 | 1042 | filter Get-NetForestDomain { 1043 | <# 1044 | .SYNOPSIS 1045 | 1046 | Return all domains for a given forest. 1047 | 1048 | .PARAMETER Forest 1049 | 1050 | The forest name to query domain for. 1051 | 1052 | .PARAMETER Credential 1053 | 1054 | A [Management.Automation.PSCredential] object of alternate credentials 1055 | for connection to the target domain. 1056 | 1057 | .EXAMPLE 1058 | 1059 | PS C:\> Get-NetForestDomain 1060 | 1061 | .EXAMPLE 1062 | 1063 | PS C:\> Get-NetForestDomain -Forest external.local 1064 | #> 1065 | 1066 | param( 1067 | [Parameter(ValueFromPipeline=$True)] 1068 | [String] 1069 | $Forest, 1070 | 1071 | [Management.Automation.PSCredential] 1072 | $Credential 1073 | ) 1074 | 1075 | $ForestObject = Get-NetForest -Forest $Forest -Credential $Credential 1076 | 1077 | if($ForestObject) { 1078 | $ForestObject.Domains 1079 | } 1080 | } 1081 | 1082 | 1083 | filter Get-NetForestCatalog { 1084 | <# 1085 | .SYNOPSIS 1086 | 1087 | Return all global catalogs for a given forest. 1088 | 1089 | .PARAMETER Forest 1090 | 1091 | The forest name to query domain for. 1092 | 1093 | .PARAMETER Credential 1094 | 1095 | A [Management.Automation.PSCredential] object of alternate credentials 1096 | for connection to the target domain. 1097 | 1098 | .EXAMPLE 1099 | 1100 | PS C:\> Get-NetForestCatalog 1101 | #> 1102 | 1103 | param( 1104 | [Parameter(ValueFromPipeline=$True)] 1105 | [String] 1106 | $Forest, 1107 | 1108 | [Management.Automation.PSCredential] 1109 | $Credential 1110 | ) 1111 | 1112 | $ForestObject = Get-NetForest -Forest $Forest -Credential $Credential 1113 | 1114 | if($ForestObject) { 1115 | $ForestObject.FindAllGlobalCatalogs() 1116 | } 1117 | } 1118 | 1119 | 1120 | filter Get-NetDomainController { 1121 | <# 1122 | .SYNOPSIS 1123 | 1124 | Return the current domain controllers for the active domain. 1125 | 1126 | .PARAMETER Domain 1127 | 1128 | The domain to query for domain controllers, defaults to the current domain. 1129 | 1130 | .PARAMETER DomainController 1131 | 1132 | Domain controller to reflect LDAP queries through. 1133 | 1134 | .PARAMETER LDAP 1135 | 1136 | Switch. Use LDAP queries to determine the domain controllers. 1137 | 1138 | .PARAMETER Credential 1139 | 1140 | A [Management.Automation.PSCredential] object of alternate credentials 1141 | for connection to the target domain. 1142 | 1143 | .EXAMPLE 1144 | 1145 | PS C:\> Get-NetDomainController -Domain 'test.local' 1146 | 1147 | Determine the domain controllers for 'test.local'. 1148 | 1149 | .EXAMPLE 1150 | 1151 | PS C:\> Get-NetDomainController -Domain 'test.local' -LDAP 1152 | 1153 | Determine the domain controllers for 'test.local' using LDAP queries. 1154 | 1155 | .EXAMPLE 1156 | 1157 | PS C:\> 'test.local' | Get-NetDomainController 1158 | 1159 | Determine the domain controllers for 'test.local'. 1160 | #> 1161 | 1162 | [CmdletBinding()] 1163 | param( 1164 | [Parameter(ValueFromPipeline=$True)] 1165 | [String] 1166 | $Domain, 1167 | 1168 | [String] 1169 | $DomainController, 1170 | 1171 | [Switch] 1172 | $LDAP, 1173 | 1174 | [Management.Automation.PSCredential] 1175 | $Credential 1176 | ) 1177 | 1178 | if($LDAP -or $DomainController) { 1179 | # filter string to return all domain controllers 1180 | Get-NetComputer -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -Filter '(userAccountControl:1.2.840.113556.1.4.803:=8192)' 1181 | } 1182 | else { 1183 | $FoundDomain = Get-NetDomain -Domain $Domain -Credential $Credential 1184 | if($FoundDomain) { 1185 | $Founddomain.DomainControllers 1186 | } 1187 | } 1188 | } 1189 | 1190 | 1191 | function Get-ObjectAcl { 1192 | <# 1193 | .SYNOPSIS 1194 | Returns the ACLs associated with a specific active directory object. 1195 | 1196 | Thanks Sean Metcalf (@pyrotek3) for the idea and guidance. 1197 | 1198 | .PARAMETER SamAccountName 1199 | 1200 | Object name to filter for. 1201 | 1202 | .PARAMETER Name 1203 | 1204 | Object name to filter for. 1205 | 1206 | .PARAMETER DistinguishedName 1207 | 1208 | Object distinguished name to filter for. 1209 | 1210 | .PARAMETER ResolveGUIDs 1211 | 1212 | Switch. Resolve GUIDs to their display names. 1213 | 1214 | .PARAMETER Filter 1215 | 1216 | A customized ldap filter string to use, e.g. "(description=*admin*)" 1217 | 1218 | .PARAMETER ADSpath 1219 | 1220 | The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 1221 | Useful for OU queries. 1222 | 1223 | .PARAMETER ADSprefix 1224 | 1225 | Prefix to set for the searcher (like "CN=Sites,CN=Configuration") 1226 | 1227 | .PARAMETER RightsFilter 1228 | 1229 | Only return results with the associated rights, "All", "ResetPassword","WriteMembers" 1230 | 1231 | .PARAMETER Domain 1232 | 1233 | The domain to use for the query, defaults to the current domain. 1234 | 1235 | .PARAMETER DomainController 1236 | 1237 | Domain controller to reflect LDAP queries through. 1238 | 1239 | .PARAMETER PageSize 1240 | 1241 | The PageSize to set for the LDAP searcher object. 1242 | 1243 | .PARAMETER Credential 1244 | 1245 | A [Management.Automation.PSCredential] object of alternate credentials 1246 | for connection to the target domain. 1247 | 1248 | .EXAMPLE 1249 | 1250 | PS C:\> Get-ObjectAcl -SamAccountName matt.admin -domain testlab.local 1251 | 1252 | Get the ACLs for the matt.admin user in the testlab.local domain 1253 | 1254 | .EXAMPLE 1255 | 1256 | PS C:\> Get-ObjectAcl -SamAccountName matt.admin -domain testlab.local -ResolveGUIDs 1257 | 1258 | Get the ACLs for the matt.admin user in the testlab.local domain and 1259 | resolve relevant GUIDs to their display names. 1260 | 1261 | .EXAMPLE 1262 | 1263 | PS C:\> Get-NetOU -FullData | Get-ObjectAcl -ResolveGUIDs 1264 | 1265 | Enumerate the ACL permissions for all OUs in the domain. 1266 | #> 1267 | 1268 | [CmdletBinding()] 1269 | Param ( 1270 | [Parameter(ValueFromPipelineByPropertyName=$True)] 1271 | [String] 1272 | $SamAccountName, 1273 | 1274 | [Parameter(ValueFromPipelineByPropertyName=$True)] 1275 | [String] 1276 | $Name = "*", 1277 | 1278 | [Parameter(ValueFromPipelineByPropertyName=$True)] 1279 | [String] 1280 | $DistinguishedName = "*", 1281 | 1282 | [Switch] 1283 | $ResolveGUIDs, 1284 | 1285 | [String] 1286 | $Filter, 1287 | 1288 | [String] 1289 | $ADSpath, 1290 | 1291 | [String] 1292 | $ADSprefix, 1293 | 1294 | [String] 1295 | [ValidateSet("All","ResetPassword","WriteMembers")] 1296 | $RightsFilter, 1297 | 1298 | [String] 1299 | $Domain, 1300 | 1301 | [String] 1302 | $DomainController, 1303 | 1304 | [ValidateRange(1,10000)] 1305 | [Int] 1306 | $PageSize = 200, 1307 | 1308 | [Management.Automation.PSCredential] 1309 | $Credential 1310 | ) 1311 | 1312 | begin { 1313 | $Searcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -ADSprefix $ADSprefix -PageSize $PageSize -Credential $Credential 1314 | 1315 | # get a GUID -> name mapping 1316 | if($ResolveGUIDs) { 1317 | $GUIDs = Get-GUIDMap -Domain $Domain -DomainController $DomainController -PageSize $PageSize -Credential $Credential 1318 | } 1319 | } 1320 | 1321 | process { 1322 | 1323 | if ($Searcher) { 1324 | 1325 | if($SamAccountName) { 1326 | $Searcher.filter="(&(samaccountname=$SamAccountName)(name=$Name)(distinguishedname=$DistinguishedName)$Filter)" 1327 | } 1328 | else { 1329 | $Searcher.filter="(&(name=$Name)(distinguishedname=$DistinguishedName)$Filter)" 1330 | } 1331 | 1332 | try { 1333 | $Results = $Searcher.FindAll() 1334 | 1335 | $Results | Where-Object {$_} | ForEach-Object { 1336 | if($Credential) { 1337 | $Object = New-Object -TypeName System.DirectoryServices.DirectoryEntry($_.path, $($Credential.UserName),$($Credential.GetNetworkCredential().password)) 1338 | } 1339 | else { 1340 | $Object = [adsi]($_.path) 1341 | } 1342 | 1343 | if($Object.distinguishedname) { 1344 | $Access = $Object.PsBase.ObjectSecurity.access 1345 | $Access | ForEach-Object { 1346 | $_ | Add-Member NoteProperty 'ObjectDN' $Object.distinguishedname[0] 1347 | 1348 | if($Object.objectsid[0]){ 1349 | $S = (New-Object System.Security.Principal.SecurityIdentifier($Object.objectsid[0],0)).Value 1350 | } 1351 | else { 1352 | $S = $Null 1353 | } 1354 | 1355 | $_ | Add-Member NoteProperty 'ObjectSID' $S 1356 | $_ 1357 | } 1358 | } 1359 | } | ForEach-Object { 1360 | if($RightsFilter) { 1361 | $GuidFilter = Switch ($RightsFilter) { 1362 | "ResetPassword" { "00299570-246d-11d0-a768-00aa006e0529" } 1363 | "WriteMembers" { "bf9679c0-0de6-11d0-a285-00aa003049e2" } 1364 | Default { "00000000-0000-0000-0000-000000000000"} 1365 | } 1366 | if($_.ObjectType -eq $GuidFilter) { $_ } 1367 | } 1368 | else { 1369 | $_ 1370 | } 1371 | } | ForEach-Object { 1372 | if($GUIDs) { 1373 | # if we're resolving GUIDs, map them them to the resolved hash table 1374 | $AclProperties = @{} 1375 | $_.psobject.properties | ForEach-Object { 1376 | if( ($_.Name -eq 'ObjectType') -or ($_.Name -eq 'InheritedObjectType') ) { 1377 | try { 1378 | $AclProperties[$_.Name] = $GUIDS[$_.Value.toString()] 1379 | } 1380 | catch { 1381 | $AclProperties[$_.Name] = $_.Value 1382 | } 1383 | } 1384 | else { 1385 | $AclProperties[$_.Name] = $_.Value 1386 | } 1387 | } 1388 | New-Object -TypeName PSObject -Property $AclProperties 1389 | } 1390 | else { $_ } 1391 | } 1392 | $Results.dispose() 1393 | $Searcher.dispose() 1394 | } 1395 | catch { 1396 | Write-Warning $_ 1397 | } 1398 | } 1399 | } 1400 | } 1401 | 1402 | 1403 | function Add-ObjectAcl { 1404 | <# 1405 | .SYNOPSIS 1406 | 1407 | Adds an ACL for a specific active directory object. 1408 | 1409 | AdminSDHolder ACL approach from Sean Metcalf (@pyrotek3) 1410 | https://adsecurity.org/?p=1906 1411 | 1412 | ACE setting method adapted from https://social.technet.microsoft.com/Forums/windowsserver/en-US/df3bfd33-c070-4a9c-be98-c4da6e591a0a/forum-faq-using-powershell-to-assign-permissions-on-active-directory-objects. 1413 | 1414 | 'ResetPassword' doesn't need to know the user's current password 1415 | 'WriteMembers' allows for the modification of group membership 1416 | 1417 | .PARAMETER TargetSamAccountName 1418 | 1419 | Target object name to filter for. 1420 | 1421 | .PARAMETER TargetName 1422 | 1423 | Target object name to filter for. 1424 | 1425 | .PARAMETER TargetDistinguishedName 1426 | 1427 | Target object distinguished name to filter for. 1428 | 1429 | .PARAMETER TargetFilter 1430 | 1431 | A customized ldap filter string to use to find a target, e.g. "(description=*admin*)" 1432 | 1433 | .PARAMETER TargetADSpath 1434 | 1435 | The LDAP source for the target, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 1436 | 1437 | .PARAMETER TargetADSprefix 1438 | 1439 | Prefix to set for the target searcher (like "CN=Sites,CN=Configuration") 1440 | 1441 | .PARAMETER PrincipalSID 1442 | 1443 | The SID of the principal object to add for access. 1444 | 1445 | .PARAMETER PrincipalName 1446 | 1447 | The name of the principal object to add for access. 1448 | 1449 | .PARAMETER PrincipalSamAccountName 1450 | 1451 | The samAccountName of the principal object to add for access. 1452 | 1453 | .PARAMETER Rights 1454 | 1455 | Rights to add for the principal, "All","ResetPassword","WriteMembers","DCSync" 1456 | 1457 | .PARAMETER Domain 1458 | 1459 | The domain to use for the target query, defaults to the current domain. 1460 | 1461 | .PARAMETER DomainController 1462 | 1463 | Domain controller to reflect LDAP queries through. 1464 | 1465 | .PARAMETER PageSize 1466 | 1467 | The PageSize to set for the LDAP searcher object. 1468 | 1469 | .EXAMPLE 1470 | 1471 | Add-ObjectAcl -TargetSamAccountName matt -PrincipalSamAccountName john 1472 | 1473 | Grants 'john' all full access rights to the 'matt' account. 1474 | 1475 | .EXAMPLE 1476 | 1477 | Add-ObjectAcl -TargetSamAccountName matt -PrincipalSamAccountName john -Rights ResetPassword 1478 | 1479 | Grants 'john' the right to reset the password for the 'matt' account. 1480 | 1481 | .LINK 1482 | 1483 | https://adsecurity.org/?p=1906 1484 | 1485 | https://social.technet.microsoft.com/Forums/windowsserver/en-US/df3bfd33-c070-4a9c-be98-c4da6e591a0a/forum-faq-using-powershell-to-assign-permissions-on-active-directory-objects?forum=winserverpowershell 1486 | #> 1487 | 1488 | [CmdletBinding()] 1489 | Param ( 1490 | [String] 1491 | $TargetSamAccountName, 1492 | 1493 | [String] 1494 | $TargetName = "*", 1495 | 1496 | [Alias('DN')] 1497 | [String] 1498 | $TargetDistinguishedName = "*", 1499 | 1500 | [String] 1501 | $TargetFilter, 1502 | 1503 | [String] 1504 | $TargetADSpath, 1505 | 1506 | [String] 1507 | $TargetADSprefix, 1508 | 1509 | [String] 1510 | [ValidatePattern('^S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+')] 1511 | $PrincipalSID, 1512 | 1513 | [String] 1514 | $PrincipalName, 1515 | 1516 | [String] 1517 | $PrincipalSamAccountName, 1518 | 1519 | [String] 1520 | [ValidateSet("All","ResetPassword","WriteMembers","DCSync")] 1521 | $Rights = "All", 1522 | 1523 | [String] 1524 | $RightsGUID, 1525 | 1526 | [String] 1527 | $Domain, 1528 | 1529 | [String] 1530 | $DomainController, 1531 | 1532 | [ValidateRange(1,10000)] 1533 | [Int] 1534 | $PageSize = 200 1535 | ) 1536 | 1537 | begin { 1538 | $Searcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $TargetADSpath -ADSprefix $TargetADSprefix -PageSize $PageSize 1539 | 1540 | if(!$PrincipalSID) { 1541 | $Principal = Get-ADObject -Domain $Domain -DomainController $DomainController -Name $PrincipalName -SamAccountName $PrincipalSamAccountName -PageSize $PageSize 1542 | 1543 | if(!$Principal) { 1544 | throw "Error resolving principal" 1545 | } 1546 | $PrincipalSID = $Principal.objectsid 1547 | } 1548 | if(!$PrincipalSID) { 1549 | throw "Error resolving principal" 1550 | } 1551 | } 1552 | 1553 | process { 1554 | 1555 | if ($Searcher) { 1556 | 1557 | if($TargetSamAccountName) { 1558 | $Searcher.filter="(&(samaccountname=$TargetSamAccountName)(name=$TargetName)(distinguishedname=$TargetDistinguishedName)$TargetFilter)" 1559 | } 1560 | else { 1561 | $Searcher.filter="(&(name=$TargetName)(distinguishedname=$TargetDistinguishedName)$TargetFilter)" 1562 | } 1563 | 1564 | try { 1565 | $Searcher.FindAll() | Where-Object {$_} | ForEach-Object { 1566 | # adapted from https://social.technet.microsoft.com/Forums/windowsserver/en-US/df3bfd33-c070-4a9c-be98-c4da6e591a0a/forum-faq-using-powershell-to-assign-permissions-on-active-directory-objects 1567 | 1568 | $TargetDN = $_.Properties.distinguishedname 1569 | 1570 | $Identity = [System.Security.Principal.IdentityReference] ([System.Security.Principal.SecurityIdentifier]$PrincipalSID) 1571 | $InheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance] "None" 1572 | $ControlType = [System.Security.AccessControl.AccessControlType] "Allow" 1573 | $ACEs = @() 1574 | 1575 | if($RightsGUID) { 1576 | $GUIDs = @($RightsGUID) 1577 | } 1578 | else { 1579 | $GUIDs = Switch ($Rights) { 1580 | # ResetPassword doesn't need to know the user's current password 1581 | "ResetPassword" { "00299570-246d-11d0-a768-00aa006e0529" } 1582 | # allows for the modification of group membership 1583 | "WriteMembers" { "bf9679c0-0de6-11d0-a285-00aa003049e2" } 1584 | # 'DS-Replication-Get-Changes' = 1131f6aa-9c07-11d1-f79f-00c04fc2dcd2 1585 | # 'DS-Replication-Get-Changes-All' = 1131f6ad-9c07-11d1-f79f-00c04fc2dcd2 1586 | # 'DS-Replication-Get-Changes-In-Filtered-Set' = 89e95b76-444d-4c62-991a-0facbeda640c 1587 | # when applied to a domain's ACL, allows for the use of DCSync 1588 | "DCSync" { "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2", "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2", "89e95b76-444d-4c62-991a-0facbeda640c"} 1589 | } 1590 | } 1591 | 1592 | if($GUIDs) { 1593 | foreach($GUID in $GUIDs) { 1594 | $NewGUID = New-Object Guid $GUID 1595 | $ADRights = [System.DirectoryServices.ActiveDirectoryRights] "ExtendedRight" 1596 | $ACEs += New-Object System.DirectoryServices.ActiveDirectoryAccessRule $Identity,$ADRights,$ControlType,$NewGUID,$InheritanceType 1597 | } 1598 | } 1599 | else { 1600 | # deault to GenericAll rights 1601 | $ADRights = [System.DirectoryServices.ActiveDirectoryRights] "GenericAll" 1602 | $ACEs += New-Object System.DirectoryServices.ActiveDirectoryAccessRule $Identity,$ADRights,$ControlType,$InheritanceType 1603 | } 1604 | 1605 | Write-Verbose "Granting principal $PrincipalSID '$Rights' on $($_.Properties.distinguishedname)" 1606 | 1607 | try { 1608 | # add all the new ACEs to the specified object 1609 | ForEach ($ACE in $ACEs) { 1610 | Write-Verbose "Granting principal $PrincipalSID '$($ACE.ObjectType)' rights on $($_.Properties.distinguishedname)" 1611 | $Object = [adsi]($_.path) 1612 | $Object.PsBase.ObjectSecurity.AddAccessRule($ACE) 1613 | $Object.PsBase.commitchanges() 1614 | } 1615 | } 1616 | catch { 1617 | Write-Warning "Error granting principal $PrincipalSID '$Rights' on $TargetDN : $_" 1618 | } 1619 | } 1620 | } 1621 | catch { 1622 | Write-Warning "Error: $_" 1623 | } 1624 | } 1625 | } 1626 | } 1627 | 1628 | 1629 | filter Get-GUIDMap { 1630 | <# 1631 | .SYNOPSIS 1632 | 1633 | Helper to build a hash table of [GUID] -> resolved names 1634 | 1635 | Heavily adapted from http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx 1636 | 1637 | .PARAMETER Domain 1638 | 1639 | The domain to use for the query, defaults to the current domain. 1640 | 1641 | .PARAMETER DomainController 1642 | 1643 | Domain controller to reflect LDAP queries through. 1644 | 1645 | .PARAMETER PageSize 1646 | 1647 | The PageSize to set for the LDAP searcher object. 1648 | 1649 | .LINK 1650 | 1651 | http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx 1652 | #> 1653 | 1654 | [CmdletBinding()] 1655 | Param ( 1656 | [Parameter(ValueFromPipeline=$True)] 1657 | [String] 1658 | $Domain, 1659 | 1660 | [String] 1661 | $DomainController, 1662 | 1663 | [ValidateRange(1,10000)] 1664 | [Int] 1665 | $PageSize = 200, 1666 | 1667 | [Management.Automation.PSCredential] 1668 | $Credential 1669 | ) 1670 | 1671 | $GUIDs = @{'00000000-0000-0000-0000-000000000000' = 'All'} 1672 | 1673 | $SchemaPath = (Get-NetForest -Credential $Credential).schema.name 1674 | 1675 | $SchemaSearcher = Get-DomainSearcher -ADSpath $SchemaPath -Domain $Domain -DomainController $DomainController -PageSize $PageSize -Credential $Credential 1676 | if($SchemaSearcher) { 1677 | $SchemaSearcher.filter = "(schemaIDGUID=*)" 1678 | try { 1679 | $SchemaSearcher.FindAll() | Where-Object {$_} | ForEach-Object { 1680 | # convert the GUID 1681 | $GUIDs[(New-Object Guid (,$_.properties.schemaidguid[0])).Guid] = $_.properties.name[0] 1682 | } 1683 | } 1684 | catch { 1685 | Write-Debug "Error in building GUID map: $_" 1686 | } 1687 | } 1688 | 1689 | $RightsSearcher = Get-DomainSearcher -ADSpath $SchemaPath.replace("Schema","Extended-Rights") -Domain $Domain -DomainController $DomainController -PageSize $PageSize -Credential $Credential 1690 | if ($RightsSearcher) { 1691 | $RightsSearcher.filter = "(objectClass=controlAccessRight)" 1692 | try { 1693 | $RightsSearcher.FindAll() | Where-Object {$_} | ForEach-Object { 1694 | # convert the GUID 1695 | $GUIDs[$_.properties.rightsguid[0].toString()] = $_.properties.name[0] 1696 | } 1697 | } 1698 | catch { 1699 | Write-Debug "Error in building GUID map: $_" 1700 | } 1701 | } 1702 | 1703 | $GUIDs 1704 | } 1705 | 1706 | 1707 | function Get-NetComputer { 1708 | <# 1709 | .SYNOPSIS 1710 | 1711 | This function utilizes adsisearcher to query the current AD context 1712 | for current computer objects. Based off of Carlos Perez's Audit.psm1 1713 | script in Posh-SecMod (link below). 1714 | 1715 | .PARAMETER ComputerName 1716 | 1717 | Return computers with a specific name, wildcards accepted. 1718 | 1719 | .PARAMETER SPN 1720 | 1721 | Return computers with a specific service principal name, wildcards accepted. 1722 | 1723 | .PARAMETER OperatingSystem 1724 | 1725 | Return computers with a specific operating system, wildcards accepted. 1726 | 1727 | .PARAMETER ServicePack 1728 | 1729 | Return computers with a specific service pack, wildcards accepted. 1730 | 1731 | .PARAMETER Filter 1732 | 1733 | A customized ldap filter string to use, e.g. "(description=*admin*)" 1734 | 1735 | .PARAMETER Printers 1736 | 1737 | Switch. Return only printers. 1738 | 1739 | .PARAMETER Ping 1740 | 1741 | Switch. Ping each host to ensure it's up before enumerating. 1742 | 1743 | .PARAMETER FullData 1744 | 1745 | Switch. Return full computer objects instead of just system names (the default). 1746 | 1747 | .PARAMETER Domain 1748 | 1749 | The domain to query for computers, defaults to the current domain. 1750 | 1751 | .PARAMETER DomainController 1752 | 1753 | Domain controller to reflect LDAP queries through. 1754 | 1755 | .PARAMETER ADSpath 1756 | 1757 | The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 1758 | Useful for OU queries. 1759 | 1760 | .PARAMETER SiteName 1761 | 1762 | The AD Site name to search for computers. 1763 | 1764 | .PARAMETER Unconstrained 1765 | 1766 | Switch. Return computer objects that have unconstrained delegation. 1767 | 1768 | .PARAMETER PageSize 1769 | 1770 | The PageSize to set for the LDAP searcher object. 1771 | 1772 | .PARAMETER Credential 1773 | 1774 | A [Management.Automation.PSCredential] object of alternate credentials 1775 | for connection to the target domain. 1776 | 1777 | .EXAMPLE 1778 | 1779 | PS C:\> Get-NetComputer 1780 | 1781 | Returns the current computers in current domain. 1782 | 1783 | .EXAMPLE 1784 | 1785 | PS C:\> Get-NetComputer -SPN mssql* 1786 | 1787 | Returns all MS SQL servers on the domain. 1788 | 1789 | .EXAMPLE 1790 | 1791 | PS C:\> Get-NetComputer -Domain testing 1792 | 1793 | Returns the current computers in 'testing' domain. 1794 | 1795 | .EXAMPLE 1796 | 1797 | PS C:\> Get-NetComputer -Domain testing -FullData 1798 | 1799 | Returns full computer objects in the 'testing' domain. 1800 | 1801 | .LINK 1802 | 1803 | https://github.com/darkoperator/Posh-SecMod/blob/master/Audit/Audit.psm1 1804 | #> 1805 | 1806 | [CmdletBinding()] 1807 | Param ( 1808 | [Parameter(ValueFromPipeline=$True)] 1809 | [Alias('HostName')] 1810 | [String] 1811 | $ComputerName = '*', 1812 | 1813 | [String] 1814 | $SPN, 1815 | 1816 | [String] 1817 | $OperatingSystem, 1818 | 1819 | [String] 1820 | $ServicePack, 1821 | 1822 | [String] 1823 | $Filter, 1824 | 1825 | [Switch] 1826 | $Printers, 1827 | 1828 | [Switch] 1829 | $Ping, 1830 | 1831 | [Switch] 1832 | $FullData, 1833 | 1834 | [String] 1835 | $Domain, 1836 | 1837 | [String] 1838 | $DomainController, 1839 | 1840 | [String] 1841 | $ADSpath, 1842 | 1843 | [String] 1844 | $SiteName, 1845 | 1846 | [Switch] 1847 | $Unconstrained, 1848 | 1849 | [ValidateRange(1,10000)] 1850 | [Int] 1851 | $PageSize = 200, 1852 | 1853 | [Management.Automation.PSCredential] 1854 | $Credential 1855 | ) 1856 | 1857 | begin { 1858 | # so this isn't repeated if multiple computer names are passed on the pipeline 1859 | $CompSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize -Credential $Credential 1860 | } 1861 | 1862 | process { 1863 | 1864 | if ($CompSearcher) { 1865 | 1866 | # if we're checking for unconstrained delegation 1867 | if($Unconstrained) { 1868 | Write-Verbose "Searching for computers with for unconstrained delegation" 1869 | $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=524288)" 1870 | } 1871 | # set the filters for the seracher if it exists 1872 | if($Printers) { 1873 | Write-Verbose "Searching for printers" 1874 | # $CompSearcher.filter="(&(objectCategory=printQueue)$Filter)" 1875 | $Filter += "(objectCategory=printQueue)" 1876 | } 1877 | if($SPN) { 1878 | Write-Verbose "Searching for computers with SPN: $SPN" 1879 | $Filter += "(servicePrincipalName=$SPN)" 1880 | } 1881 | if($OperatingSystem) { 1882 | $Filter += "(operatingsystem=$OperatingSystem)" 1883 | } 1884 | if($ServicePack) { 1885 | $Filter += "(operatingsystemservicepack=$ServicePack)" 1886 | } 1887 | if($SiteName) { 1888 | $Filter += "(serverreferencebl=$SiteName)" 1889 | } 1890 | 1891 | $CompFilter = "(&(sAMAccountType=805306369)(dnshostname=$ComputerName)$Filter)" 1892 | Write-Verbose "Get-NetComputer filter : '$CompFilter'" 1893 | $CompSearcher.filter = $CompFilter 1894 | 1895 | try { 1896 | 1897 | $CompSearcher.FindAll() | Where-Object {$_} | ForEach-Object { 1898 | $Up = $True 1899 | if($Ping) { 1900 | # TODO: how can these results be piped to ping for a speedup? 1901 | $Up = Test-Connection -Count 1 -Quiet -ComputerName $_.properties.dnshostname 1902 | } 1903 | if($Up) { 1904 | # return full data objects 1905 | if ($FullData) { 1906 | # convert/process the LDAP fields for each result 1907 | Convert-LDAPProperty -Properties $_.Properties 1908 | } 1909 | else { 1910 | # otherwise we're just returning the DNS host name 1911 | $_.properties.dnshostname 1912 | } 1913 | } 1914 | } 1915 | } 1916 | catch { 1917 | Write-Warning "Error: $_" 1918 | } 1919 | } 1920 | } 1921 | } 1922 | 1923 | 1924 | function Get-NetOU { 1925 | <# 1926 | .SYNOPSIS 1927 | 1928 | Gets a list of all current OUs in a domain. 1929 | 1930 | .PARAMETER OUName 1931 | 1932 | The OU name to query for, wildcards accepted. 1933 | 1934 | .PARAMETER GUID 1935 | 1936 | Only return OUs with the specified GUID in their gplink property. 1937 | 1938 | .PARAMETER Domain 1939 | 1940 | The domain to query for OUs, defaults to the current domain. 1941 | 1942 | .PARAMETER DomainController 1943 | 1944 | Domain controller to reflect LDAP queries through. 1945 | 1946 | .PARAMETER ADSpath 1947 | 1948 | The LDAP source to search through. 1949 | 1950 | .PARAMETER FullData 1951 | 1952 | Switch. Return full OU objects instead of just object names (the default). 1953 | 1954 | .PARAMETER PageSize 1955 | 1956 | The PageSize to set for the LDAP searcher object. 1957 | 1958 | .PARAMETER Credential 1959 | 1960 | A [Management.Automation.PSCredential] object of alternate credentials 1961 | for connection to the target domain. 1962 | 1963 | .EXAMPLE 1964 | 1965 | PS C:\> Get-NetOU 1966 | 1967 | Returns the current OUs in the domain. 1968 | 1969 | .EXAMPLE 1970 | 1971 | PS C:\> Get-NetOU -OUName *admin* -Domain testlab.local 1972 | 1973 | Returns all OUs with "admin" in their name in the testlab.local domain. 1974 | 1975 | .EXAMPLE 1976 | 1977 | PS C:\> Get-NetOU -GUID 123-... 1978 | 1979 | Returns all OUs with linked to the specified group policy object. 1980 | 1981 | .EXAMPLE 1982 | 1983 | PS C:\> "*admin*","*server*" | Get-NetOU 1984 | 1985 | Get the full OU names for the given search terms piped on the pipeline. 1986 | #> 1987 | 1988 | [CmdletBinding()] 1989 | Param ( 1990 | [Parameter(ValueFromPipeline=$True)] 1991 | [String] 1992 | $OUName = '*', 1993 | 1994 | [String] 1995 | $GUID, 1996 | 1997 | [String] 1998 | $Domain, 1999 | 2000 | [String] 2001 | $DomainController, 2002 | 2003 | [String] 2004 | $ADSpath, 2005 | 2006 | [Switch] 2007 | $FullData, 2008 | 2009 | [ValidateRange(1,10000)] 2010 | [Int] 2011 | $PageSize = 200, 2012 | 2013 | [Management.Automation.PSCredential] 2014 | $Credential 2015 | ) 2016 | 2017 | begin { 2018 | $OUSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize 2019 | } 2020 | process { 2021 | if ($OUSearcher) { 2022 | if ($GUID) { 2023 | # if we're filtering for a GUID in .gplink 2024 | $OUSearcher.filter="(&(objectCategory=organizationalUnit)(name=$OUName)(gplink=*$GUID*))" 2025 | } 2026 | else { 2027 | $OUSearcher.filter="(&(objectCategory=organizationalUnit)(name=$OUName))" 2028 | } 2029 | 2030 | try { 2031 | $OUSearcher.FindAll() | Where-Object {$_} | ForEach-Object { 2032 | if ($FullData) { 2033 | # convert/process the LDAP fields for each result 2034 | Convert-LDAPProperty -Properties $_.Properties 2035 | } 2036 | else { 2037 | # otherwise just returning the ADS paths of the OUs 2038 | $_.properties.adspath 2039 | } 2040 | } 2041 | } 2042 | catch { 2043 | Write-Warning $_ 2044 | } 2045 | } 2046 | } 2047 | } 2048 | 2049 | 2050 | function Get-NetGroup { 2051 | <# 2052 | .SYNOPSIS 2053 | 2054 | Gets a list of all current groups in a domain, or all 2055 | the groups a given user/group object belongs to. 2056 | 2057 | .PARAMETER GroupName 2058 | 2059 | The group name to query for, wildcards accepted. 2060 | 2061 | .PARAMETER SID 2062 | 2063 | The group SID to query for. 2064 | 2065 | .PARAMETER UserName 2066 | 2067 | The user name (or group name) to query for all effective 2068 | groups of. 2069 | 2070 | .PARAMETER Filter 2071 | 2072 | A customized ldap filter string to use, e.g. "(description=*admin*)" 2073 | 2074 | .PARAMETER Domain 2075 | 2076 | The domain to query for groups, defaults to the current domain. 2077 | 2078 | .PARAMETER DomainController 2079 | 2080 | Domain controller to reflect LDAP queries through. 2081 | 2082 | .PARAMETER ADSpath 2083 | 2084 | The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 2085 | Useful for OU queries. 2086 | 2087 | .PARAMETER AdminCount 2088 | 2089 | Switch. Return group with adminCount=1. 2090 | 2091 | .PARAMETER FullData 2092 | 2093 | Switch. Return full group objects instead of just object names (the default). 2094 | 2095 | .PARAMETER RawSids 2096 | 2097 | Switch. Return raw SIDs when using "Get-NetGroup -UserName X" 2098 | 2099 | .PARAMETER PageSize 2100 | 2101 | The PageSize to set for the LDAP searcher object. 2102 | 2103 | .PARAMETER Credential 2104 | 2105 | A [Management.Automation.PSCredential] object of alternate credentials 2106 | for connection to the target domain. 2107 | 2108 | .EXAMPLE 2109 | 2110 | PS C:\> Get-NetGroup 2111 | 2112 | Returns the current groups in the domain. 2113 | 2114 | .EXAMPLE 2115 | 2116 | PS C:\> Get-NetGroup -GroupName *admin* 2117 | 2118 | Returns all groups with "admin" in their group name. 2119 | 2120 | .EXAMPLE 2121 | 2122 | PS C:\> Get-NetGroup -Domain testing -FullData 2123 | 2124 | Returns full group data objects in the 'testing' domain 2125 | #> 2126 | 2127 | [CmdletBinding()] 2128 | param( 2129 | [Parameter(ValueFromPipeline=$True)] 2130 | [String] 2131 | $GroupName = '*', 2132 | 2133 | [String] 2134 | $SID, 2135 | 2136 | [String] 2137 | $UserName, 2138 | 2139 | [String] 2140 | $Filter, 2141 | 2142 | [String] 2143 | $Domain, 2144 | 2145 | [String] 2146 | $DomainController, 2147 | 2148 | [String] 2149 | $ADSpath, 2150 | 2151 | [Switch] 2152 | $AdminCount, 2153 | 2154 | [Switch] 2155 | $FullData, 2156 | 2157 | [Switch] 2158 | $RawSids, 2159 | 2160 | [ValidateRange(1,10000)] 2161 | [Int] 2162 | $PageSize = 200, 2163 | 2164 | [Management.Automation.PSCredential] 2165 | $Credential 2166 | ) 2167 | 2168 | begin { 2169 | $GroupSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize 2170 | } 2171 | 2172 | process { 2173 | if($GroupSearcher) { 2174 | 2175 | if($AdminCount) { 2176 | Write-Verbose "Checking for adminCount=1" 2177 | $Filter += "(admincount=1)" 2178 | } 2179 | 2180 | if ($UserName) { 2181 | # get the raw user object 2182 | $User = Get-ADObject -SamAccountName $UserName -Domain $Domain -DomainController $DomainController -Credential $Credential -ReturnRaw -PageSize $PageSize 2183 | 2184 | # convert the user to a directory entry 2185 | $UserDirectoryEntry = $User.GetDirectoryEntry() 2186 | 2187 | # cause the cache to calculate the token groups for the user 2188 | $UserDirectoryEntry.RefreshCache("tokenGroups") 2189 | 2190 | $UserDirectoryEntry.TokenGroups | ForEach-Object { 2191 | # convert the token group sid 2192 | $GroupSid = (New-Object System.Security.Principal.SecurityIdentifier($_,0)).Value 2193 | 2194 | # ignore the built in users and default domain user group 2195 | if(!($GroupSid -match '^S-1-5-32-545|-513$')) { 2196 | if($FullData) { 2197 | Get-ADObject -SID $GroupSid -PageSize $PageSize -Domain $Domain -DomainController $DomainController -Credential $Credential 2198 | } 2199 | else { 2200 | if($RawSids) { 2201 | $GroupSid 2202 | } 2203 | else { 2204 | Convert-SidToName $GroupSid 2205 | } 2206 | } 2207 | } 2208 | } 2209 | } 2210 | else { 2211 | if ($SID) { 2212 | $GroupSearcher.filter = "(&(objectCategory=group)(objectSID=$SID)$Filter)" 2213 | } 2214 | else { 2215 | $GroupSearcher.filter = "(&(objectCategory=group)(name=$GroupName)$Filter)" 2216 | } 2217 | 2218 | $GroupSearcher.FindAll() | Where-Object {$_} | ForEach-Object { 2219 | # if we're returning full data objects 2220 | if ($FullData) { 2221 | # convert/process the LDAP fields for each result 2222 | Convert-LDAPProperty -Properties $_.Properties 2223 | } 2224 | else { 2225 | # otherwise we're just returning the group name 2226 | $_.properties.samaccountname 2227 | } 2228 | } 2229 | } 2230 | } 2231 | } 2232 | } 2233 | 2234 | 2235 | function Get-NetGroupMember { 2236 | <# 2237 | .SYNOPSIS 2238 | 2239 | This function users [ADSI] and LDAP to query the current AD context 2240 | or trusted domain for users in a specified group. If no GroupName is 2241 | specified, it defaults to querying the "Domain Admins" group. 2242 | This is a replacement for "net group 'name' /domain" 2243 | 2244 | .PARAMETER GroupName 2245 | 2246 | The group name to query for users. 2247 | 2248 | .PARAMETER SID 2249 | 2250 | The Group SID to query for users. If not given, it defaults to 512 "Domain Admins" 2251 | 2252 | .PARAMETER Filter 2253 | 2254 | A customized ldap filter string to use, e.g. "(description=*admin*)" 2255 | 2256 | .PARAMETER Domain 2257 | 2258 | The domain to query for group users, defaults to the current domain. 2259 | 2260 | .PARAMETER DomainController 2261 | 2262 | Domain controller to reflect LDAP queries through. 2263 | 2264 | .PARAMETER ADSpath 2265 | 2266 | The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 2267 | Useful for OU queries. 2268 | 2269 | .PARAMETER FullData 2270 | 2271 | Switch. Returns full data objects instead of just group/users. 2272 | 2273 | .PARAMETER Recurse 2274 | 2275 | Switch. If the group member is a group, recursively try to query its members as well. 2276 | 2277 | .PARAMETER UseMatchingRule 2278 | 2279 | Switch. Use LDAP_MATCHING_RULE_IN_CHAIN in the LDAP search query when -Recurse is specified. 2280 | Much faster than manual recursion, but doesn't reveal cross-domain groups. 2281 | 2282 | .PARAMETER PageSize 2283 | 2284 | The PageSize to set for the LDAP searcher object. 2285 | 2286 | .PARAMETER Credential 2287 | 2288 | A [Management.Automation.PSCredential] object of alternate credentials 2289 | for connection to the target domain. 2290 | 2291 | .EXAMPLE 2292 | 2293 | PS C:\> Get-NetGroupMember 2294 | 2295 | Returns the usernames that of members of the "Domain Admins" domain group. 2296 | 2297 | .EXAMPLE 2298 | 2299 | PS C:\> Get-NetGroupMember -Domain testing -GroupName "Power Users" 2300 | 2301 | Returns the usernames that of members of the "Power Users" group in the 'testing' domain. 2302 | 2303 | .LINK 2304 | 2305 | http://www.powershellmagazine.com/2013/05/23/pstip-retrieve-group-membership-of-an-active-directory-group-recursively/ 2306 | #> 2307 | 2308 | [CmdletBinding()] 2309 | param( 2310 | [Parameter(ValueFromPipeline=$True)] 2311 | [String] 2312 | $GroupName, 2313 | 2314 | [String] 2315 | $SID, 2316 | 2317 | [String] 2318 | $Domain, 2319 | 2320 | [String] 2321 | $DomainController, 2322 | 2323 | [String] 2324 | $ADSpath, 2325 | 2326 | [Switch] 2327 | $FullData, 2328 | 2329 | [Switch] 2330 | $Recurse, 2331 | 2332 | [Switch] 2333 | $UseMatchingRule, 2334 | 2335 | [ValidateRange(1,10000)] 2336 | [Int] 2337 | $PageSize = 200, 2338 | 2339 | [Management.Automation.PSCredential] 2340 | $Credential 2341 | ) 2342 | 2343 | begin { 2344 | # so this isn't repeated if users are passed on the pipeline 2345 | $GroupSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -Credential $Credential -ADSpath $ADSpath -PageSize $PageSize 2346 | 2347 | if(!$DomainController) { 2348 | $DomainController = ((Get-NetDomain -Credential $Credential).PdcRoleOwner).Name 2349 | } 2350 | 2351 | if(!$Domain) { 2352 | $Domain = Get-NetDomain -Credential $Credential 2353 | } 2354 | } 2355 | 2356 | process { 2357 | 2358 | if ($GroupSearcher) { 2359 | 2360 | if ($Recurse -and $UseMatchingRule) { 2361 | # resolve the group to a distinguishedname 2362 | if ($GroupName) { 2363 | $Group = Get-NetGroup -GroupName $GroupName -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -PageSize $PageSize 2364 | } 2365 | elseif ($SID) { 2366 | $Group = Get-NetGroup -SID $SID -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -PageSize $PageSize 2367 | } 2368 | else { 2369 | # default to domain admins 2370 | $SID = (Get-DomainSID -Domain $Domain -Credential $Credential) + "-512" 2371 | $Group = Get-NetGroup -SID $SID -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData -PageSize $PageSize 2372 | } 2373 | $GroupDN = $Group.distinguishedname 2374 | $GroupFoundName = $Group.name 2375 | 2376 | if ($GroupDN) { 2377 | $GroupSearcher.filter = "(&(samAccountType=805306368)(memberof:1.2.840.113556.1.4.1941:=$GroupDN)$Filter)" 2378 | $GroupSearcher.PropertiesToLoad.AddRange(('distinguishedName','samaccounttype','lastlogon','lastlogontimestamp','dscorepropagationdata','objectsid','whencreated','badpasswordtime','accountexpires','iscriticalsystemobject','name','usnchanged','objectcategory','description','codepage','instancetype','countrycode','distinguishedname','cn','admincount','logonhours','objectclass','logoncount','usncreated','useraccountcontrol','objectguid','primarygroupid','lastlogoff','samaccountname','badpwdcount','whenchanged','memberof','pwdlastset','adspath')) 2379 | 2380 | $Members = $GroupSearcher.FindAll() 2381 | $GroupFoundName = $GroupName 2382 | } 2383 | else { 2384 | Write-Error "Unable to find Group" 2385 | } 2386 | } 2387 | else { 2388 | if ($GroupName) { 2389 | $GroupSearcher.filter = "(&(objectCategory=group)(name=$GroupName)$Filter)" 2390 | } 2391 | elseif ($SID) { 2392 | $GroupSearcher.filter = "(&(objectCategory=group)(objectSID=$SID)$Filter)" 2393 | } 2394 | else { 2395 | # default to domain admins 2396 | $SID = (Get-DomainSID -Domain $Domain -Credential $Credential) + "-512" 2397 | $GroupSearcher.filter = "(&(objectCategory=group)(objectSID=$SID)$Filter)" 2398 | } 2399 | 2400 | $GroupSearcher.FindAll() | ForEach-Object { 2401 | try { 2402 | if (!($_) -or !($_.properties) -or !($_.properties.name)) { continue } 2403 | 2404 | $GroupFoundName = $_.properties.name[0] 2405 | $Members = @() 2406 | 2407 | if ($_.properties.member.Count -eq 0) { 2408 | $Finished = $False 2409 | $Bottom = 0 2410 | $Top = 0 2411 | while(!$Finished) { 2412 | $Top = $Bottom + 1499 2413 | $MemberRange="member;range=$Bottom-$Top" 2414 | $Bottom += 1500 2415 | $GroupSearcher.PropertiesToLoad.Clear() 2416 | [void]$GroupSearcher.PropertiesToLoad.Add("$MemberRange") 2417 | try { 2418 | $Result = $GroupSearcher.FindOne() 2419 | if ($Result) { 2420 | $RangedProperty = $_.Properties.PropertyNames -like "member;range=*" 2421 | $Results = $_.Properties.item($RangedProperty) 2422 | if ($Results.count -eq 0) { 2423 | $Finished = $True 2424 | } 2425 | else { 2426 | $Results | ForEach-Object { 2427 | $Members += $_ 2428 | } 2429 | } 2430 | } 2431 | else { 2432 | $Finished = $True 2433 | } 2434 | } 2435 | catch [System.Management.Automation.MethodInvocationException] { 2436 | $Finished = $True 2437 | } 2438 | } 2439 | } 2440 | else { 2441 | $Members = $_.properties.member 2442 | } 2443 | } 2444 | catch { 2445 | Write-Verbose $_ 2446 | } 2447 | } 2448 | } 2449 | 2450 | $Members | Where-Object {$_} | ForEach-Object { 2451 | # if we're doing the LDAP_MATCHING_RULE_IN_CHAIN recursion 2452 | if ($Recurse -and $UseMatchingRule) { 2453 | $Properties = $_.Properties 2454 | } 2455 | else { 2456 | if($DomainController) { 2457 | $Result = [adsi]"LDAP://$DomainController/$_" 2458 | } 2459 | else { 2460 | $Result = [adsi]"LDAP://$_" 2461 | } 2462 | if($Result){ 2463 | $Properties = $Result.Properties 2464 | } 2465 | } 2466 | 2467 | if($Properties) { 2468 | 2469 | $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Properties.samaccounttype 2470 | 2471 | if ($FullData) { 2472 | $GroupMember = Convert-LDAPProperty -Properties $Properties 2473 | } 2474 | else { 2475 | $GroupMember = New-Object PSObject 2476 | } 2477 | 2478 | $GroupMember | Add-Member Noteproperty 'GroupDomain' $Domain 2479 | $GroupMember | Add-Member Noteproperty 'GroupName' $GroupFoundName 2480 | 2481 | try { 2482 | $MemberDN = $Properties.distinguishedname[0] 2483 | 2484 | # extract the FQDN from the Distinguished Name 2485 | $MemberDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' 2486 | } 2487 | catch { 2488 | $MemberDN = $Null 2489 | $MemberDomain = $Null 2490 | } 2491 | 2492 | if ($Properties.samaccountname) { 2493 | # forest users have the samAccountName set 2494 | $MemberName = $Properties.samaccountname[0] 2495 | } 2496 | else { 2497 | # external trust users have a SID, so convert it 2498 | try { 2499 | $MemberName = Convert-SidToName $Properties.cn[0] 2500 | } 2501 | catch { 2502 | # if there's a problem contacting the domain to resolve the SID 2503 | $MemberName = $Properties.cn 2504 | } 2505 | } 2506 | 2507 | if($Properties.objectSid) { 2508 | $MemberSid = ((New-Object System.Security.Principal.SecurityIdentifier $Properties.objectSid[0],0).Value) 2509 | } 2510 | else { 2511 | $MemberSid = $Null 2512 | } 2513 | 2514 | $GroupMember | Add-Member Noteproperty 'MemberDomain' $MemberDomain 2515 | $GroupMember | Add-Member Noteproperty 'MemberName' $MemberName 2516 | $GroupMember | Add-Member Noteproperty 'MemberSid' $MemberSid 2517 | $GroupMember | Add-Member Noteproperty 'IsGroup' $IsGroup 2518 | $GroupMember | Add-Member Noteproperty 'MemberDN' $MemberDN 2519 | $GroupMember 2520 | 2521 | # if we're doing manual recursion 2522 | if ($Recurse -and !$UseMatchingRule -and $IsGroup -and $MemberName) { 2523 | if($FullData) { 2524 | Get-NetGroupMember -FullData -Domain $MemberDomain -DomainController $DomainController -Credential $Credential -GroupName $MemberName -Recurse -PageSize $PageSize 2525 | } 2526 | else { 2527 | Get-NetGroupMember -Domain $MemberDomain -DomainController $DomainController -Credential $Credential -GroupName $MemberName -Recurse -PageSize $PageSize 2528 | } 2529 | } 2530 | } 2531 | 2532 | } 2533 | } 2534 | } 2535 | } 2536 | 2537 | function Get-NetUser { 2538 | <# 2539 | .SYNOPSIS 2540 | 2541 | Query information for a given user or users in the domain 2542 | using ADSI and LDAP. Another -Domain can be specified to 2543 | query for users across a trust. 2544 | Replacement for "net users /domain" 2545 | 2546 | .PARAMETER UserName 2547 | 2548 | Username filter string, wildcards accepted. 2549 | 2550 | .PARAMETER Domain 2551 | 2552 | The domain to query for users, defaults to the current domain. 2553 | 2554 | .PARAMETER DomainController 2555 | 2556 | Domain controller to reflect LDAP queries through. 2557 | 2558 | .PARAMETER ADSpath 2559 | 2560 | The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 2561 | Useful for OU queries. 2562 | 2563 | .PARAMETER Filter 2564 | 2565 | A customized ldap filter string to use, e.g. "(description=*admin*)" 2566 | 2567 | .PARAMETER AdminCount 2568 | 2569 | Switch. Return users with adminCount=1. 2570 | 2571 | .PARAMETER SPN 2572 | 2573 | Switch. Only return user objects with non-null service principal names. 2574 | 2575 | .PARAMETER Unconstrained 2576 | 2577 | Switch. Return users that have unconstrained delegation. 2578 | 2579 | .PARAMETER AllowDelegation 2580 | 2581 | Switch. Return user accounts that are not marked as 'sensitive and not allowed for delegation' 2582 | 2583 | .PARAMETER PageSize 2584 | 2585 | The PageSize to set for the LDAP searcher object. 2586 | 2587 | .PARAMETER Credential 2588 | 2589 | A [Management.Automation.PSCredential] object of alternate credentials 2590 | for connection to the target domain. 2591 | 2592 | .EXAMPLE 2593 | 2594 | PS C:\> Get-NetUser -Domain testing 2595 | 2596 | .EXAMPLE 2597 | 2598 | PS C:\> Get-NetUser -ADSpath "LDAP://OU=secret,DC=testlab,DC=local" 2599 | #> 2600 | 2601 | param( 2602 | [Parameter(Position=0, ValueFromPipeline=$True)] 2603 | [String] 2604 | $UserName, 2605 | 2606 | [String] 2607 | $Domain, 2608 | 2609 | [String] 2610 | $DomainController, 2611 | 2612 | [String] 2613 | $ADSpath, 2614 | 2615 | [String] 2616 | $Filter, 2617 | 2618 | [Switch] 2619 | $SPN, 2620 | 2621 | [Switch] 2622 | $AdminCount, 2623 | 2624 | [Switch] 2625 | $Unconstrained, 2626 | 2627 | [Switch] 2628 | $AllowDelegation, 2629 | 2630 | [ValidateRange(1,10000)] 2631 | [Int] 2632 | $PageSize = 200, 2633 | 2634 | [Management.Automation.PSCredential] 2635 | $Credential 2636 | ) 2637 | 2638 | begin { 2639 | # so this isn't repeated if users are passed on the pipeline 2640 | $UserSearcher = Get-DomainSearcher -Domain $Domain -ADSpath $ADSpath -DomainController $DomainController -PageSize $PageSize -Credential $Credential 2641 | } 2642 | 2643 | process { 2644 | if($UserSearcher) { 2645 | 2646 | # if we're checking for unconstrained delegation 2647 | if($Unconstrained) { 2648 | Write-Verbose "Checking for unconstrained delegation" 2649 | $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=524288)" 2650 | } 2651 | if($AllowDelegation) { 2652 | Write-Verbose "Checking for users who can be delegated" 2653 | # negation of "Accounts that are sensitive and not trusted for delegation" 2654 | $Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=1048574))" 2655 | } 2656 | if($AdminCount) { 2657 | Write-Verbose "Checking for adminCount=1" 2658 | $Filter += "(admincount=1)" 2659 | } 2660 | 2661 | # check if we're using a username filter or not 2662 | if($UserName) { 2663 | # samAccountType=805306368 indicates user objects 2664 | $UserSearcher.filter="(&(samAccountType=805306368)(samAccountName=$UserName)$Filter)" 2665 | } 2666 | elseif($SPN) { 2667 | $UserSearcher.filter="(&(samAccountType=805306368)(servicePrincipalName=*)$Filter)" 2668 | } 2669 | else { 2670 | # filter is something like "(samAccountName=*blah*)" if specified 2671 | $UserSearcher.filter="(&(samAccountType=805306368)$Filter)" 2672 | } 2673 | 2674 | $Results = $UserSearcher.FindAll() 2675 | $Results | Where-Object {$_} | ForEach-Object { 2676 | # convert/process the LDAP fields for each result 2677 | $User = Convert-LDAPProperty -Properties $_.Properties 2678 | $User.PSObject.TypeNames.Add('PowerView.User') 2679 | $User 2680 | } 2681 | $Results.dispose() 2682 | $UserSearcher.dispose() 2683 | } 2684 | } 2685 | } 2686 | 2687 | 2688 | 2689 | 2690 | ######################################################## 2691 | # 2692 | # LAPS functions below. 2693 | # 2694 | ######################################################## 2695 | 2696 | function Find-AdmPwdExtendedRights { 2697 | <# 2698 | .SYNOPSIS 2699 | 2700 | This function leverages Get-ObjectAcl to query the domain 2701 | for all computer objects with LAPS enabled, then parses each 2702 | ExtendedRight ACL assignment to determine which users can read 2703 | the ms-Mcs-AdmPwd attribute and if it was specifically delegated 2704 | by the system administrator. 2705 | 2706 | Credits to @harmj0y and @pyrotek3 for research 2707 | 2708 | Author: @leoloobeek 2709 | 2710 | .PARAMETER Domain 2711 | 2712 | The domain to use for the query, defaults to the current domain. 2713 | 2714 | .PARAMETER DomainController 2715 | 2716 | Domain controller to reflect LDAP queries through. 2717 | 2718 | .PARAMETER Filter 2719 | 2720 | Filter to apply to LDAP queries, defaults to all computers with LAPS enabled. 2721 | 2722 | .PARAMETER ExcludeDelegated 2723 | 2724 | Switch. Only show users with "All Extended Rights". Default False. 2725 | 2726 | .PARAMETER PageSize 2727 | 2728 | The PageSize to set for the LDAP searcher object. 2729 | 2730 | .PARAMETER Credential 2731 | 2732 | A [Management.Automation.PSCredential] object of alternate credentials 2733 | for connection to the target domain. 2734 | 2735 | NOTE: You must use FQDN of domain: testlab.local\user, not testlab\user 2736 | 2737 | .EXAMPLE 2738 | 2739 | PS C:\> Find-AdmPwdExtendedRights 2740 | 2741 | Description 2742 | ----------- 2743 | Get users who can read the ms-Mcs-AdmPwd confidential attribute for all LAPS enabled computers 2744 | 2745 | Reason: Delegated 2746 | System admin delegated this group to view the password 2747 | Reason: All 2748 | This user/group has all extended rights permission and can view the password 2749 | 2750 | .EXAMPLE 2751 | 2752 | PS C:\> Find-AdmPwdExtendedRights -ComputerName victim1.testlab.local 2753 | 2754 | Description 2755 | ----------- 2756 | Only retrieves ExtendedRights for the computer specified 2757 | 2758 | .EXAMPLE 2759 | 2760 | PS C:\> Find-AdmPwdExtendedRights -Credential testlab.local\user -Domain testlab.local -DomainController 192.168.1.1 2761 | 2762 | Description 2763 | ----------- 2764 | Retrieve LAPS ACL information from computer not connected to testlab.local domain. 2765 | 2766 | .LINK 2767 | 2768 | https://adsecurity.org/?p=1790 2769 | https://blogs.msdn.microsoft.com/laps/2015/07/17/laps-and-permission-to-join-computer-to-domain/ 2770 | http://www.harmj0y.net/blog/redteaming/abusing-gpo-permissions/ 2771 | #> 2772 | [CmdletBinding()] 2773 | param( 2774 | 2775 | [String] 2776 | $Domain, 2777 | 2778 | [String] 2779 | $DomainController, 2780 | 2781 | [String] 2782 | $ComputerName, 2783 | 2784 | [String] 2785 | $Filter = "(objectCategory=Computer)(ms-mcs-admpwdexpirationtime=*)", 2786 | 2787 | [Switch] 2788 | $ExcludeDelegated, 2789 | 2790 | [ValidateRange(1,10000)] 2791 | [Int] 2792 | $PageSize = 200, 2793 | 2794 | [Management.Automation.PSCredential] 2795 | $Credential 2796 | 2797 | ) 2798 | begin { 2799 | 2800 | if($ComputerName) { 2801 | $LAPSFilter = "$Filter(dNSHostName=$ComputerName)" 2802 | } 2803 | else { 2804 | $LAPSFilter = "$Filter" 2805 | } 2806 | 2807 | Write-Verbose "Retrieving all ExtendedRight ACLs for domain $Domain" 2808 | $ExtendedRights = Get-ObjectAcl -ResolveGUIDs -Filter $LAPSFilter -Domain $Domain -DomainController $DomainController -Credential $Credential -PageSize $PageSize | Where-Object { $_.ActiveDirectoryRights -match "ExtendedRight" } 2809 | 2810 | # Build a hash table of DN and hostnames 2811 | $CompMap = @{} 2812 | $ComputerObjects = Get-NetComputer -Filter "(ms-mcs-admpwdexpirationtime=*)" -FullData -Domain $Domain -DomainController $DomainController -Credential $Credential | ForEach-Object { $CompMap.Add($_.distinguishedname, $_.dnshostname) } 2813 | 2814 | if($Credential){ 2815 | # Build a hash table of SIDs and user/group 2816 | Write-Verbose "Retrieving all users and groups to resolve SIDs when using PSCredential" 2817 | $SIDMap = @{} 2818 | Get-NetUser -Domain $Domain -DomainController $DomainController -Credential $Credential | ForEach-Object { $SIDMap.Add($_.objectsid, $_.samaccountname) } 2819 | Get-NetGroup -FullData -Domain $Domain -DomainController $DomainController -Credential $Credential | ForEach-Object { $SIDMap.Add($_.objectsid, $_.samaccountname) } 2820 | } 2821 | } 2822 | process { 2823 | 2824 | $ExtendedRights | ForEach-Object { 2825 | 2826 | $ComputerName = $CompMap[$_.ObjectDN] 2827 | Write-Verbose "Parsing ACLs for $ComputerName" 2828 | $Identity = $_.IdentityReference 2829 | 2830 | if($_.ObjectType -match "ms-Mcs-AdmPwd" -and !($ExcludeDelegated)) { 2831 | $Reason = "Delegated" 2832 | } 2833 | elseif($_.ObjectType -match "All" -and $_.IdentityReference -notmatch "BUILTIN") { 2834 | $Reason = "All" 2835 | } 2836 | else { return } 2837 | 2838 | # Map the SID to user/group if not on domain 2839 | if($Credential) { 2840 | if($SIDMap.Contains($Identity.ToString())) { 2841 | $Identity = $SIDMap[$Identity.ToString()] 2842 | } 2843 | } 2844 | 2845 | $ExtendedRightUser = New-Object PSObject 2846 | $ExtendedRightUser | Add-Member Noteproperty 'ComputerName' "$ComputerName" 2847 | $ExtendedRightUser | Add-Member Noteproperty 'Identity' "$Identity" 2848 | $ExtendedRightUser | Add-Member Noteproperty 'Reason' "$Reason" 2849 | $ExtendedRightUser 2850 | 2851 | } 2852 | } 2853 | } 2854 | 2855 | 2856 | function Find-LAPSDelegatedGroups { 2857 | <# 2858 | .SYNOPSIS 2859 | 2860 | This function uses Get-NetOU to query the domain 2861 | for all OUs and finds the groups the system admin delegated 2862 | to read the ms-Mcs-AdmPwd attribute. This will not find users 2863 | with special permissions that can also read the attribute 2864 | (i.e. users configured with the All Extended Rights permission) 2865 | 2866 | Credits to @harmj0y and @pyrotek3 for research 2867 | 2868 | Author: @leoloobeek 2869 | 2870 | .PARAMETER Domain 2871 | 2872 | The domain to use for the query, defaults to the current domain. 2873 | 2874 | .PARAMETER DomainController 2875 | 2876 | Domain controller to reflect LDAP queries through. 2877 | 2878 | .PARAMETER PageSize 2879 | 2880 | The PageSize to set for the LDAP searcher object. 2881 | 2882 | .PARAMETER Credential 2883 | 2884 | A [Management.Automation.PSCredential] object of alternate credentials 2885 | for connection to the target domain. 2886 | 2887 | NOTE: You must use FQDN of domain: testlab.local\user, not testlab\user 2888 | 2889 | .EXAMPLE 2890 | 2891 | PS C:\> Find-LAPSDelegatedGroups 2892 | 2893 | Description 2894 | ----------- 2895 | Retrieves the groups delegated to read the ms-Mcs-AdmPwd for each OU 2896 | 2897 | .LINK 2898 | 2899 | http://www.harmj0y.net/blog/powershell/running-laps-with-powerview/ 2900 | https://adsecurity.org/?p=1790 2901 | #> 2902 | [CmdletBinding()] 2903 | param( 2904 | 2905 | [String] 2906 | $DomainController, 2907 | 2908 | [String] 2909 | $Domain, 2910 | 2911 | [ValidateRange(1,10000)] 2912 | [Int] 2913 | $PageSize = 200, 2914 | 2915 | [Management.Automation.PSCredential] 2916 | $Credential 2917 | 2918 | ) 2919 | 2920 | # Next few lines taken from http://www.harmj0y.net/blog/powershell/running-laps-with-powerview/ 2921 | Get-NetOU -Domain $Domain -DomainController $DomainController -Credential $Credential -FullData | 2922 | Get-ObjectAcl -Domain $Domain -DomainController $DomainController -Credential $Credential -ResolveGUIDs | Where-Object { 2923 | ($_.ObjectType -like 'ms-Mcs-AdmPwd') -and 2924 | ($_.ActiveDirectoryRights -match 'ReadProperty') 2925 | } | ForEach-Object { 2926 | $dn = $_.ObjectDN 2927 | $ir = $_.IdentityReference 2928 | $DelegatedGroup = New-Object PSObject 2929 | $DelegatedGroup | Add-Member NoteProperty 'OrgUnit' "$dn" 2930 | $DelegatedGroup | Add-Member Noteproperty 'Delegated Groups' "$ir" 2931 | $DelegatedGroup 2932 | } 2933 | } 2934 | 2935 | 2936 | function Get-LAPSComputers { 2937 | <# 2938 | .SYNOPSIS 2939 | 2940 | Retrieves all computers with LAPS enabled. Passwords for the 2941 | accounts are displayed if the user has access to view the 2942 | ms-Mcs-AdmPwd attribute. 2943 | 2944 | Similar to @kfosaaen's Get-LAPSPasswords but leverages @harmj0y's 2945 | PowerView functions and can be used to find computers with LAPS 2946 | enabled even if user does not have permissions to view password. 2947 | 2948 | Note: Parameters are taken from Get-NetComputer as this function 2949 | is essentially a wrapper around it. 2950 | 2951 | .PARAMETER ComputerName 2952 | 2953 | Return computers with a specific name, wildcards accepted. 2954 | 2955 | .PARAMETER SPN 2956 | 2957 | Return computers with a specific service principal name, wildcards accepted. 2958 | 2959 | .PARAMETER Domain 2960 | 2961 | The domain to query for computers, defaults to the current domain. 2962 | 2963 | .PARAMETER DomainController 2964 | 2965 | Domain controller to reflect LDAP queries through. 2966 | 2967 | .PARAMETER ADSpath 2968 | 2969 | The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 2970 | Useful for OU queries. 2971 | 2972 | .PARAMETER SiteName 2973 | 2974 | The AD Site name to search for computers. 2975 | 2976 | .PARAMETER Unconstrained 2977 | 2978 | Switch. Return computer objects that have unconstrained delegation. 2979 | 2980 | .PARAMETER PageSize 2981 | 2982 | The PageSize to set for the LDAP searcher object. 2983 | 2984 | .PARAMETER Credential 2985 | 2986 | A [Management.Automation.PSCredential] object of alternate credentials 2987 | for connection to the target domain. 2988 | 2989 | NOTE: You must use FQDN of domain: testlab.local\user, not testlab\user 2990 | 2991 | .EXAMPLE 2992 | 2993 | PS C:\> Get-LAPSComputers 2994 | 2995 | Description 2996 | ----------- 2997 | Retreives all computer objects from domain with LAPS enabled and displays 2998 | the time the password expires and the password if the user has read access. 2999 | 3000 | .LINK 3001 | 3002 | https://github.com/kfosaaen/Get-LAPSPasswords 3003 | https://blog.netspi.com/running-laps-around-cleartext-passwords/ 3004 | #> 3005 | [CmdletBinding()] 3006 | Param ( 3007 | [Parameter(ValueFromPipeline=$True)] 3008 | [Alias('HostName')] 3009 | [String] 3010 | $ComputerName = '*', 3011 | 3012 | [String] 3013 | $SPN, 3014 | 3015 | [String] 3016 | $Domain, 3017 | 3018 | [String] 3019 | $DomainController, 3020 | 3021 | [String] 3022 | $ADSpath, 3023 | 3024 | [String] 3025 | $SiteName, 3026 | 3027 | [Switch] 3028 | $Unconstrained, 3029 | 3030 | [ValidateRange(1,10000)] 3031 | [Int] 3032 | $PageSize = 200, 3033 | 3034 | [Management.Automation.PSCredential] 3035 | $Credential 3036 | ) 3037 | process { 3038 | 3039 | Get-NetComputer -FullData -Filter "(ms-mcs-admpwdexpirationtime=*)" @PSBoundParameters | ForEach-Object { 3040 | 3041 | $HostName = $_.dnshostname 3042 | $Password = $_."ms-mcs-admpwd" 3043 | 3044 | # epoch conversion code taken directly from https://github.com/kfosaaen/Get-LAPSPasswords/blob/master/Get-LAPSPasswords.ps1 3045 | If ($_."ms-MCS-AdmPwdExpirationTime" -ge 0) { 3046 | $CurrentExpiration = $([datetime]::FromFileTime([convert]::ToInt64($_."ms-MCS-AdmPwdExpirationTime",10))) 3047 | } 3048 | Else{ 3049 | $CurrentExpiration = "N/A" 3050 | } 3051 | 3052 | $Computer = New-Object PSObject 3053 | $Computer | Add-Member NoteProperty 'ComputerName' "$HostName" 3054 | $Computer | Add-Member Noteproperty 'Password' "$Password" 3055 | $Computer | Add-Member Noteproperty 'Expiration' "$CurrentExpiration" 3056 | $Computer 3057 | 3058 | } 3059 | } 3060 | 3061 | } 3062 | --------------------------------------------------------------------------------