├── ACLight - version 1 ├── ACLight.ps1 ├── ACLight.psd1 ├── ACLight.psm1 ├── Execute-ACLight.bat └── README.md ├── ACLight2.ps1 ├── ACLight2.psd1 ├── ACLight2.psm1 ├── Execute-ACLight2.bat ├── LICENSE └── README.md /ACLight - version 1/ACLight.ps1: -------------------------------------------------------------------------------- 1 | <#---------------------------------------------------------------------------------------------------- 2 | 3 | ########################################################################################## 4 | # # 5 | # Discovering Privileged Accounts and Shadow Admins - using Advanced ACLs Analysis # 6 | # # 7 | ########################################################################################## 8 | 9 | 10 | Release Notes: 11 | 12 | The ACLight is a tool for discovering Privileged Accounts through advanced ACLs analysis. 13 | It will discover the Shadow Admins in the network. 14 | It queries the Active Directory for its objects' ACLs and then filters the sensitive permissions from each one of them. 15 | The results are the domain privileged accounts in the network (from the advanced ACLs perspective of the AD). 16 | It automatically scans all the domains of the forest. 17 | You can run the scan with just any regular user in the domain (could be non-privleged user) and it needs PowerShell version 3+. 18 | 19 | Version 1.0: 28.8.16 20 | Version 1.1: 15.9.16 21 | version 2.0: 17.5.17 22 | version 2.1: 4.6.17 23 | 24 | Authors: Asaf Hecht (@hechtov) - Cyberark's research team. 25 | Using functions from the great PowerView project created by: Will Schroeder (@harmj0y). 26 | The original PowerView have more functionalities: 27 | Powerview: https://github.com/PowerShellEmpire/PowerTools/tree/master/PowerView 28 | 29 | ------------------------------------------------------------------------------------------------------ 30 | 31 | HOW TO RUN: 32 | 33 | option 1 - Just double click on "Execute-ACLight.bat". 34 | 35 | - OR - 36 | 37 | option 2 - Open cmd: 38 | Go to "ACLight" main folder -> 1) Type: cd "" 39 | Run the "ACLight" script -> 2) Type: powershell -noprofile -ExecutionPolicy Bypass Import-Module '.\ACLight.psm1' -force ; Start-ACLsAnalysis 40 | 41 | - OR - 42 | 43 | Option 3 - Open PowerShell (with -ExecutionPolicy Bypass): 44 | 1) cd "" 45 | 2) Import-Module '.\ACLight.psm1' -force 46 | 3) Start-ACLsAnalysis 47 | 48 | Execute it and check the result! 49 | You should take care of all the privileged accounts that the tool discovered for you. 50 | Especially - take care of the Shadow Admins! 51 | Those are accounts with direct sensitive ACLs assignments (not through the known privileged groups). 52 | 53 | ------------------------------------------------------------------------------------------------------ 54 | 55 | THE RESULTS FILES: 56 | 57 | 1) First check the - "Accounts with extra permissions.txt" file - It's straight-forward & powerful list of the privileged accounts that were discovered in the network. 58 | 2) "All entities with extra permissions.txt" - Will give you more sensitive entities in the network (also the "empty" entities like empty groups). 59 | 3) "Privileged Accounts Permissions - Final Report.csv" - This is the final summary report - in this file you can see what is the exact sensitive permission each account has. 60 | 4) "Privileged Accounts Permissions - Irregular Accounts.csv" - Similar to the final report just with only the privileged accounts that have direct permissions (not through their group membership). 61 | 5) "research.com - Full Output.csv" - Every domain that was scanned will have a csv with more raw results of the ACLs. 62 | 63 | 64 | ----------------------------------------------------------------------------------------------------#> 65 | 66 | ##Requires -Version 3.0 or above 67 | 68 | ###################################################################### 69 | # # 70 | # Section 1 - main functions for advanced analysis of the ACLs # 71 | # # 72 | ###################################################################### 73 | 74 | # Create the results folder 75 | $resultsPath = $PSScriptRoot + "\Results" 76 | if (Test-Path $resultsPath) 77 | { 78 | write-verbose "The results folder was already exists" 79 | } 80 | else 81 | { 82 | New-Item -ItemType directory -Path $resultsPath 83 | } 84 | 85 | # Function for advanced ACLs analysis in a specified domain 86 | function Start-domainACLsAnalysis { 87 | 88 | [CmdletBinding()] 89 | Param ( 90 | [Parameter(ValueFromPipeline=$True)] 91 | [String] 92 | $Full = $False, 93 | 94 | [String] 95 | $SamAccountName, 96 | 97 | [String] 98 | $Name = "*", 99 | 100 | [Alias('DN')] 101 | [String] 102 | $DistinguishedName = "*", 103 | 104 | [String] 105 | $Filter, 106 | 107 | [String] 108 | $ADSpath, 109 | 110 | [String] 111 | $ADSprefix, 112 | 113 | [String] 114 | $Domain, 115 | 116 | [String] 117 | $DomainController, 118 | 119 | [String] 120 | $exportCsvFile = "C:\scanACLsResults.csv", 121 | 122 | [ValidateRange(1,10000)] 123 | [Int] 124 | $PageSize = 200 125 | ) 126 | 127 | #clean the csv output file 128 | if (Test-Path $exportCsvFile) { 129 | Remove-Item $exportCsvFile 130 | } 131 | 132 | $Domaintime = New-Object system.Diagnostics.Stopwatch 133 | $DomainTotaltime = New-Object system.Diagnostics.Stopwatch 134 | $Domaintime.Start() 135 | $DomainTotaltime.Start() 136 | 137 | $DomainList = @() 138 | $PrivilegedOwners = @() 139 | $PrivilegedEntities = @() 140 | $PrivilegedGroups = @() 141 | $PrivilegedAccounts = @() 142 | $GroupMembersDB = @{} 143 | $domainPrivilegedOwners = @() 144 | $domainPrivilegedEntities = @() 145 | $count++ 146 | $DomainDN = "DC=$($Domain.Replace('.', ',DC='))" 147 | Write-Output "Starting the scan for Domain: $domain" 148 | 149 | ############################################################################################################################################### 150 | # Important - here you can choose each sensitive Active Directory objects you want to scan. 151 | # You can add or remove scan filters and check the new results. 152 | # It will affect the scanning time duration and the results might include less privileged accounts (if you choose less sensitive AD objects). 153 | # It's also recommended to add here the privilged accounts that were discovered in previous scans - to discover who has control over them 154 | ############################################################################################################################################### 155 | 156 | # the root of the domain 157 | Invoke-ACLScanner -Full $Full -exportCsvFile $exportCsvFile -Domain $Domain -DistinguishedName $DomainDN 158 | # wild char on "admin" - it will be very interesting but also might includes less sensitive objects 159 | Invoke-ACLScanner @PSBoundParameters -Name '*admin*' 160 | # more built-in sensitive groups, every organization can add here more of his unique sensitive groups 161 | Invoke-ACLScanner @PSBoundParameters -Name 'Server Operators' 162 | Invoke-ACLScanner @PSBoundParameters -Name 'Account Operators' 163 | Invoke-ACLScanner @PSBoundParameters -Name 'Backup Operators' 164 | Invoke-ACLScanner @PSBoundParameters -Name 'Group Policy Creator Owners' 165 | # the krbtgt account 166 | Invoke-ACLScanner @PSBoundParameters -Name 'Krbtgt' 167 | # the main containers 168 | $ObjectName = "CN=Users,$DomainDN" 169 | Invoke-ACLScanner @PSBoundParameters -DistinguishedName $ObjectName 170 | $ObjectName = "CN=Computers,$DomainDN" 171 | Invoke-ACLScanner @PSBoundParameters -DistinguishedName $ObjectName 172 | $ObjectName = "CN=System,$DomainDN" 173 | Invoke-ACLScanner @PSBoundParameters -DistinguishedName $ObjectName 174 | $ObjectName = "CN=Policies,CN=System,$DomainDN" 175 | Invoke-ACLScanner @PSBoundParameters -DistinguishedName $ObjectName 176 | $ObjectName = "CN=Managed Service Accounts,$DomainDN" 177 | Invoke-ACLScanner @PSBoundParameters -DistinguishedName $ObjectName 178 | # the AdminSDHolder object 179 | $ObjectName = "CN=AdminSDHolder,CN=System,$DomainDN" 180 | Invoke-ACLScanner @PSBoundParameters -DistinguishedName $ObjectName 181 | 182 | #Analyze every OUs, if it's not Full scan it analyzes only the Domain Controller OU 183 | $domainOU = Get-NetOU -Domain $Domain 184 | $counter = 0 185 | $numberOU = $domainOU.count 186 | foreach ($OU in $domainOU){ 187 | $counter++ 188 | $OUdn = 'None' 189 | $NameArray = $OU -split("/") 190 | [int]$NameCount = 0 191 | ForEach ($NameCell in $NameArray) 192 | { 193 | $NameCount++ 194 | if ($NameCount -eq 4){ 195 | $OUdn = $NameCell 196 | } 197 | } 198 | if ($OUdn -match "Domain Controller"){ 199 | if ($Full -eq $True){ 200 | if ($counter -eq 1) { 201 | Write-Output "Finished 13 analysis queries, there are still $numberOU more" 202 | } 203 | } 204 | Invoke-ACLScanner @PSBoundParameters -DistinguishedName $OUdn 205 | } 206 | else { 207 | if ($Full -eq $True){ 208 | if ($counter -eq 1) { 209 | Write-Output "Finished 13 analysis queries, there are still $numberOU more" 210 | } 211 | Invoke-ACLScanner @PSBoundParameters -DistinguishedName $OUdn 212 | } 213 | } 214 | } 215 | 216 | Write-Output "Finish first analysis on Domain: $Domain" 217 | $Domaintime.Stop() 218 | $runtime = $Domaintime.Elapsed.TotalMilliseconds 219 | $runtime = ($runtime/1000) 220 | $runtimeMin = ($runtime/60) 221 | $runtimeHours = ($runtime/3600) 222 | $runtime = [math]::round($runtime , 2) 223 | $runtimeMin = [math]::round($runtimeMin , 2) 224 | $runtimeHours = [math]::round($runtimeHours , 3) 225 | Write-Host "Time elapsed for this stage: $runtime Second, $runtimeMin Minutes, $runtimeHours Hours" 226 | $Domaintime.reset() 227 | $Domaintime.start() 228 | 229 | $NewListACLs = @() 230 | $ListObjectDNs = @() 231 | $domainGroups = Get-NetGroup -Domain $Domain 232 | if ($domainGroups.count -eq 0){ 233 | write-warning "There was a critical problem of getting the domain groups" 234 | } 235 | 236 | $ObjectMembersList = @{} 237 | $counterLines = 0 238 | $NameCount = 0 239 | 240 | Import-Csv $exportCsvFile | Where-Object {$_} | ForEach-Object { 241 | If ($ListObjectDNs -notcontains $_.ObjectDN) 242 | { 243 | $ListObjectDNs += $_.ObjectDN 244 | } 245 | #adding group members 246 | $GroupMembers = $Null 247 | $EntityType = "Other" 248 | 249 | $domainGroupName = "None" 250 | $NameArray = $_.UpdatedIdentityReference -Split("\\") 251 | $NameCount = 0 252 | ForEach ($NameCell in $NameArray) 253 | { 254 | $NameCount++ 255 | if ($NameCount -eq 1) 256 | {continue} 257 | else 258 | {$domainGroupName = $NameCell} 259 | } 260 | if ($domainGroups -contains $domainGroupName) { 261 | $EntityType = "Group" 262 | if ($GroupMembersDB.ContainsKey($domainGroupName)){ 263 | $GroupMembers = $GroupMembersDB.$domainGroupName 264 | } 265 | else { 266 | try { 267 | $GroupMembersRecursive = Get-NetGroupMember -Domain $Domain -Recurse -UseMatchingRule -GroupName $domainGroupName 268 | } 269 | catch { 270 | $GroupMembersRecursive = Get-NetGroupMember -Domain $Domain -GroupName $domainGroupName 271 | #Write-Warning $_ 272 | } 273 | $GroupMembers = @() 274 | foreach ($Entity in $GroupMembersRecursive){ 275 | if (!($GroupMembers -match $Entity.MemberName)){ 276 | $GroupMembers += $Entity.MemberName 277 | } 278 | } 279 | $GroupMembersDB.add($domainGroupName, $GroupMembers) 280 | $GroupMembersRecursive = $Null 281 | } 282 | } 283 | $GroupMembersCount = $GroupMembers.count 284 | 285 | #create the $ObjectMembersList 286 | $isMemberOfOtherGroups = $Null 287 | if ($domainGroups -contains $domainGroupName) { 288 | if ($ObjectMembersList.ContainsKey($_.ObjectDN)){ 289 | $ObjectDN = $_.ObjectDN 290 | foreach ($user in $GroupMembers){ 291 | if ($ObjectMembersList.$ObjectDN -notcontains $domainGroupName){ 292 | $ObjectMembersList.$ObjectDN += $domainGroupName 293 | if ($ObjectMembersList.$ObjectDN -notcontains $user){ 294 | $ObjectMembersList.$ObjectDN += $user 295 | } 296 | } 297 | } 298 | } 299 | else { 300 | $ObjectMembersList.add($_.ObjectDN, $GroupMembers) 301 | } 302 | } 303 | 304 | #in the future step it checks the class of the object 305 | $ObjectClassCategory = $Null 306 | 307 | #creates the structure to output the csv 308 | $ObjectACE = [PSCustomObject][ordered] @{ 309 | #$ObjectACE = [PSCustomObject] @{ 310 | ObjectDN = [string]$_.ObjectDN 311 | ObjectOwner = [string]$_.ObjectOwner 312 | EntityName = [string]$_.UpdatedIdentityReference 313 | ActiveDirectoryRights = [string]$_.ActiveDirectoryRights 314 | ObjectRights = [string]$_.ObjectType 315 | ObjectClass = [string]$_.ObjectClass 316 | ObjectClassCategory = [string]$ObjectClassCategory 317 | EntityType = [string]$EntityType 318 | EntityGroupMembers = [string]$GroupMembers 319 | EntityGroupMembersCount = [string]$GroupMembersCount 320 | isMemberOfOtherGroups = [string]$isMemberOfOtherGroups 321 | IsInherited = [string]$_.IsInherited 322 | PropagationFlags = [string]$_.PropagationFlags 323 | InheritanceFlags = [string]$_.InheritanceFlags 324 | InheritedObjectType = [string]$_.InheritedObjectType 325 | InheritanceType = [string]$_.InheritanceType 326 | ObjectFlags = [string]$_.ObjectFlags 327 | AccessControlType = [string]$_.AccessControlType 328 | ObjectSID = [string]$_.ObjectSID 329 | IdentitySID = [string]$_.IdentitySID 330 | 331 | } 332 | $NewListACLs += $ObjectACE 333 | #counter 334 | $counterLines++ 335 | $counter = $counterLines 336 | if (($counter %= 50000) -eq 0){ 337 | write-host "$counterLines Permission lines were finished" 338 | } 339 | $_ = $Null 340 | } 341 | 342 | Write-Output "`nGood, the second stage was over" 343 | $Domaintime.Stop() 344 | $runtime = $Domaintime.Elapsed.TotalMilliseconds 345 | $runtime = ($runtime/1000) 346 | $runtimeMin = ($runtime/60) 347 | $runtimeHours = ($runtime/3600) 348 | $runtime = [math]::round($runtime , 2) 349 | $runtimeMin = [math]::round($runtimeMin , 2) 350 | $runtimeHours = [math]::round($runtimeHours , 3) 351 | Write-Host "Time of the second stage: $runtime Second, $runtimeMin Minutes, $runtimeHours Hours" 352 | $Domaintime.reset() 353 | $Domaintime.start() 354 | 355 | foreach ($ACE in $NewListACLs){ 356 | #check object class 357 | $ObjectClassCategory = $ACE.ObjectClass 358 | if ($ACE.ObjectClass -match "domain") {$ObjectClassCategory = "Domain"} 359 | elseif ($ACE.ObjectClass -match "container") {$ObjectClassCategory = "Container"} 360 | elseif ($ACE.ObjectClass -match "group") {$ObjectClassCategory = "Group"} 361 | elseif ($ACE.ObjectClass -match "computer") {$ObjectClassCategory = "Computer"} 362 | elseif ($ACE.ObjectClass -match "user") {$ObjectClassCategory = "User"} 363 | elseif ($ACE.ObjectClass -match "dns") {$ObjectClassCategory = "DNS"} 364 | elseif ($ACE.ObjectClass -match "organizationalUnit") {$ObjectClassCategory = "OU"} 365 | $ACE.ObjectClassCategory = $ObjectClassCategory 366 | 367 | try { 368 | $isMemberOfOtherGroups = 'False' 369 | $NameArray = $ACE.EntityName -split("\\") 370 | [int]$NameCount = 0 371 | ForEach ($NameCell in $NameArray) 372 | { 373 | $NameCount++ 374 | if ($NameCount -eq 1) 375 | {continue} 376 | else 377 | {$userName = $NameCell} 378 | } 379 | $ObjectDN = $ACE.ObjectDN 380 | if ($userName.count -gt 0) { 381 | if ($ObjectMembersList.$ObjectDN -contains $userName) { 382 | $isMemberOfOtherGroups = 'True' 383 | } 384 | } 385 | $ACE.isMemberOfOtherGroups = $isMemberOfOtherGroups 386 | } 387 | catch{} 388 | } 389 | 390 | $numObjectAnalyzed = $ListObjectDNs.Count 391 | $NewListACLs | Export-Csv -NoTypeInformation $exportCsvFile 392 | 393 | Write-Output "`nExcellent, The scan for $Domain was finished - check the results file:" 394 | Write-Output $exportCsvFile 395 | Write-Output "`nNumber of Objects that were Analyzed: $numObjectAnalyzed" 396 | $DomainTotaltime.Stop() 397 | $runtime = $DomainTotaltime.Elapsed.TotalMilliseconds 398 | $runtime = ($runtime/1000) 399 | $runtimeMin = ($runtime/60) 400 | $runtimeHours = ($runtime/3600) 401 | $runtime = [math]::round($runtime , 2) 402 | $runtimeMin = [math]::round($runtimeMin , 2) 403 | $runtimeHours = [math]::round($runtimeHours , 3) 404 | Write-Host "The scan for this Domain took: $runtime Second, $runtimeMin Minutes, $runtimeHours Hours" 405 | } 406 | 407 | # Function to reorder the results for more straight-forward output 408 | function OrderPermissionsByAccounts { 409 | 410 | [CmdletBinding()] 411 | Param ( 412 | [String] 413 | $inputCSV, 414 | 415 | [String] 416 | $Domain, 417 | 418 | [array] 419 | $privilegedAccountList, 420 | 421 | [hashtable] 422 | $domainsPrivilegedAccountDB, 423 | 424 | [String] 425 | $exportCsvFolder 426 | ) 427 | 428 | $newAccountPermissionList = @() 429 | $owner = "ObjectOwner" 430 | $privDomainAcc = $domainsPrivilegedAccountDB.$Domain 431 | Import-Csv $inputCSV | Where-Object {$_} | ForEach-Object { 432 | foreach($account in $privilegedAccountList){ 433 | if (($_.EntityName -eq $account) -or ($_.EntityGroupMembers -eq $account)){ 434 | $accountPermissionLine = [PSCustomObject][ordered] @{ 435 | Domain = [string]$Domain 436 | AccountName = [string]$account 437 | AccountGroup = [string]$_.EntityName 438 | ActiveDirectoryRights = [string]$_.ActiveDirectoryRights 439 | ObjectRights = [string]$_.ObjectRights 440 | ObjectDN = [string]$_.ObjectDN 441 | ObjectOwner = [string]$_.ObjectOwner 442 | ObjectClassCategory = [string]$_.ObjectClassCategory 443 | } 444 | $newAccountPermissionList += $accountPermissionLine 445 | } 446 | else{ 447 | if ($_.ObjectOwner -eq $account){ 448 | $accountPermissionLine = [PSCustomObject][ordered] @{ 449 | Domain = [string]$Domain 450 | AccountName = [string]$account 451 | AccountGroup = [string]$_.ObjectOwner 452 | ActiveDirectoryRights = [string]$owner 453 | ObjectRights = [string]$owner 454 | ObjectDN = [string]$_.ObjectDN 455 | ObjectOwner = [string]$_.ObjectOwner 456 | ObjectClassCategory = [string]$_.ObjectClassCategory 457 | } 458 | $newAccountPermissionList += $accountPermissionLine 459 | } 460 | } 461 | } 462 | foreach($account in $privDomainAcc){ 463 | if ($_.EntityGroupMembers -match $account){ 464 | $accountPermissionLine = [PSCustomObject][ordered] @{ 465 | Domain = [string]$Domain 466 | AccountName = [string]$account 467 | AccountGroup = [string]$_.EntityName 468 | ActiveDirectoryRights = [string]$_.ActiveDirectoryRights 469 | ObjectRights = [string]$_.ObjectRights 470 | ObjectDN = [string]$_.ObjectDN 471 | ObjectOwner = [string]$_.ObjectOwner 472 | ObjectClassCategory = [string]$_.ObjectClassCategory 473 | } 474 | $newAccountPermissionList += $accountPermissionLine 475 | } 476 | } 477 | } 478 | $exportCsvFolder += $Domain 479 | $exportCsvFolder += " - Sensitive Accounts.csv" 480 | $exportAccCsvFile = $exportCsvFolder 481 | $newAccountPermissionList | sort AccountName, AccountGroup, Domain, ObjectDN | Export-Csv -NoTypeInformation $exportAccCsvFile 482 | } 483 | 484 | # The main function - here it's the starting point of the Privileged ACLs scan 485 | function Start-ACLsAnalysis { 486 | <# 487 | .SYNOPSIS 488 | Thi is the function to start the ACLs advanced scan. 489 | It will do analysis of the Permissions and ACLs on all the domains in the forest - automatically. 490 | In the end of the scanning - there will be good reports in the output folder. 491 | The scan will discover who are the privileged accounts in the forest and what permissions exactly they have. 492 | 493 | .EXAMPLE 494 | 1. Open PowerShell 495 | 2. Import-Module '.\ACLight.psm1' -force 496 | 3. Start-ACLsAnalysis 497 | 498 | #> 499 | 500 | [CmdletBinding()] 501 | Param ( 502 | [Parameter(ValueFromPipeline=$True)] 503 | [String] 504 | $Full = $False, 505 | 506 | [String] 507 | $SamAccountName, 508 | 509 | [String] 510 | $Name = "*", 511 | 512 | [Alias('DN')] 513 | [String] 514 | $DistinguishedName = "*", 515 | 516 | [String] 517 | $Filter, 518 | 519 | [String] 520 | $ADSpath, 521 | 522 | [String] 523 | $ADSprefix, 524 | 525 | [String] 526 | $Domain, 527 | 528 | [String] 529 | $DomainController, 530 | 531 | [String] 532 | $ScriptRoot = $PSScriptRoot, 533 | 534 | [String] 535 | $exportCsvFolder = "$resultsPath", 536 | 537 | [ValidateRange(1,10000)] 538 | [Int] 539 | $PageSize = 200 540 | ) 541 | 542 | if ($PSVersionTable.PSVersion.Major -ge 3){ 543 | $time = New-Object system.Diagnostics.Stopwatch 544 | $stagetime = New-Object system.Diagnostics.Stopwatch 545 | $time.Start() 546 | 547 | Write-Output "`nGreat, the scan was started.`nIt could take a while (5-60+ mins) depends on the size of the network`n" 548 | 549 | $PathFolder = $exportCsvFolder 550 | $PathFolder = $PathFolder.substring($PathFolder.length - 1, 1) 551 | if ($PathFolder -ne "\"){ 552 | $exportCsvFolder += "\" 553 | } 554 | 555 | $DomainList = Get-NetForestDomain 556 | $count = 0 557 | $privilegedAccountList = @() 558 | $privilegedAllList = @() 559 | $processPointersList = @() 560 | $domainsPrivilegedAccountDB = @{} 561 | $domainNumber = $DomainList.count 562 | Write-Output "Discovered $domainNumber Domain" 563 | 564 | # run ACLs analysis on every domain 565 | foreach ($Domain in $DomainList){ 566 | Write-Output "`n******************************`nOpened process for analyzing Domain: $Domain`n" 567 | $exportCsvFile = $exportCsvFolder 568 | $exportCsvFile += $Domain 569 | $exportCsvFile += " - Full Output.csv" 570 | $exportCsvFile = '\"' + $exportCsvFile + '\"' 571 | 572 | # The scan will automatically scan all the domain in a parallel time - it's much more time efficient 573 | $processPointer = start-process powershell.exe -PassThru -WorkingDirectory $ScriptRoot -argument "-noprofile -ExecutionPolicy Bypass Import-Module '.\ACLight.psm1' -force ; Start-domainACLsAnalysis -Full $Full -exportCsvFile $exportCsvFile -Domain $Domain" 574 | $processPointersList += $processPointer 575 | 576 | # if you don't won't to scan all the domains in parallel (but one after the other): 577 | #$processPointer | Wait-Process 578 | } 579 | 580 | Write-Output "Waiting for all the scans to be completed.." 581 | foreach ($processPt in $processPointersList){ 582 | try{ 583 | $processPt | Wait-Process 584 | } 585 | catch{ 586 | } 587 | } 588 | 589 | Write-Output "All the processes completed. Now, starting Accounts analysis.." 590 | 591 | foreach ($Domain in $DomainList){ 592 | $exportCsvFile = $exportCsvFolder 593 | $exportCsvFile += $Domain 594 | $exportCsvFile += " - Full Output.csv" 595 | #create the final list of privileged accounts 596 | $privilegedDomainAccountList = @() 597 | $domainGroups = Get-NetGroup -Domain $Domain 598 | $domainUsers = Get-NetUser -Domain $Domain 599 | $domainUserList = @() 600 | $privDomain = @() 601 | $domainUserList = $domainUsers.name 602 | 603 | Import-Csv $exportCsvFile | Where-Object {$_} | ForEach-Object { 604 | If ($privilegedDomainAccountList -notcontains $_.ObjectOwner){ 605 | $privilegedDomainAccountList += $_.ObjectOwner 606 | } 607 | If ($privilegedDomainAccountList -notcontains $_.EntityName){ 608 | $privilegedDomainAccountList += $_.EntityName 609 | } 610 | } 611 | 612 | $EntityStartName = "" 613 | foreach ($fullNameEntity in $privilegedDomainAccountList){ 614 | $domainEntityName = $fullNameEntity 615 | if ($fullNameEntity -match "\\"){ 616 | $NameArray = $fullNameEntity -split("\\") 617 | $NameCount = 0 618 | ForEach ($NameCell in $NameArray) 619 | { 620 | $NameCount++ 621 | if ($NameCount -eq 1){ 622 | $EntityStartName = $NameCell 623 | } 624 | else 625 | {$domainEntityName = $NameCell} 626 | } 627 | } 628 | 629 | if ($privilegedAllList -notcontains $fullNameEntity){ 630 | $privilegedAllList += $fullNameEntity 631 | } 632 | if ($EntityStartName -notmatch "BUILTIN"){ 633 | if ($domainGroups -contains $domainEntityName){ 634 | try { 635 | $GroupMembersRecursive = Get-NetGroupMember -domain $Domain -Recurse -UseMatchingRule -GroupName $domainEntityName 636 | } 637 | catch { 638 | $GroupMembersRecursive = Get-NetGroupMember -domain $Domain -GroupName $domainEntityName 639 | #Write-Warning $_ 640 | } 641 | foreach ($accountName in $GroupMembersRecursive){ 642 | $accountDomainName = $EntityStartName + "\" + $accountName.MemberName 643 | if ($privilegedAccountList -notcontains $accountDomainName){ 644 | $privilegedAccountList += $accountDomainName 645 | #create hash table for accounts by their domain values 646 | if ($privilegedAllList -notcontains $accountDomainName){ 647 | $privilegedAllList += $accountDomainName 648 | } 649 | } 650 | $accountN = $accountName.MemberName 651 | if ($privDomain -notcontains $accountN){ 652 | $privDomain += $accountN 653 | } 654 | } 655 | } 656 | else { 657 | if ($domainUserList -contains $domainEntityName){ 658 | if ($privilegedAccountList -notcontains $fullNameEntity ){ 659 | $privilegedAccountList += $fullNameEntity 660 | } 661 | if ($privDomain -notcontains $domainEntityName){ 662 | $privDomain += $domainEntityName 663 | } 664 | } 665 | } 666 | } 667 | # adding a special test for the dangerous case of "Authenticated Users" 668 | if ($fullNameEntity -like "NT AUTHORITY\Authenticated Users"){ 669 | if ($privilegedAccountList -notcontains $fullNameEntity ){ 670 | $privilegedAccountList += $fullNameEntity 671 | } 672 | } 673 | } 674 | 675 | $domainsPrivilegedAccountDB.add($Domain, $privDomain) 676 | 677 | $exportCsvFile = $exportCsvFolder 678 | $exportCsvFile += $Domain 679 | $exportCsvFile += " - Full Output.csv" 680 | OrderPermissionsByAccounts -inputCSV $exportCsvFile -Domain $Domain -domainsPrivilegedAccountDB $domainsPrivilegedAccountDB -privilegedAccountList $privilegedAccountList -exportCsvFolder $exportCsvFolder 681 | } 682 | $exportAllAccCsvFile = $exportCsvFolder 683 | $exportAllAccCsvFile += "Privileged Accounts Permissions - Final Report.csv" 684 | $exportAllIrregularAccCsvFile = $exportCsvFolder + "Privileged Accounts Permissions - Irregular Accounts.csv" 685 | 686 | if (Test-Path $exportAllAccCsvFile) { 687 | Remove-Item $exportAllAccCsvFile 688 | } 689 | if (Test-Path $exportAllIrregularAccCsvFile) { 690 | Remove-Item $exportAllIrregularAccCsvFile 691 | } 692 | foreach ($Domain in $DomainList){ 693 | $exportAccCsvFile = $exportCsvFolder 694 | $exportAccCsvFile += $Domain 695 | $exportAccCsvFile += " - Sensitive Accounts.csv" 696 | $importedCsvData = Import-Csv $exportAccCsvFile 697 | $importedCsvData | sort Domain,AccountName,AccountGroup,ActiveDirectoryRights,ObjectRights,ObjectDN,ObjectOwner,ObjectClassCategory -Unique | Export-Csv -NoTypeInformation -append $exportAllAccCsvFile 698 | $importedCsvData | Where { ($_.AccountGroup -eq $_.AccountName)} | sort Domain,AccountName,AccountGroup,ActiveDirectoryRights,ObjectRights,ObjectDN,ObjectOwner,ObjectClassCategory -Unique | Export-Csv -NoTypeInformation -append $exportAllIrregularAccCsvFile 699 | if (Test-Path $exportAccCsvFile) { 700 | Remove-Item $exportAccCsvFile 701 | } 702 | } 703 | 704 | Write-Host "Finished Account analysis" 705 | #create the final list of the privileged Accounts 706 | $exportListFile = $exportCsvFolder + "Accounts with extra permissions.txt" 707 | $privilegedAccountList | sort | Out-File $exportListFile 708 | $numberAccounts = $privilegedAccountList.count 709 | 710 | Write-host "`nDiscovered $numberAccounts privileged accounts" -ForegroundColor Yellow 711 | Write-host "Check the list of the accounts with extra permissions:`n$exportListFile" 712 | write-host "`nPrivileged ACLs scan completed - the results are in the folder:`n$exportCsvFolder`nCheck the `"Final Report`""-ForegroundColor Yellow 713 | 714 | $exportListFile = $exportCsvFolder + "All entities with extra permissions.txt" 715 | $privilegedAllList | sort | Out-File $exportListFile 716 | 717 | $time.Stop() 718 | $runtime = $time.Elapsed.TotalMilliseconds 719 | $runtime = ($runtime/1000) 720 | $runtimeMin = ($runtime/60) 721 | $runtimeHours = ($runtime/3600) 722 | $runtime = [math]::round($runtime , 2) 723 | $runtimeMin = [math]::round($runtimeMin , 2) 724 | $runtimeHours = [math]::round($runtimeHours , 3) 725 | #Write-Output "`n----------FINISHED----------`n`nTotal time of the scaning: $runtime Second, $runtimeMin Minutes, $runtimeHours Hours" 726 | #Write-Output "Check the results files in the folder: `n$exportCsvFolder `n" 727 | } 728 | else { 729 | Write-Output "`nSorry,`nThe tool need powershell version 3 or higher to perform the efficient Permissions scan`nYou can upgrade the PowerShell version from Microsoft official website:`nhttps://www.microsoft.com/en-us/download/details.aspx?id=34595`n`nFinished without running.`n" 730 | } 731 | } 732 | 733 | 734 | ############################################################### 735 | # # 736 | # Section 2 - functions from PowerView # 737 | # The filter in Invoke-ACLScanner function was modified # 738 | # # 739 | ############################################################### 740 | 741 | function Get-NetUser { 742 | <# 743 | .SYNOPSIS 744 | 745 | Query information for a given user or users in the domain 746 | using ADSI and LDAP. Another -Domain can be specified to 747 | query for users across a trust. 748 | Replacement for "net users /domain" 749 | 750 | .PARAMETER UserName 751 | 752 | Username filter string, wildcards accepted. 753 | 754 | .PARAMETER Domain 755 | 756 | The domain to query for users, defaults to the current domain. 757 | 758 | .PARAMETER DomainController 759 | 760 | Domain controller to reflect LDAP queries through. 761 | 762 | .PARAMETER ADSpath 763 | 764 | The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 765 | Useful for OU queries. 766 | 767 | .PARAMETER Filter 768 | 769 | A customized ldap filter string to use, e.g. "(description=*admin*)" 770 | 771 | .PARAMETER AdminCount 772 | 773 | Switch. Return users with adminCount=1. 774 | 775 | .PARAMETER SPN 776 | 777 | Switch. Only return user objects with non-null service principal names. 778 | 779 | .PARAMETER Unconstrained 780 | 781 | Switch. Return users that have unconstrained delegation. 782 | 783 | .PARAMETER AllowDelegation 784 | 785 | Switch. Return user accounts that are not marked as 'sensitive and not allowed for delegation' 786 | 787 | .PARAMETER PageSize 788 | 789 | The PageSize to set for the LDAP searcher object. 790 | 791 | .EXAMPLE 792 | 793 | PS C:\> Get-NetUser -Domain testing 794 | 795 | .EXAMPLE 796 | 797 | PS C:\> Get-NetUser -ADSpath "LDAP://OU=secret,DC=testlab,DC=local" 798 | #> 799 | 800 | [CmdletBinding()] 801 | param( 802 | [Parameter(ValueFromPipeline=$True)] 803 | [String] 804 | $UserName, 805 | 806 | [String] 807 | $Domain, 808 | 809 | [String] 810 | $DomainController, 811 | 812 | [String] 813 | $ADSpath, 814 | 815 | [String] 816 | $Filter, 817 | 818 | [Switch] 819 | $SPN, 820 | 821 | [Switch] 822 | $AdminCount, 823 | 824 | [Switch] 825 | $Unconstrained, 826 | 827 | [Switch] 828 | $AllowDelegation, 829 | 830 | [ValidateRange(1,10000)] 831 | [Int] 832 | $PageSize = 200 833 | ) 834 | 835 | begin { 836 | # so this isn't repeated if users are passed on the pipeline 837 | $UserSearcher = Get-DomainSearcher -Domain $Domain -ADSpath $ADSpath -DomainController $DomainController -PageSize $PageSize 838 | } 839 | 840 | process { 841 | if($UserSearcher) { 842 | 843 | # if we're checking for unconstrained delegation 844 | if($Unconstrained) { 845 | Write-Verbose "Checking for unconstrained delegation" 846 | $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=524288)" 847 | } 848 | if($AllowDelegation) { 849 | Write-Verbose "Checking for users who can be delegated" 850 | # negation of "Accounts that are sensitive and not trusted for delegation" 851 | $Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=1048574))" 852 | } 853 | if($AdminCount) { 854 | Write-Verbose "Checking for adminCount=1" 855 | $Filter += "(admincount=1)" 856 | } 857 | 858 | # check if we're using a username filter or not 859 | if($UserName) { 860 | # samAccountType=805306368 indicates user objects 861 | $UserSearcher.filter="(&(samAccountType=805306368)(samAccountName=$UserName)$Filter)" 862 | } 863 | elseif($SPN) { 864 | $UserSearcher.filter="(&(samAccountType=805306368)(servicePrincipalName=*)$Filter)" 865 | } 866 | else { 867 | # filter is something like "(samAccountName=*blah*)" if specified 868 | $UserSearcher.filter="(&(samAccountType=805306368)$Filter)" 869 | } 870 | 871 | $UserSearcher.FindAll() | Where-Object {$_} | ForEach-Object { 872 | # convert/process the LDAP fields for each result 873 | Convert-LDAPProperty -Properties $_.Properties 874 | } 875 | } 876 | } 877 | } 878 | 879 | 880 | 881 | function Get-NetForest { 882 | <# 883 | .SYNOPSIS 884 | 885 | Returns a given forest object. 886 | 887 | .PARAMETER Forest 888 | 889 | The forest name to query for, defaults to the current domain. 890 | 891 | .EXAMPLE 892 | 893 | PS C:\> Get-NetForest -Forest external.domain 894 | #> 895 | 896 | [CmdletBinding()] 897 | param( 898 | [Parameter(ValueFromPipeline=$True)] 899 | [String] 900 | $Forest 901 | ) 902 | 903 | process { 904 | if($Forest) { 905 | $ForestContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Forest', $Forest) 906 | try { 907 | $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ForestContext) 908 | } 909 | catch { 910 | Write-Debug "The specified forest $Forest does not exist, could not be contacted, or there isn't an existing trust." 911 | $Null 912 | } 913 | } 914 | else { 915 | # otherwise use the current forest 916 | $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() 917 | } 918 | 919 | if($ForestObject) { 920 | # get the SID of the forest root 921 | $ForestSid = (New-Object System.Security.Principal.NTAccount($ForestObject.RootDomain,"krbtgt")).Translate([System.Security.Principal.SecurityIdentifier]).Value 922 | $Parts = $ForestSid -Split "-" 923 | $ForestSid = $Parts[0..$($Parts.length-2)] -join "-" 924 | $ForestObject | Add-Member NoteProperty 'RootDomainSid' $ForestSid 925 | $ForestObject 926 | } 927 | } 928 | } 929 | 930 | 931 | function Get-NetForestDomain { 932 | <# 933 | .SYNOPSIS 934 | 935 | Return all domains for a given forest. 936 | 937 | .PARAMETER Forest 938 | 939 | The forest name to query domain for. 940 | 941 | .PARAMETER Domain 942 | 943 | Return domains that match this term/wildcard. 944 | 945 | .EXAMPLE 946 | 947 | PS C:\> Get-NetForestDomain 948 | 949 | .EXAMPLE 950 | 951 | PS C:\> Get-NetForestDomain -Forest external.local 952 | #> 953 | 954 | [CmdletBinding()] 955 | param( 956 | [Parameter(ValueFromPipeline=$True)] 957 | [String] 958 | $Forest, 959 | 960 | [String] 961 | $Domain 962 | ) 963 | 964 | process { 965 | if($Domain) { 966 | # try to detect a wild card so we use -like 967 | if($Domain.Contains('*')) { 968 | (Get-NetForest -Forest $Forest).Domains | Where-Object {$_.Name -like $Domain} 969 | } 970 | else { 971 | # match the exact domain name if there's not a wildcard 972 | (Get-NetForest -Forest $Forest).Domains | Where-Object {$_.Name.ToLower() -eq $Domain.ToLower()} 973 | } 974 | } 975 | else { 976 | # return all domains 977 | $ForestObject = Get-NetForest -Forest $Forest 978 | if($ForestObject) { 979 | $ForestObject.Domains 980 | } 981 | } 982 | } 983 | } 984 | 985 | 986 | function Get-NetDomain { 987 | <# 988 | .SYNOPSIS 989 | 990 | Returns a given domain object. 991 | 992 | .PARAMETER Domain 993 | 994 | The domain name to query for, defaults to the current domain. 995 | 996 | .EXAMPLE 997 | 998 | PS C:\> Get-NetDomain -Domain testlab.local 999 | 1000 | .LINK 1001 | 1002 | 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 1003 | #> 1004 | 1005 | [CmdletBinding()] 1006 | param( 1007 | [Parameter(ValueFromPipeline=$True)] 1008 | [String] 1009 | $Domain 1010 | ) 1011 | 1012 | process { 1013 | if($Domain) { 1014 | $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $Domain) 1015 | try { 1016 | [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) 1017 | } 1018 | catch { 1019 | Write-Warning "The specified domain $Domain does not exist, could not be contacted, or there isn't an existing trust." 1020 | $Null 1021 | } 1022 | } 1023 | else { 1024 | [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() 1025 | } 1026 | } 1027 | } 1028 | 1029 | 1030 | function Get-NetGroup { 1031 | <# 1032 | .SYNOPSIS 1033 | 1034 | Gets a list of all current groups in a domain, or all 1035 | the groups a given user/group object belongs to. 1036 | 1037 | .PARAMETER GroupName 1038 | 1039 | The group name to query for, wildcards accepted. 1040 | 1041 | .PARAMETER SID 1042 | 1043 | The group SID to query for. 1044 | 1045 | .PARAMETER UserName 1046 | 1047 | The user name (or group name) to query for all effective 1048 | groups of. 1049 | 1050 | .PARAMETER Filter 1051 | 1052 | A customized ldap filter string to use, e.g. "(description=*admin*)" 1053 | 1054 | .PARAMETER Domain 1055 | 1056 | The domain to query for groups, defaults to the current domain. 1057 | 1058 | .PARAMETER DomainController 1059 | 1060 | Domain controller to reflect LDAP queries through. 1061 | 1062 | .PARAMETER ADSpath 1063 | 1064 | The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 1065 | Useful for OU queries. 1066 | 1067 | .PARAMETER AdminCount 1068 | 1069 | Switch. Return group with adminCount=1. 1070 | 1071 | .PARAMETER FullData 1072 | 1073 | Switch. Return full group objects instead of just object names (the default). 1074 | 1075 | .PARAMETER RawSids 1076 | 1077 | Switch. Return raw SIDs when using "Get-NetGroup -UserName X" 1078 | 1079 | .PARAMETER PageSize 1080 | 1081 | The PageSize to set for the LDAP searcher object. 1082 | 1083 | .EXAMPLE 1084 | 1085 | PS C:\> Get-NetGroup 1086 | 1087 | Returns the current groups in the domain. 1088 | 1089 | .EXAMPLE 1090 | 1091 | PS C:\> Get-NetGroup -GroupName *admin* 1092 | 1093 | Returns all groups with "admin" in their group name. 1094 | 1095 | .EXAMPLE 1096 | 1097 | PS C:\> Get-NetGroup -Domain testing -FullData 1098 | 1099 | Returns full group data objects in the 'testing' domain 1100 | #> 1101 | 1102 | [CmdletBinding()] 1103 | param( 1104 | [Parameter(ValueFromPipeline=$True)] 1105 | [String] 1106 | $GroupName = '*', 1107 | 1108 | [String] 1109 | $SID, 1110 | 1111 | [String] 1112 | $UserName, 1113 | 1114 | [String] 1115 | $Filter, 1116 | 1117 | [String] 1118 | $Domain, 1119 | 1120 | [String] 1121 | $DomainController, 1122 | 1123 | [String] 1124 | $ADSpath, 1125 | 1126 | [Switch] 1127 | $AdminCount, 1128 | 1129 | [Switch] 1130 | $FullData, 1131 | 1132 | [Switch] 1133 | $RawSids, 1134 | 1135 | [ValidateRange(1,10000)] 1136 | [Int] 1137 | $PageSize = 200 1138 | ) 1139 | 1140 | begin { 1141 | $GroupSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize 1142 | } 1143 | 1144 | process { 1145 | if($GroupSearcher) { 1146 | 1147 | if($AdminCount) { 1148 | Write-Verbose "Checking for adminCount=1" 1149 | $Filter += "(admincount=1)" 1150 | } 1151 | 1152 | if ($UserName) { 1153 | # get the raw user object 1154 | $User = Get-ADObject -SamAccountName $UserName -Domain $Domain -DomainController $DomainController -ReturnRaw -PageSize $PageSize 1155 | 1156 | # convert the user to a directory entry 1157 | $UserDirectoryEntry = $User.GetDirectoryEntry() 1158 | 1159 | # cause the cache to calculate the token groups for the user 1160 | $UserDirectoryEntry.RefreshCache("tokenGroups") 1161 | 1162 | $UserDirectoryEntry.TokenGroups | Foreach-Object { 1163 | # convert the token group sid 1164 | $GroupSid = (New-Object System.Security.Principal.SecurityIdentifier($_,0)).Value 1165 | 1166 | # ignore the built in users and default domain user group 1167 | if(!($GroupSid -match '^S-1-5-32-545|-513$')) { 1168 | if($FullData) { 1169 | Get-ADObject -SID $GroupSid -PageSize $PageSize 1170 | } 1171 | else { 1172 | if($RawSids) { 1173 | $GroupSid 1174 | } 1175 | else { 1176 | Convert-SidToName $GroupSid 1177 | } 1178 | } 1179 | } 1180 | } 1181 | } 1182 | else { 1183 | if ($SID) { 1184 | $GroupSearcher.filter = "(&(objectCategory=group)(objectSID=$SID)$Filter)" 1185 | } 1186 | else { 1187 | $GroupSearcher.filter = "(&(objectCategory=group)(name=$GroupName)$Filter)" 1188 | } 1189 | 1190 | $GroupSearcher.FindAll() | Where-Object {$_} | ForEach-Object { 1191 | # if we're returning full data objects 1192 | if ($FullData) { 1193 | # convert/process the LDAP fields for each result 1194 | Convert-LDAPProperty -Properties $_.Properties 1195 | } 1196 | else { 1197 | # otherwise we're just returning the group name 1198 | $_.properties.samaccountname 1199 | } 1200 | } 1201 | } 1202 | } 1203 | } 1204 | } 1205 | 1206 | 1207 | function Get-NetComputer { 1208 | <# 1209 | .SYNOPSIS 1210 | 1211 | This function utilizes adsisearcher to query the current AD context 1212 | for current computer objects. Based off of Carlos Perez's Audit.psm1 1213 | script in Posh-SecMod (link below). 1214 | 1215 | .PARAMETER ComputerName 1216 | 1217 | Return computers with a specific name, wildcards accepted. 1218 | 1219 | .PARAMETER SPN 1220 | 1221 | Return computers with a specific service principal name, wildcards accepted. 1222 | 1223 | .PARAMETER OperatingSystem 1224 | 1225 | Return computers with a specific operating system, wildcards accepted. 1226 | 1227 | .PARAMETER ServicePack 1228 | 1229 | Return computers with a specific service pack, wildcards accepted. 1230 | 1231 | .PARAMETER Filter 1232 | 1233 | A customized ldap filter string to use, e.g. "(description=*admin*)" 1234 | 1235 | .PARAMETER Printers 1236 | 1237 | Switch. Return only printers. 1238 | 1239 | .PARAMETER Ping 1240 | 1241 | Switch. Ping each host to ensure it's up before enumerating. 1242 | 1243 | .PARAMETER FullData 1244 | 1245 | Switch. Return full computer objects instead of just system names (the default). 1246 | 1247 | .PARAMETER Domain 1248 | 1249 | The domain to query for computers, defaults to the current domain. 1250 | 1251 | .PARAMETER DomainController 1252 | 1253 | Domain controller to reflect LDAP queries through. 1254 | 1255 | .PARAMETER ADSpath 1256 | 1257 | The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 1258 | Useful for OU queries. 1259 | 1260 | .PARAMETER Unconstrained 1261 | 1262 | Switch. Return computer objects that have unconstrained delegation. 1263 | 1264 | .PARAMETER PageSize 1265 | 1266 | The PageSize to set for the LDAP searcher object. 1267 | 1268 | .EXAMPLE 1269 | 1270 | PS C:\> Get-NetComputer 1271 | 1272 | Returns the current computers in current domain. 1273 | 1274 | .EXAMPLE 1275 | 1276 | PS C:\> Get-NetComputer -SPN mssql* 1277 | 1278 | Returns all MS SQL servers on the domain. 1279 | 1280 | .EXAMPLE 1281 | 1282 | PS C:\> Get-NetComputer -Domain testing 1283 | 1284 | Returns the current computers in 'testing' domain. 1285 | 1286 | .EXAMPLE 1287 | 1288 | PS C:\> Get-NetComputer -Domain testing -FullData 1289 | 1290 | Returns full computer objects in the 'testing' domain. 1291 | 1292 | .LINK 1293 | 1294 | https://github.com/darkoperator/Posh-SecMod/blob/master/Audit/Audit.psm1 1295 | #> 1296 | 1297 | [CmdletBinding()] 1298 | Param ( 1299 | [Parameter(ValueFromPipeline=$True)] 1300 | [Alias('HostName')] 1301 | [String] 1302 | $ComputerName = '*', 1303 | 1304 | [String] 1305 | $SPN, 1306 | 1307 | [String] 1308 | $OperatingSystem, 1309 | 1310 | [String] 1311 | $ServicePack, 1312 | 1313 | [String] 1314 | $Filter, 1315 | 1316 | [Switch] 1317 | $Printers, 1318 | 1319 | [Switch] 1320 | $Ping, 1321 | 1322 | [Switch] 1323 | $FullData, 1324 | 1325 | [String] 1326 | $Domain, 1327 | 1328 | [String] 1329 | $DomainController, 1330 | 1331 | [String] 1332 | $ADSpath, 1333 | 1334 | [Switch] 1335 | $Unconstrained, 1336 | 1337 | [ValidateRange(1,10000)] 1338 | [Int] 1339 | $PageSize = 200 1340 | ) 1341 | 1342 | begin { 1343 | # so this isn't repeated if users are passed on the pipeline 1344 | $CompSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize 1345 | } 1346 | 1347 | process { 1348 | 1349 | if ($CompSearcher) { 1350 | 1351 | # if we're checking for unconstrained delegation 1352 | if($Unconstrained) { 1353 | Write-Verbose "Searching for computers with for unconstrained delegation" 1354 | $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=524288)" 1355 | } 1356 | # set the filters for the seracher if it exists 1357 | if($Printers) { 1358 | Write-Verbose "Searching for printers" 1359 | # $CompSearcher.filter="(&(objectCategory=printQueue)$Filter)" 1360 | $Filter += "(objectCategory=printQueue)" 1361 | } 1362 | if($SPN) { 1363 | Write-Verbose "Searching for computers with SPN: $SPN" 1364 | $Filter += "(servicePrincipalName=$SPN)" 1365 | } 1366 | if($OperatingSystem) { 1367 | $Filter += "(operatingsystem=$OperatingSystem)" 1368 | } 1369 | if($ServicePack) { 1370 | $Filter += "(operatingsystemservicepack=$ServicePack)" 1371 | } 1372 | 1373 | $CompSearcher.filter = "(&(sAMAccountType=805306369)(dnshostname=$ComputerName)$Filter)" 1374 | 1375 | try { 1376 | 1377 | $CompSearcher.FindAll() | Where-Object {$_} | ForEach-Object { 1378 | $Up = $True 1379 | if($Ping) { 1380 | # TODO: how can these results be piped to ping for a speedup? 1381 | $Up = Test-Connection -Count 1 -Quiet -ComputerName $_.properties.dnshostname 1382 | } 1383 | if($Up) { 1384 | # return full data objects 1385 | if ($FullData) { 1386 | # convert/process the LDAP fields for each result 1387 | Convert-LDAPProperty -Properties $_.Properties 1388 | } 1389 | else { 1390 | # otherwise we're just returning the DNS host name 1391 | $_.properties.dnshostname 1392 | } 1393 | } 1394 | } 1395 | } 1396 | catch { 1397 | Write-Warning "Error: $_" 1398 | } 1399 | } 1400 | } 1401 | } 1402 | 1403 | 1404 | function Get-DomainSID { 1405 | <# 1406 | .SYNOPSIS 1407 | 1408 | Gets the SID for the domain. 1409 | 1410 | .PARAMETER Domain 1411 | 1412 | The domain to query, defaults to the current domain. 1413 | 1414 | .EXAMPLE 1415 | 1416 | C:\> Get-DomainSID -Domain TEST 1417 | 1418 | Returns SID for the domain 'TEST' 1419 | #> 1420 | 1421 | param( 1422 | [String] 1423 | $Domain 1424 | ) 1425 | 1426 | $FoundDomain = Get-NetDomain -Domain $Domain 1427 | 1428 | if($FoundDomain) { 1429 | # query for the primary domain controller so we can extract the domain SID for filtering 1430 | $PrimaryDC = $FoundDomain.PdcRoleOwner 1431 | $PrimaryDCSID = (Get-NetComputer -Domain $Domain -ComputerName $PrimaryDC -FullData).objectsid 1432 | $Parts = $PrimaryDCSID.split("-") 1433 | $Parts[0..($Parts.length -2)] -join "-" 1434 | } 1435 | } 1436 | 1437 | 1438 | 1439 | 1440 | function Convert-SidToName { 1441 | <# 1442 | .SYNOPSIS 1443 | 1444 | Converts a security identifier (SID) to a group/user name. 1445 | 1446 | .PARAMETER SID 1447 | 1448 | The SID to convert. 1449 | 1450 | .EXAMPLE 1451 | 1452 | PS C:\> Convert-SidToName S-1-5-21-2620891829-2411261497-1773853088-1105 1453 | #> 1454 | [CmdletBinding()] 1455 | param( 1456 | [Parameter(Mandatory=$True,ValueFromPipeline=$True)] 1457 | [String] 1458 | $SID 1459 | ) 1460 | 1461 | process { 1462 | try { 1463 | $SID2 = $SID.trim('*') 1464 | 1465 | # try to resolve any built-in SIDs first 1466 | # from https://support.microsoft.com/en-us/kb/243330 1467 | Switch ($SID2) 1468 | { 1469 | 'S-1-0' { 'Null Authority' } 1470 | 'S-1-0-0' { 'Nobody' } 1471 | 'S-1-1' { 'World Authority' } 1472 | 'S-1-1-0' { 'Everyone' } 1473 | 'S-1-2' { 'Local Authority' } 1474 | 'S-1-2-0' { 'Local' } 1475 | 'S-1-2-1' { 'Console Logon ' } 1476 | 'S-1-3' { 'Creator Authority' } 1477 | 'S-1-3-0' { 'Creator Owner' } 1478 | 'S-1-3-1' { 'Creator Group' } 1479 | 'S-1-3-2' { 'Creator Owner Server' } 1480 | 'S-1-3-3' { 'Creator Group Server' } 1481 | 'S-1-3-4' { 'Owner Rights' } 1482 | 'S-1-4' { 'Non-unique Authority' } 1483 | 'S-1-5' { 'NT Authority' } 1484 | 'S-1-5-1' { 'Dialup' } 1485 | 'S-1-5-2' { 'Network' } 1486 | 'S-1-5-3' { 'Batch' } 1487 | 'S-1-5-4' { 'Interactive' } 1488 | 'S-1-5-6' { 'Service' } 1489 | 'S-1-5-7' { 'Anonymous' } 1490 | 'S-1-5-8' { 'Proxy' } 1491 | 'S-1-5-9' { 'Enterprise Domain Controllers' } 1492 | 'S-1-5-10' { 'Principal Self' } 1493 | 'S-1-5-11' { 'Authenticated Users' } 1494 | 'S-1-5-12' { 'Restricted Code' } 1495 | 'S-1-5-13' { 'Terminal Server Users' } 1496 | 'S-1-5-14' { 'Remote Interactive Logon' } 1497 | 'S-1-5-15' { 'This Organization ' } 1498 | 'S-1-5-17' { 'This Organization ' } 1499 | 'S-1-5-18' { 'Local System' } 1500 | 'S-1-5-19' { 'NT Authority' } 1501 | 'S-1-5-20' { 'NT Authority' } 1502 | 'S-1-5-80-0' { 'All Services ' } 1503 | 'S-1-5-32-544' { 'BUILTIN\Administrators' } 1504 | 'S-1-5-32-545' { 'BUILTIN\Users' } 1505 | 'S-1-5-32-546' { 'BUILTIN\Guests' } 1506 | 'S-1-5-32-547' { 'BUILTIN\Power Users' } 1507 | 'S-1-5-32-548' { 'BUILTIN\Account Operators' } 1508 | 'S-1-5-32-549' { 'BUILTIN\Server Operators' } 1509 | 'S-1-5-32-550' { 'BUILTIN\Print Operators' } 1510 | 'S-1-5-32-551' { 'BUILTIN\Backup Operators' } 1511 | 'S-1-5-32-552' { 'BUILTIN\Replicators' } 1512 | 'S-1-5-32-554' { 'BUILTIN\Pre-Windows 2000 Compatible Access' } 1513 | 'S-1-5-32-555' { 'BUILTIN\Remote Desktop Users' } 1514 | 'S-1-5-32-556' { 'BUILTIN\Network Configuration Operators' } 1515 | 'S-1-5-32-557' { 'BUILTIN\Incoming Forest Trust Builders' } 1516 | 'S-1-5-32-558' { 'BUILTIN\Performance Monitor Users' } 1517 | 'S-1-5-32-559' { 'BUILTIN\Performance Log Users' } 1518 | 'S-1-5-32-560' { 'BUILTIN\Windows Authorization Access Group' } 1519 | 'S-1-5-32-561' { 'BUILTIN\Terminal Server License Servers' } 1520 | 'S-1-5-32-562' { 'BUILTIN\Distributed COM Users' } 1521 | 'S-1-5-32-569' { 'BUILTIN\Cryptographic Operators' } 1522 | 'S-1-5-32-573' { 'BUILTIN\Event Log Readers' } 1523 | 'S-1-5-32-574' { 'BUILTIN\Certificate Service DCOM Access' } 1524 | 'S-1-5-32-575' { 'BUILTIN\RDS Remote Access Servers' } 1525 | 'S-1-5-32-576' { 'BUILTIN\RDS Endpoint Servers' } 1526 | 'S-1-5-32-577' { 'BUILTIN\RDS Management Servers' } 1527 | 'S-1-5-32-578' { 'BUILTIN\Hyper-V Administrators' } 1528 | 'S-1-5-32-579' { 'BUILTIN\Access Control Assistance Operators' } 1529 | 'S-1-5-32-580' { 'BUILTIN\Access Control Assistance Operators' } 1530 | Default { 1531 | $Obj = (New-Object System.Security.Principal.SecurityIdentifier($SID2)) 1532 | $Obj.Translate( [System.Security.Principal.NTAccount]).Value 1533 | } 1534 | } 1535 | } 1536 | catch { 1537 | # Write-Warning "Invalid SID: $SID" 1538 | $SID 1539 | } 1540 | } 1541 | } 1542 | 1543 | 1544 | function Convert-NameToSid { 1545 | <# 1546 | .SYNOPSIS 1547 | 1548 | Converts a given user/group name to a security identifier (SID). 1549 | 1550 | .PARAMETER ObjectName 1551 | 1552 | The user/group name to convert, can be 'user' or 'DOMAIN\user' format. 1553 | 1554 | .PARAMETER Domain 1555 | 1556 | Specific domain for the given user account, defaults to the current domain. 1557 | 1558 | .EXAMPLE 1559 | 1560 | PS C:\> Convert-NameToSid 'DEV\dfm' 1561 | #> 1562 | [CmdletBinding()] 1563 | param( 1564 | [Parameter(Mandatory=$True,ValueFromPipeline=$True)] 1565 | [String] 1566 | [Alias('Name')] 1567 | $ObjectName, 1568 | 1569 | [String] 1570 | $Domain = (Get-NetDomain).Name 1571 | ) 1572 | 1573 | process { 1574 | 1575 | $ObjectName = $ObjectName -replace "/","\" 1576 | 1577 | if($ObjectName.contains("\")) { 1578 | # if we get a DOMAIN\user format, auto convert it 1579 | $Domain = $ObjectName.split("\")[0] 1580 | $ObjectName = $ObjectName.split("\")[1] 1581 | } 1582 | 1583 | try { 1584 | $Obj = (New-Object System.Security.Principal.NTAccount($Domain,$ObjectName)) 1585 | $Obj.Translate([System.Security.Principal.SecurityIdentifier]).Value 1586 | } 1587 | catch { 1588 | Write-Verbose "Invalid object/name: $Domain\$ObjectName" 1589 | $Null 1590 | } 1591 | } 1592 | } 1593 | 1594 | 1595 | function Convert-LDAPProperty { 1596 | # helper to convert specific LDAP property result fields 1597 | param( 1598 | [Parameter(Mandatory=$True,ValueFromPipeline=$True)] 1599 | [ValidateNotNullOrEmpty()] 1600 | $Properties 1601 | ) 1602 | 1603 | $ObjectProperties = @{} 1604 | 1605 | $Properties.PropertyNames | ForEach-Object { 1606 | if (($_ -eq "objectsid") -or ($_ -eq "sidhistory")) { 1607 | # convert the SID to a string 1608 | $ObjectProperties[$_] = (New-Object System.Security.Principal.SecurityIdentifier($Properties[$_][0],0)).Value 1609 | } 1610 | elseif($_ -eq "objectguid") { 1611 | # convert the GUID to a string 1612 | $ObjectProperties[$_] = (New-Object Guid (,$Properties[$_][0])).Guid 1613 | } 1614 | elseif( ($_ -eq "lastlogon") -or ($_ -eq "lastlogontimestamp") -or ($_ -eq "pwdlastset") -or ($_ -eq "lastlogoff") -or ($_ -eq "badPasswordTime") ) { 1615 | # convert timestamps 1616 | if ($Properties[$_][0] -is [System.MarshalByRefObject]) { 1617 | # if we have a System.__ComObject 1618 | $Temp = $Properties[$_][0] 1619 | [Int32]$High = $Temp.GetType().InvokeMember("HighPart", [System.Reflection.BindingFlags]::GetProperty, $null, $Temp, $null) 1620 | [Int32]$Low = $Temp.GetType().InvokeMember("LowPart", [System.Reflection.BindingFlags]::GetProperty, $null, $Temp, $null) 1621 | $ObjectProperties[$_] = ([datetime]::FromFileTime([Int64]("0x{0:x8}{1:x8}" -f $High, $Low))) 1622 | } 1623 | else { 1624 | $ObjectProperties[$_] = ([datetime]::FromFileTime(($Properties[$_][0]))) 1625 | } 1626 | } 1627 | elseif($Properties[$_][0] -is [System.MarshalByRefObject]) { 1628 | # convert misc com objects 1629 | $Prop = $Properties[$_] 1630 | try { 1631 | $Temp = $Prop[$_][0] 1632 | Write-Verbose $_ 1633 | [Int32]$High = $Temp.GetType().InvokeMember("HighPart", [System.Reflection.BindingFlags]::GetProperty, $null, $Temp, $null) 1634 | [Int32]$Low = $Temp.GetType().InvokeMember("LowPart", [System.Reflection.BindingFlags]::GetProperty, $null, $Temp, $null) 1635 | $ObjectProperties[$_] = [Int64]("0x{0:x8}{1:x8}" -f $High, $Low) 1636 | } 1637 | catch { 1638 | $ObjectProperties[$_] = $Prop[$_] 1639 | } 1640 | } 1641 | elseif($Properties[$_].count -eq 1) { 1642 | $ObjectProperties[$_] = $Properties[$_][0] 1643 | } 1644 | else { 1645 | $ObjectProperties[$_] = $Properties[$_] 1646 | } 1647 | } 1648 | 1649 | New-Object -TypeName PSObject -Property $ObjectProperties 1650 | } 1651 | 1652 | 1653 | 1654 | 1655 | function Get-NetGroupMember { 1656 | <# 1657 | .SYNOPSIS 1658 | 1659 | This function users [ADSI] and LDAP to query the current AD context 1660 | or trusted domain for users in a specified group. If no GroupName is 1661 | specified, it defaults to querying the "Domain Admins" group. 1662 | This is a replacement for "net group 'name' /domain" 1663 | 1664 | .PARAMETER GroupName 1665 | 1666 | The group name to query for users. 1667 | 1668 | .PARAMETER SID 1669 | 1670 | The Group SID to query for users. If not given, it defaults to 512 "Domain Admins" 1671 | 1672 | .PARAMETER Filter 1673 | 1674 | A customized ldap filter string to use, e.g. "(description=*admin*)" 1675 | 1676 | .PARAMETER Domain 1677 | 1678 | The domain to query for group users, defaults to the current domain. 1679 | 1680 | .PARAMETER DomainController 1681 | 1682 | Domain controller to reflect LDAP queries through. 1683 | 1684 | .PARAMETER ADSpath 1685 | 1686 | The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 1687 | Useful for OU queries. 1688 | 1689 | .PARAMETER FullData 1690 | 1691 | Switch. Returns full data objects instead of just group/users. 1692 | 1693 | .PARAMETER Recurse 1694 | 1695 | Switch. If the group member is a group, recursively try to query its members as well. 1696 | 1697 | .PARAMETER UseMatchingRule 1698 | 1699 | Switch. Use LDAP_MATCHING_RULE_IN_CHAIN in the LDAP search query when -Recurse is specified. 1700 | Much faster than manual recursion, but doesn't reveal cross-domain groups. 1701 | 1702 | .PARAMETER PageSize 1703 | 1704 | The PageSize to set for the LDAP searcher object. 1705 | 1706 | .EXAMPLE 1707 | 1708 | PS C:\> Get-NetGroupMember 1709 | 1710 | Returns the usernames that of members of the "Domain Admins" domain group. 1711 | 1712 | .EXAMPLE 1713 | 1714 | PS C:\> Get-NetGroupMember -Domain testing -GroupName "Power Users" 1715 | 1716 | Returns the usernames that of members of the "Power Users" group in the 'testing' domain. 1717 | 1718 | .LINK 1719 | 1720 | http://www.powershellmagazine.com/2013/05/23/pstip-retrieve-group-membership-of-an-active-directory-group-recursively/ 1721 | #> 1722 | 1723 | [CmdletBinding()] 1724 | param( 1725 | [Parameter(ValueFromPipeline=$True)] 1726 | [String] 1727 | $GroupName, 1728 | 1729 | [String] 1730 | $SID, 1731 | 1732 | [String] 1733 | $Domain = (Get-NetDomain).Name, 1734 | 1735 | [String] 1736 | $DomainController, 1737 | 1738 | [String] 1739 | $ADSpath, 1740 | 1741 | [Switch] 1742 | $FullData, 1743 | 1744 | [Switch] 1745 | $Recurse, 1746 | 1747 | [Switch] 1748 | $UseMatchingRule, 1749 | 1750 | [ValidateRange(1,10000)] 1751 | [Int] 1752 | $PageSize = 200 1753 | ) 1754 | 1755 | begin { 1756 | # so this isn't repeated if users are passed on the pipeline 1757 | $GroupSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize 1758 | 1759 | if(!$DomainController) { 1760 | $DomainController = ((Get-NetDomain).PdcRoleOwner).Name 1761 | } 1762 | } 1763 | 1764 | process { 1765 | ## 1766 | #$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')) 1767 | if ($GroupSearcher) { 1768 | 1769 | if ($Recurse -and $UseMatchingRule) { 1770 | # resolve the group to a distinguishedname 1771 | if ($GroupName) { 1772 | $Group = Get-NetGroup -GroupName $GroupName -Domain $Domain -FullData -PageSize $PageSize 1773 | } 1774 | elseif ($SID) { 1775 | $Group = Get-NetGroup -SID $SID -Domain $Domain -FullData -PageSize $PageSize 1776 | } 1777 | else { 1778 | # default to domain admins 1779 | $SID = (Get-DomainSID -Domain $Domain) + "-512" 1780 | $Group = Get-NetGroup -SID $SID -Domain $Domain -FullData -PageSize $PageSize 1781 | } 1782 | $GroupDN = $Group.distinguishedname 1783 | $GroupFoundName = $Group.name 1784 | 1785 | if ($GroupDN) { 1786 | $GroupSearcher.filter = "(&(samAccountType=805306368)(memberof:1.2.840.113556.1.4.1941:=$GroupDN)$Filter)" 1787 | #updated 1788 | $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')) 1789 | #$GroupSearcher.PropertiesToLoad.AddRange(('distinguishedName','samaccounttype','objectsid','name','cn','objectclass','useraccountcontrol','objectguid','memberof','adspath')) 1790 | 1791 | $Members = $GroupSearcher.FindAll() 1792 | $GroupFoundName = $GroupName 1793 | } 1794 | else { 1795 | Write-Error "Unable to find Group" 1796 | } 1797 | } 1798 | else { 1799 | if ($GroupName) { 1800 | $GroupSearcher.filter = "(&(objectCategory=group)(name=$GroupName)$Filter)" 1801 | } 1802 | elseif ($SID) { 1803 | $GroupSearcher.filter = "(&(objectCategory=group)(objectSID=$SID)$Filter)" 1804 | } 1805 | else { 1806 | # default to domain admins 1807 | $SID = (Get-DomainSID -Domain $Domain) + "-512" 1808 | $GroupSearcher.filter = "(&(objectCategory=group)(objectSID=$SID)$Filter)" 1809 | } 1810 | 1811 | $GroupSearcher.FindAll() | ForEach-Object { 1812 | try { 1813 | if (!($_) -or !($_.properties) -or !($_.properties.name)) { continue } 1814 | 1815 | $GroupFoundName = $_.properties.name[0] 1816 | $Members = @() 1817 | 1818 | if ($_.properties.member.Count -eq 0) { 1819 | $Finished = $False 1820 | $Bottom = 0 1821 | $Top = 0 1822 | while(!$Finished) { 1823 | $Top = $Bottom + 1499 1824 | $MemberRange="member;range=$Bottom-$Top" 1825 | $Bottom += 1500 1826 | $GroupSearcher.PropertiesToLoad.Clear() 1827 | [void]$GroupSearcher.PropertiesToLoad.Add("$MemberRange") 1828 | try { 1829 | $Result = $GroupSearcher.FindOne() 1830 | if ($Result) { 1831 | $RangedProperty = $_.Properties.PropertyNames -like "member;range=*" 1832 | $Results = $_.Properties.item($RangedProperty) 1833 | if ($Results.count -eq 0) { 1834 | $Finished = $True 1835 | } 1836 | else { 1837 | $Results | ForEach-Object { 1838 | $Members += $_ 1839 | } 1840 | } 1841 | } 1842 | else { 1843 | $Finished = $True 1844 | } 1845 | } 1846 | catch [System.Management.Automation.MethodInvocationException] { 1847 | $Finished = $True 1848 | } 1849 | } 1850 | } 1851 | else { 1852 | $Members = $_.properties.member 1853 | } 1854 | } 1855 | catch { 1856 | Write-Verbose $_ 1857 | } 1858 | } 1859 | } 1860 | 1861 | $Members | Where-Object {$_} | ForEach-Object { 1862 | # if we're doing the LDAP_MATCHING_RULE_IN_CHAIN recursion 1863 | if ($Recurse -and $UseMatchingRule) { 1864 | $Properties = $_.Properties 1865 | } 1866 | else { 1867 | if($DomainController) { 1868 | $Result = [adsi]"LDAP://$DomainController/$_" 1869 | } 1870 | else { 1871 | $Result = [adsi]"LDAP://$_" 1872 | } 1873 | if($Result){ 1874 | $Properties = $Result.Properties 1875 | } 1876 | } 1877 | 1878 | if($Properties) { 1879 | 1880 | if($Properties.samaccounttype -notmatch '805306368') { 1881 | $IsGroup = $True 1882 | } 1883 | else { 1884 | $IsGroup = $False 1885 | } 1886 | 1887 | if ($FullData) { 1888 | $GroupMember = Convert-LDAPProperty -Properties $Properties 1889 | } 1890 | else { 1891 | $GroupMember = New-Object PSObject 1892 | } 1893 | 1894 | $GroupMember | Add-Member Noteproperty 'GroupDomain' $Domain 1895 | $GroupMember | Add-Member Noteproperty 'GroupName' $GroupFoundName 1896 | 1897 | try { 1898 | $MemberDN = $Properties.distinguishedname[0] 1899 | 1900 | # extract the FQDN from the Distinguished Name 1901 | $MemberDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' 1902 | } 1903 | catch { 1904 | $MemberDN = $Null 1905 | $MemberDomain = $Null 1906 | } 1907 | 1908 | if ($Properties.samaccountname) { 1909 | # forest users have the samAccountName set 1910 | $MemberName = $Properties.samaccountname[0] 1911 | } 1912 | else { 1913 | # external trust users have a SID, so convert it 1914 | try { 1915 | $MemberName = Convert-SidToName $Properties.cn[0] 1916 | } 1917 | catch { 1918 | # if there's a problem contacting the domain to resolve the SID 1919 | $MemberName = $Properties.cn 1920 | } 1921 | } 1922 | 1923 | if($Properties.objectSid) { 1924 | $MemberSid = ((New-Object System.Security.Principal.SecurityIdentifier $Properties.objectSid[0],0).Value) 1925 | } 1926 | else { 1927 | $MemberSid = $Null 1928 | } 1929 | 1930 | $GroupMember | Add-Member Noteproperty 'MemberDomain' $MemberDomain 1931 | $GroupMember | Add-Member Noteproperty 'MemberName' $MemberName 1932 | $GroupMember | Add-Member Noteproperty 'MemberSid' $MemberSid 1933 | $GroupMember | Add-Member Noteproperty 'IsGroup' $IsGroup 1934 | $GroupMember | Add-Member Noteproperty 'MemberDN' $MemberDN 1935 | $GroupMember 1936 | 1937 | # if we're doing manual recursion 1938 | if ($Recurse -and !$UseMatchingRule -and $IsGroup -and $MemberName) { 1939 | Get-NetGroupMember -FullData -Domain $MemberDomain -DomainController $DomainController -GroupName $MemberName -Recurse -PageSize $PageSize 1940 | } 1941 | } 1942 | 1943 | } 1944 | } 1945 | } 1946 | } 1947 | 1948 | 1949 | function Get-DomainSearcher { 1950 | <# 1951 | .SYNOPSIS 1952 | 1953 | Helper used by various functions that takes an ADSpath and 1954 | domain specifier and builds the correct ADSI searcher object. 1955 | 1956 | .PARAMETER Domain 1957 | 1958 | The domain to use for the query, defaults to the current domain. 1959 | 1960 | .PARAMETER DomainController 1961 | 1962 | Domain controller to reflect LDAP queries through. 1963 | 1964 | .PARAMETER ADSpath 1965 | 1966 | The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 1967 | Useful for OU queries. 1968 | 1969 | .PARAMETER ADSprefix 1970 | 1971 | Prefix to set for the searcher (like "CN=Sites,CN=Configuration") 1972 | 1973 | .PARAMETER PageSize 1974 | 1975 | The PageSize to set for the LDAP searcher object. 1976 | 1977 | .EXAMPLE 1978 | 1979 | PS C:\> Get-DomainSearcher -Domain testlab.local 1980 | 1981 | .EXAMPLE 1982 | 1983 | PS C:\> Get-DomainSearcher -Domain testlab.local -DomainController SECONDARY.dev.testlab.local 1984 | #> 1985 | 1986 | [CmdletBinding()] 1987 | param( 1988 | [String] 1989 | $Domain, 1990 | 1991 | [String] 1992 | $DomainController, 1993 | 1994 | [String] 1995 | $ADSpath, 1996 | 1997 | [String] 1998 | $ADSprefix, 1999 | 2000 | [ValidateRange(1,10000)] 2001 | [Int] 2002 | $PageSize = 200 2003 | ) 2004 | 2005 | if(!$Domain) { 2006 | $Domain = (Get-NetDomain).name 2007 | } 2008 | else { 2009 | if(!$DomainController) { 2010 | try { 2011 | # if there's no -DomainController specified, try to pull the primary DC 2012 | # to reflect queries through 2013 | $DomainController = ((Get-NetDomain).PdcRoleOwner).Name 2014 | } 2015 | catch { 2016 | throw "Get-DomainSearcher: Error in retrieving PDC for current domain" 2017 | } 2018 | } 2019 | } 2020 | 2021 | $SearchString = "LDAP://" 2022 | 2023 | if($DomainController) { 2024 | $SearchString += $DomainController + "/" 2025 | } 2026 | if($ADSprefix) { 2027 | $SearchString += $ADSprefix + "," 2028 | } 2029 | 2030 | if($ADSpath) { 2031 | if($ADSpath -like "GC://*") { 2032 | # if we're searching the global catalog 2033 | $DistinguishedName = $AdsPath 2034 | $SearchString = "" 2035 | } 2036 | else { 2037 | if($ADSpath -like "LDAP://*") { 2038 | $ADSpath = $ADSpath.Substring(7) 2039 | } 2040 | $DistinguishedName = $ADSpath 2041 | } 2042 | } 2043 | else { 2044 | $DistinguishedName = "DC=$($Domain.Replace('.', ',DC='))" 2045 | } 2046 | 2047 | $SearchString += $DistinguishedName 2048 | Write-Verbose "Get-DomainSearcher search string: $SearchString" 2049 | 2050 | $Searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]$SearchString) 2051 | $Searcher.PageSize = $PageSize 2052 | <#new - to search only specific properties 2053 | $Properies = "samaccountname","displayname", "SID", "userprincipalname", "memberof","ObjectClass","objectsid", "objectguid","distinguishedName","name","cn", "dnshostname" 2054 | foreach ($Property in $Properies) 2055 | { 2056 | $Searcher.PropertiesToLoad.Add($Property) | Out-Null 2057 | } 2058 | #> 2059 | $Searcher 2060 | } 2061 | 2062 | 2063 | function Get-NetOU { 2064 | <# 2065 | .SYNOPSIS 2066 | 2067 | Gets a list of all current OUs in a domain. 2068 | 2069 | .PARAMETER OUName 2070 | 2071 | The OU name to query for, wildcards accepted. 2072 | 2073 | .PARAMETER GUID 2074 | 2075 | Only return OUs with the specified GUID in their gplink property. 2076 | 2077 | .PARAMETER Domain 2078 | 2079 | The domain to query for OUs, defaults to the current domain. 2080 | 2081 | .PARAMETER DomainController 2082 | 2083 | Domain controller to reflect LDAP queries through. 2084 | 2085 | .PARAMETER ADSpath 2086 | 2087 | The LDAP source to search through. 2088 | 2089 | .PARAMETER FullData 2090 | 2091 | Switch. Return full OU objects instead of just object names (the default). 2092 | 2093 | .PARAMETER PageSize 2094 | 2095 | The PageSize to set for the LDAP searcher object. 2096 | 2097 | .EXAMPLE 2098 | 2099 | PS C:\> Get-NetOU 2100 | 2101 | Returns the current OUs in the domain. 2102 | 2103 | .EXAMPLE 2104 | 2105 | PS C:\> Get-NetOU -OUName *admin* -Domain testlab.local 2106 | 2107 | Returns all OUs with "admin" in their name in the testlab.local domain. 2108 | 2109 | .EXAMPLE 2110 | 2111 | PS C:\> Get-NetOU -GUID 123-... 2112 | 2113 | Returns all OUs with linked to the specified group policy object. 2114 | #> 2115 | 2116 | [CmdletBinding()] 2117 | Param ( 2118 | [Parameter(ValueFromPipeline=$True)] 2119 | [String] 2120 | $OUName = '*', 2121 | 2122 | [String] 2123 | $GUID, 2124 | 2125 | [String] 2126 | $Domain, 2127 | 2128 | [String] 2129 | $DomainController, 2130 | 2131 | [String] 2132 | $ADSpath, 2133 | 2134 | [Switch] 2135 | $FullData, 2136 | 2137 | [ValidateRange(1,10000)] 2138 | [Int] 2139 | $PageSize = 200 2140 | ) 2141 | 2142 | begin { 2143 | $OUSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize 2144 | } 2145 | process { 2146 | if ($OUSearcher) { 2147 | if ($GUID) { 2148 | # if we're filtering for a GUID in .gplink 2149 | $OUSearcher.filter="(&(objectCategory=organizationalUnit)(name=$OUName)(gplink=*$GUID*))" 2150 | } 2151 | else { 2152 | $OUSearcher.filter="(&(objectCategory=organizationalUnit)(name=$OUName))" 2153 | } 2154 | 2155 | $OUSearcher.FindAll() | Where-Object {$_} | ForEach-Object { 2156 | if ($FullData) { 2157 | # convert/process the LDAP fields for each result 2158 | Convert-LDAPProperty -Properties $_.Properties 2159 | } 2160 | else { 2161 | # otherwise just returning the ADS paths of the OUs 2162 | $_.properties.adspath 2163 | } 2164 | } 2165 | } 2166 | } 2167 | } 2168 | 2169 | 2170 | function Get-NetForest { 2171 | <# 2172 | .SYNOPSIS 2173 | 2174 | Returns a given forest object. 2175 | 2176 | .PARAMETER Forest 2177 | 2178 | The forest name to query for, defaults to the current domain. 2179 | 2180 | .EXAMPLE 2181 | 2182 | PS C:\> Get-NetForest -Forest external.domain 2183 | #> 2184 | 2185 | [CmdletBinding()] 2186 | param( 2187 | [Parameter(ValueFromPipeline=$True)] 2188 | [String] 2189 | $Forest 2190 | ) 2191 | 2192 | process { 2193 | if($Forest) { 2194 | $ForestContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Forest', $Forest) 2195 | try { 2196 | $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ForestContext) 2197 | } 2198 | catch { 2199 | Write-Debug "The specified forest $Forest does not exist, could not be contacted, or there isn't an existing trust." 2200 | $Null 2201 | } 2202 | } 2203 | else { 2204 | # otherwise use the current forest 2205 | $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() 2206 | } 2207 | 2208 | if($ForestObject) { 2209 | # get the SID of the forest root 2210 | $ForestSid = (New-Object System.Security.Principal.NTAccount($ForestObject.RootDomain,"krbtgt")).Translate([System.Security.Principal.SecurityIdentifier]).Value 2211 | $Parts = $ForestSid -Split "-" 2212 | $ForestSid = $Parts[0..$($Parts.length-2)] -join "-" 2213 | $ForestObject | Add-Member NoteProperty 'RootDomainSid' $ForestSid 2214 | $ForestObject 2215 | } 2216 | } 2217 | } 2218 | 2219 | 2220 | function Get-ADObject { 2221 | <# 2222 | .SYNOPSIS 2223 | 2224 | Takes a domain SID and returns the user, group, or computer object 2225 | associated with it. 2226 | 2227 | .PARAMETER SID 2228 | 2229 | The SID of the domain object you're querying for. 2230 | 2231 | .PARAMETER Name 2232 | 2233 | The Name of the domain object you're querying for. 2234 | 2235 | .PARAMETER SamAccountName 2236 | 2237 | The SamAccountName of the domain object you're querying for. 2238 | 2239 | .PARAMETER Domain 2240 | 2241 | The domain to query for objects, defaults to the current domain. 2242 | 2243 | .PARAMETER DomainController 2244 | 2245 | Domain controller to reflect LDAP queries through. 2246 | 2247 | .PARAMETER ADSpath 2248 | 2249 | The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 2250 | Useful for OU queries. 2251 | 2252 | .PARAMETER Filter 2253 | 2254 | Additional LDAP filter string for the query. 2255 | 2256 | .PARAMETER ReturnRaw 2257 | 2258 | Switch. Return the raw object instead of translating its properties. 2259 | Used by Set-ADObject to modify object properties. 2260 | 2261 | .PARAMETER PageSize 2262 | 2263 | The PageSize to set for the LDAP searcher object. 2264 | 2265 | .EXAMPLE 2266 | 2267 | PS C:\> Get-ADObject -SID "S-1-5-21-2620891829-2411261497-1773853088-1110" 2268 | 2269 | Get the domain object associated with the specified SID. 2270 | 2271 | .EXAMPLE 2272 | 2273 | PS C:\> Get-ADObject -ADSpath "CN=AdminSDHolder,CN=System,DC=testlab,DC=local" 2274 | 2275 | Get the AdminSDHolder object for the testlab.local domain. 2276 | #> 2277 | 2278 | [CmdletBinding()] 2279 | Param ( 2280 | [Parameter(ValueFromPipeline=$True)] 2281 | [String] 2282 | $SID, 2283 | 2284 | [String] 2285 | $Name, 2286 | 2287 | [String] 2288 | $SamAccountName, 2289 | 2290 | [String] 2291 | $Domain, 2292 | 2293 | [String] 2294 | $DomainController, 2295 | 2296 | [String] 2297 | $ADSpath, 2298 | 2299 | [String] 2300 | $Filter, 2301 | 2302 | [Switch] 2303 | $ReturnRaw, 2304 | 2305 | [ValidateRange(1,10000)] 2306 | [Int] 2307 | $PageSize = 200 2308 | ) 2309 | process { 2310 | if($SID) { 2311 | # if a SID is passed, try to resolve it to a reachable domain name for the searcher 2312 | try { 2313 | $Name = Convert-SidToName $SID 2314 | if($Name) { 2315 | $Canonical = Convert-NT4toCanonical -ObjectName $Name 2316 | if($Canonical) { 2317 | $Domain = $Canonical.split("/")[0] 2318 | } 2319 | else { 2320 | Write-verbose "Error resolving SID '$SID'" 2321 | return $Null 2322 | } 2323 | } 2324 | } 2325 | catch { 2326 | Write-verbose "Error resolving SID '$SID' : $_" 2327 | return $Null 2328 | } 2329 | } 2330 | 2331 | $ObjectSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize 2332 | 2333 | if($ObjectSearcher) { 2334 | 2335 | if($SID) { 2336 | $ObjectSearcher.filter = "(&(objectsid=$SID)$Filter)" 2337 | } 2338 | elseif($Name) { 2339 | $ObjectSearcher.filter = "(&(name=$Name)$Filter)" 2340 | } 2341 | elseif($SamAccountName) { 2342 | $ObjectSearcher.filter = "(&(samAccountName=$SamAccountName)$Filter)" 2343 | } 2344 | 2345 | $ObjectSearcher.FindAll() | Where-Object {$_} | ForEach-Object { 2346 | if($ReturnRaw) { 2347 | $_ 2348 | } 2349 | else { 2350 | # convert/process the LDAP fields for each result 2351 | Convert-LDAPProperty -Properties $_.Properties 2352 | } 2353 | } 2354 | } 2355 | } 2356 | } 2357 | 2358 | 2359 | 2360 | function Get-GUIDMap { 2361 | <# 2362 | .SYNOPSIS 2363 | 2364 | Helper to build a hash table of [GUID] -> resolved names 2365 | 2366 | Heavily adapted from http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx 2367 | 2368 | .PARAMETER Domain 2369 | 2370 | The domain to use for the query, defaults to the current domain. 2371 | 2372 | .PARAMETER DomainController 2373 | 2374 | Domain controller to reflect LDAP queries through. 2375 | 2376 | .PARAMETER PageSize 2377 | 2378 | The PageSize to set for the LDAP searcher object. 2379 | 2380 | .LINK 2381 | 2382 | http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx 2383 | #> 2384 | 2385 | [CmdletBinding()] 2386 | Param ( 2387 | [String] 2388 | $Domain, 2389 | 2390 | [String] 2391 | $DomainController, 2392 | 2393 | [ValidateRange(1,10000)] 2394 | [Int] 2395 | $PageSize = 200 2396 | ) 2397 | 2398 | $GUIDs = @{'00000000-0000-0000-0000-000000000000' = 'All'} 2399 | 2400 | $SchemaPath = (Get-NetForest).schema.name 2401 | 2402 | $SchemaSearcher = Get-DomainSearcher -ADSpath $SchemaPath -DomainController $DomainController -PageSize $PageSize 2403 | if($SchemaSearcher) { 2404 | $SchemaSearcher.filter = "(schemaIDGUID=*)" 2405 | try { 2406 | $SchemaSearcher.FindAll() | Where-Object {$_} | ForEach-Object { 2407 | # convert the GUID 2408 | $GUIDs[(New-Object Guid (,$_.properties.schemaidguid[0])).Guid] = $_.properties.name[0] 2409 | } 2410 | } 2411 | catch { 2412 | Write-Debug "Error in building GUID map: $_" 2413 | } 2414 | } 2415 | 2416 | $RightsSearcher = Get-DomainSearcher -ADSpath $SchemaPath.replace("Schema","Extended-Rights") -DomainController $DomainController -PageSize $PageSize 2417 | if ($RightsSearcher) { 2418 | $RightsSearcher.filter = "(objectClass=controlAccessRight)" 2419 | try { 2420 | $RightsSearcher.FindAll() | Where-Object {$_} | ForEach-Object { 2421 | # convert the GUID 2422 | $GUIDs[$_.properties.rightsguid[0].toString()] = $_.properties.name[0] 2423 | } 2424 | } 2425 | catch { 2426 | Write-Debug "Error in building GUID map: $_" 2427 | } 2428 | } 2429 | 2430 | $GUIDs 2431 | } 2432 | 2433 | function Get-ObjectAcl { 2434 | <# 2435 | .SYNOPSIS 2436 | Returns the ACLs associated with a specific active directory object. 2437 | 2438 | .PARAMETER SamAccountName 2439 | 2440 | Object name to filter for. 2441 | 2442 | .PARAMETER Name 2443 | 2444 | Object name to filter for. 2445 | 2446 | .PARAMETER DistinguishedName 2447 | 2448 | Object distinguished name to filter for. 2449 | 2450 | .PARAMETER Filter 2451 | 2452 | A customized ldap filter string to use, e.g. "(description=*admin*)" 2453 | 2454 | .PARAMETER ADSpath 2455 | 2456 | The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 2457 | Useful for OU queries. 2458 | 2459 | .PARAMETER ADSprefix 2460 | 2461 | Prefix to set for the searcher (like "CN=Sites,CN=Configuration") 2462 | 2463 | .PARAMETER RightsFilter 2464 | 2465 | Only return results with the associated rights, "All", "ResetPassword","WriteMembers" 2466 | 2467 | .PARAMETER Domain 2468 | 2469 | The domain to use for the query, defaults to the current domain. 2470 | 2471 | .PARAMETER DomainController 2472 | 2473 | Domain controller to reflect LDAP queries through. 2474 | 2475 | .PARAMETER PageSize 2476 | 2477 | The PageSize to set for the LDAP searcher object. 2478 | 2479 | .EXAMPLE 2480 | 2481 | PS C:\> Get-ObjectAcl -SamAccountName matt.admin -domain testlab.local 2482 | 2483 | Get the ACLs for the matt.admin user in the testlab.local domain 2484 | 2485 | .EXAMPLE 2486 | 2487 | PS C:\> Get-ObjectAcl -SamAccountName matt.admin -domain testlab.local -ResolveGUIDs 2488 | 2489 | Get the ACLs for the matt.admin user in the testlab.local domain and 2490 | resolve relevant GUIDs to their display names. 2491 | #> 2492 | 2493 | [CmdletBinding()] 2494 | Param ( 2495 | [Parameter(ValueFromPipeline=$True)] 2496 | [String] 2497 | $Full = $False, 2498 | 2499 | [String] 2500 | $SamAccountName, 2501 | 2502 | [String] 2503 | $Name = "*", 2504 | 2505 | [Alias('DN')] 2506 | [String] 2507 | $DistinguishedName = "*", 2508 | 2509 | [String] 2510 | $Filter, 2511 | 2512 | [String] 2513 | $ADSpath, 2514 | 2515 | [String] 2516 | $ADSprefix, 2517 | 2518 | [String] 2519 | [ValidateSet("All","ResetPassword","WriteMembers")] 2520 | $RightsFilter, 2521 | 2522 | [String] 2523 | $Domain, 2524 | 2525 | [String] 2526 | $DomainController, 2527 | 2528 | [ValidateRange(1,10000)] 2529 | [Int] 2530 | $PageSize = 200, 2531 | 2532 | [String] 2533 | $exportCsvFile = "C:\scanACLsResults.csv" 2534 | ) 2535 | 2536 | begin { 2537 | $Searcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -ADSprefix $ADSprefix -PageSize $PageSize 2538 | 2539 | # get a GUID -> name mapping 2540 | $GUIDs = Get-GUIDMap -Domain $Domain -DomainController $DomainController -PageSize $PageSize 2541 | } 2542 | 2543 | process { 2544 | 2545 | if ($Searcher) { 2546 | 2547 | if($SamAccountName) { 2548 | $Searcher.filter="(&(samaccountname=$SamAccountName)(name=$Name)(distinguishedname=$DistinguishedName)$Filter)" 2549 | } 2550 | else { 2551 | $Searcher.filter="(&(name=$Name)(distinguishedname=$DistinguishedName)$Filter)" 2552 | } 2553 | try { 2554 | $Searcher.PropertiesToLoad.Clear() 2555 | $Properies = "samaccountname","displayname", "SID", "userprincipalname", "memberof","ObjectClass","objectsid", "objectguid","distinguishedName","name","cn", "dnshostname" 2556 | foreach ($Property in $Properies) 2557 | { 2558 | $Searcher.PropertiesToLoad.Add($Property) | Out-Null 2559 | } 2560 | 2561 | $GroupMembersDB = @{} 2562 | $numberObjectsDone = 0 2563 | $counter = -1 2564 | $Searcher.FindAll() | Where-Object {$_} | Foreach-Object { 2565 | if ($counter -eq -1) { 2566 | $counter++ 2567 | Write-Host "Got more objects, analyzing.." 2568 | } 2569 | $Object = [adsi]($_.path) 2570 | if($Object.distinguishedname) { 2571 | $Access = $Object.PsBase.ObjectSecurity.access 2572 | $Access | ForEach-Object { 2573 | $_ | Add-Member NoteProperty 'ObjectDN' ($Object.distinguishedname[0]) 2574 | $Objectclass = '' 2575 | $ObjectclassList = $Object.PsBase.properties.objectclass 2576 | foreach ($class in $ObjectclassList){ 2577 | $Objectclass += $class 2578 | $Objectclass += ' ' 2579 | } 2580 | $_ | Add-Member NoteProperty 'ObjectClass' ($Objectclass) 2581 | $_ | Add-Member NoteProperty 'ObjectOwner' ($Object.PsBase.ObjectSecurity.Owner) 2582 | $GroupMembers = $Null 2583 | $GroupName = $Object.PsBase.Properties.cn.value 2584 | 2585 | if($Object.objectsid[0]){ 2586 | $S = (New-Object System.Security.Principal.SecurityIdentifier($Object.objectsid[0],0)).Value 2587 | } 2588 | else { 2589 | $S = $Null 2590 | } 2591 | $_ | Add-Member NoteProperty 'ObjectSID' $S 2592 | 2593 | $NameIdentityReference = $_.IdentityReference 2594 | $NamefromSID = $NameIdentityReference 2595 | if ($_.IdentityReference -match 's-1-'){ 2596 | try { 2597 | $NamefromSID = Convert-SidToName $NameIdentityReference 2598 | } 2599 | catch {$NamefromSID = $NameIdentityReference} 2600 | } 2601 | $_ | Add-Member NoteProperty 'UpdatedIdentityReference' ($NamefromSID) 2602 | 2603 | #counter for printing counting 2604 | $numberObjectsDone ++ 2605 | $counter++ 2606 | $_ 2607 | } 2608 | } 2609 | } | ForEach-Object { 2610 | if($RightsFilter) { 2611 | $GuidFilter = Switch ($RightsFilter) { 2612 | "ResetPassword" { "00299570-246d-11d0-a768-00aa006e0529" } 2613 | "WriteMembers" { "bf9679c0-0de6-11d0-a285-00aa003049e2" } 2614 | Default { "00000000-0000-0000-0000-000000000000"} 2615 | } 2616 | if($_.ObjectType -eq $GuidFilter) { $_ } 2617 | } 2618 | else { 2619 | $_ 2620 | } 2621 | } | Foreach-Object { 2622 | if($GUIDs) { 2623 | # if we're resolving GUIDs, map them them to the resolved hash table 2624 | $AclProperties = @{} 2625 | $_.psobject.properties | ForEach-Object { 2626 | if( ($_.Name -eq 'ObjectType') -or ($_.Name -eq 'InheritedObjectType') ) { 2627 | try { 2628 | $AclProperties[$_.Name] = $GUIDS[$_.Value.toString()] 2629 | } 2630 | catch { 2631 | $AclProperties[$_.Name] = $_.Value 2632 | } 2633 | } 2634 | else { 2635 | $AclProperties[$_.Name] = $_.Value 2636 | } 2637 | } 2638 | New-Object -TypeName PSObject -Property $AclProperties 2639 | } 2640 | else { $_ } 2641 | 2642 | } 2643 | } 2644 | catch { 2645 | Write-Warning $_ 2646 | } 2647 | } 2648 | } 2649 | } 2650 | 2651 | 2652 | function Invoke-ACLScanner { 2653 | <# 2654 | .SYNOPSIS 2655 | Searches for ACLs for specifable AD objects (default to all domain objects) 2656 | It filtered to the ones who have modifiable rights. 2657 | 2658 | .PARAMETER SamAccountName 2659 | 2660 | Object name to filter for. 2661 | 2662 | .PARAMETER Name 2663 | 2664 | Object name to filter for. 2665 | 2666 | .PARAMETER DistinguishedName 2667 | 2668 | Object distinguished name to filter for. 2669 | 2670 | .PARAMETER Filter 2671 | 2672 | A customized ldap filter string to use, e.g. "(description=*admin*)" 2673 | 2674 | .PARAMETER ADSpath 2675 | 2676 | The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" 2677 | Useful for OU queries. 2678 | 2679 | .PARAMETER ADSprefix 2680 | 2681 | Prefix to set for the searcher (like "CN=Sites,CN=Configuration") 2682 | 2683 | .PARAMETER Domain 2684 | 2685 | The domain to use for the query, defaults to the current domain. 2686 | 2687 | .PARAMETER DomainController 2688 | 2689 | Domain controller to reflect LDAP queries through. 2690 | 2691 | .PARAMETER PageSize 2692 | 2693 | The PageSize to set for the LDAP searcher object. 2694 | 2695 | .EXAMPLE 2696 | 2697 | PS C:\> Invoke-ACLScanner -ResolveGUIDs | Export-CSV -NoTypeInformation acls.csv 2698 | 2699 | Enumerate all modifable ACLs in the current domain, resolving GUIDs to display 2700 | names, and export everything to a .csv 2701 | #> 2702 | 2703 | [CmdletBinding()] 2704 | Param ( 2705 | [Parameter(ValueFromPipeline=$True)] 2706 | [String] 2707 | $Full = $False, 2708 | 2709 | [String] 2710 | $SamAccountName, 2711 | 2712 | [String] 2713 | $Name = "*", 2714 | 2715 | [Alias('DN')] 2716 | [String] 2717 | $DistinguishedName = "*", 2718 | 2719 | [String] 2720 | $Filter, 2721 | 2722 | #[String] 2723 | #$RightsFilter, 2724 | 2725 | [String] 2726 | $ADSpath, 2727 | 2728 | [String] 2729 | $ADSprefix, 2730 | 2731 | [String] 2732 | $Domain, 2733 | 2734 | [String] 2735 | $DomainController, 2736 | 2737 | [String] 2738 | $exportCsvFile = "C:\scanACLsResults.csv", 2739 | 2740 | [ValidateRange(1,10000)] 2741 | [Int] 2742 | $PageSize = 200 2743 | ) 2744 | 2745 | # Get all domain ACLs with the appropriate parameters 2746 | try{ 2747 | if ($ADSpath) 2748 | {write-host "Current path to search: $ADSpath"} 2749 | 2750 | Get-ObjectACL @PSBoundParameters | ForEach-Object { 2751 | # add in the translated SID for the object identity 2752 | $_ | Add-Member Noteproperty 'IdentitySID' ($_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value) 2753 | $_ 2754 | } | Where-Object { 2755 | # 2756 | # Important Note - you can: 2757 | # 2758 | # check for any ACLs with SIDs > -1000 2759 | # or change to "(-499)" to include all the users and groups - include the built-in entities 2760 | # or chenge to "(-1)" to include all the users and groups - include the also the general and local groups (like Authenticated Users) 2761 | try { 2762 | [int]($_.IdentitySid.split("-")[-1]) -ge (-1) 2763 | } 2764 | catch {} 2765 | } | Where-Object { 2766 | # 2767 | # Important Note - you can change here the filter and get more interesting results: 2768 | # 2769 | # filter for all modifiable rights and with Allow permissions: 2770 | #($_.ActiveDirectoryRights -eq "GenericAll") -or ($_.ActiveDirectoryRights -match "Write") -or ($_.ActiveDirectoryRights -match "Create") -or ($_.ActiveDirectoryRights -match "Delete") -or (($_.ActiveDirectoryRights -match "ExtendedRight") -and ($_.AccessControlType -eq "Allow")) 2771 | # if you scan non-"user account" objects - filter for modifiable rights with less false-positves of permissions: 2772 | #(($_.ActiveDirectoryRights -eq "GenericAll") -or ($_.ActiveDirectoryRights -match "Write") -or ($_.ActiveDirectoryRights -match "Create") -or ($_.ActiveDirectoryRights -match "Delete") -or (($_.ActiveDirectoryRights -match "ExtendedRight") -and (($_.ObjectType -ne "User-Change-Password") -and ($_.ObjectType -ne "Update-Password-Not-Required-Bit") -and ($_.ObjectType -ne "Unexpire-Password") -and ($_.ObjectType -ne "Enable-Per-User-Reversibly-Encrypted-Password") -and ($_.ObjectType -ne "Send-To")))) -and ($_.AccessControlType -eq "Allow") 2773 | # for more accurate result - this filter use "black list" approach for the most privileged permissions: 2774 | (($_.ActiveDirectoryRights -eq "GenericAll") -or ($_.ActiveDirectoryRights -match "Write") -or ($_.ActiveDirectoryRights -match "Create") -or ($_.ActiveDirectoryRights -match "Delete") -or (($_.ActiveDirectoryRights -match "ExtendedRight") -and (($_.ObjectType -eq "DS-Replication-Get-Changes") -or ($_.ObjectType -eq "DS-Replication-Get-Changes-All") -or ($_.ObjectType -eq "DS-Replication-Get-Changes-In-Filtered-Set") -or ($_.ObjectType -eq "User-Force-Change-Password")))) -and ($_.AccessControlType -eq "Allow") 2775 | ###### 2776 | # or you can write here your own filters - for example, filter for accounts that have only the GenericAll permission. 2777 | ###### 2778 | 2779 | } | Export-Csv -NoTypeInformation -append $exportCsvFile 2780 | } 2781 | catch{ 2782 | Write-Warning "`n$_" 2783 | Write-Warning "Sorry but there was an error during the scanning in one or more objects." 2784 | } 2785 | } 2786 | -------------------------------------------------------------------------------- /ACLight - version 1/ACLight.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | 3 | # Script module or binary module file associated with this manifest. 4 | ModuleToProcess = 'ACLight.psm1' 5 | 6 | # Version number of this module. 7 | ModuleVersion = '2.0' 8 | 9 | # ID used to uniquely identify this module 10 | GUID = 'as1he1d9-s83j-38a7-mf27-fjs994j238sa' 11 | 12 | # Author of this module 13 | Author = "Asaf Hecht (@hechtov), it's using functions from PowerView project created by - Will Schroeder (@harmj0y)" 14 | 15 | # Copyright statement for this module 16 | Copyright = 'BSD 3-Clause' 17 | 18 | # Description of the functionality provided by this module 19 | Description = 'Privileged Account scanner through ACLs analysis - discover Shadow Admins' 20 | 21 | # Minimum version of the Windows PowerShell engine required by this module 22 | PowerShellVersion = '3.0' 23 | 24 | # Functions to export from this module 25 | FunctionsToExport = @( 26 | 'Get-ObjectAcl' , 27 | 'Invoke-ACLScanner' 28 | 'Start-domainACLsAnalysis' 29 | 'Start-ACLsAnalysis' 30 | ) 31 | 32 | # List of all files packaged with this module 33 | FileList = 'ACLight.psm1', 'ACLight.psd1', 'ACLight.ps1' 34 | 35 | } 36 | -------------------------------------------------------------------------------- /ACLight - version 1/ACLight.psm1: -------------------------------------------------------------------------------- 1 | # Usage: "Import-Module .\ACLight.psm1" 2 | 3 | Get-ChildItem (Join-Path $PSScriptRoot *.ps1) | % { . $_.FullName} 4 | -------------------------------------------------------------------------------- /ACLight - version 1/Execute-ACLight.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set var=%~d0%~p0% 3 | cd "%var%" 4 | set "var=%cd%\ACLight.psm1" 5 | echo. 6 | echo Welcome, starting ACLight scan 7 | powershell -noprofile -ExecutionPolicy Bypass Import-Module '%var%' -force ; Start-ACLsAnalysis 8 | pause -------------------------------------------------------------------------------- /ACLight - version 1/README.md: -------------------------------------------------------------------------------- 1 | # ACLight 2 | A script for advanced discovery of Privileged Accounts - includes Shadow Admins. 3 | 4 | # Check out ACLight2 - much quicker, new scan architecture and better results 5 | 6 | The tool (version 1) was published as part of the "Shadow Admins" research - more details on "Shadow Admins" are in the blog post: https://www.cyberark.com/threat-research-blog/shadow-admins-stealthy-accounts-fear 7 | 8 | The research was also presented at the InfoSecurity conference: 9 | http://www.infosecurityeurope.com/en/Sessions/39674/Shadow-Admins-Underground-Accounts-That-Undermine-The-Network 10 | 11 | # Overview 12 | ACLight is a tool for discovering privileged accounts through advanced ACLs (Access Lists) analysis. 13 | It includes the discovery of Shadow Admins in the scanned network. 14 | 15 | The tool queries the Active Directory (AD) for its objects' ACLs and then filters and analyzes the sensitive permissions of each one. 16 | The result is a list of domain privileged accounts in the network (from the advanced ACLs perspective of the AD). 17 | You can run the scan with just any regular user (could be non-privileged user) and it automatically scans all the domains of the scanned network forest. 18 | 19 | Just run it and check the result. 20 | 21 | You should take care of all the privileged accounts that the tool discovers for you. 22 | Especially - take care of the Shadow Admins - those are accounts with direct sensitive ACLs assignments (not through membership in other known privileged groups). 23 | 24 | # Usage: 25 | Option 1: 26 | - Double click on "Execute-ACLight.bat". 27 | 28 | Option 2: 29 | - Open PowerShell (with -ExecutionPolicy Bypass) 30 | - Go to "ACLight" main folder 31 | - “Import-Module '.\ACLight.psm1'” 32 | - “Start-ACLsAnalysis” 33 | 34 | # Reading the results files: 35 | 1) First check the - "Accounts with extra permissions.txt" file - It's straight-forward & important list of the privileged accounts that were discovered in the scanned network. 36 | 2) "All entities with extra permissions.txt" - The file lists all the privileged entities that were discovered, it will include not only the user accounts but also other “empty” entities like empty groups or old accounts. 37 | 3) "Privileged Accounts Permissions - Final Report.csv" - This is the final summary report - in this file you will find what are the exact sensitive permissions each account has. 38 | 4) "Privileged Accounts Permissions - Irregular Accounts.csv" - Similar to the final report with only the privileged accounts that have direct assignment of ACL permissions (not through their group membership). 39 | 5) "[Domain name] - Full Output.csv" - Raw ACLs output for each scanned domain. 40 | 41 | # Solving Scalability and Performance issues from version 1 - use the improved version of the tool: ACLight2 42 | 43 | # References: 44 | The tool uses functions from the open source project PowerView by Will Schroeder ([@harmj0y](https://twitter.com/harmj0y)) - a great project. 45 | 46 | For more comments and questions, you can contact Asaf Hecht (([@Hechtov](https://twitter.com/Hechtov)) and CyberArk Labs. 47 | -------------------------------------------------------------------------------- /ACLight2.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | 3 | # Script module or binary module file associated with this manifest. 4 | ModuleToProcess = 'ACLight.psm1' 5 | 6 | # Version number of this module. 7 | ModuleVersion = '2.0' 8 | 9 | # ID used to uniquely identify this module 10 | GUID = 'as1he1d9-s83j-38a7-mf27-fjs994j238sa' 11 | 12 | # Author of this module 13 | Author = "Asaf Hecht (@hechtov), it's using functions from PowerView project created by - Will Schroeder (@harmj0y)" 14 | 15 | # Copyright statement for this module 16 | Copyright = 'BSD 3-Clause' 17 | 18 | # Description of the functionality provided by this module 19 | Description = 'Privileged Account scanner through ACLs analysis - discover Shadow Admins' 20 | 21 | # Minimum version of the Windows PowerShell engine required by this module 22 | PowerShellVersion = '3.0' 23 | 24 | # Functions to export from this module 25 | FunctionsToExport = @( 26 | 'Get-ObjectAcl' , 27 | 'Invoke-ACLScanner' 28 | 'Start-domainACLsAnalysis' 29 | 'Start-ACLsAnalysis' 30 | ) 31 | 32 | # List of all files packaged with this module 33 | FileList = 'ACLight.psm1', 'ACLight.psd1', 'ACLight.ps1' 34 | 35 | } 36 | -------------------------------------------------------------------------------- /ACLight2.psm1: -------------------------------------------------------------------------------- 1 | # Usage: "Import-Module .\ACLight.psm1" 2 | 3 | Get-ChildItem (Join-Path $PSScriptRoot *.ps1) | % { . $_.FullName} 4 | -------------------------------------------------------------------------------- /Execute-ACLight2.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set var=%~d0%~p0% 3 | cd "%var%" 4 | set "var=%cd%\ACLight2.psm1" 5 | echo. 6 | echo Welcome, starting Multi-Layered ACLight scan 7 | powershell -noprofile -ExecutionPolicy Bypass Import-Module '%var%' -force ; Start-ACLsAnalysis 8 | pause -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ACLight 2 | A tool for advanced discovery of Privileged Accounts - including Shadow Admins. 3 | ACLight2 is the improved version of the tool. 4 | 5 | # Shadow Admins Research 6 | The tool (version 1) was published as part of the "Shadow Admins" research - more details on "Shadow Admins" are in the blog post: https://www.cyberark.com/threat-research-blog/shadow-admins-stealthy-accounts-fear 7 | 8 | The research was also presented at the InfoSecurity conference, London: [presentation link](https://www.slideshare.net/AsafHecht/the-presentation-on-my-shadow-admins-research) 9 | 10 | # Overview 11 | ACLight is a tool for discovering privileged accounts through advanced ACLs analysis (objects’ ACLs - Access Lists, aka DACL\ACEs). 12 | It includes the discovery of Shadow Admins in the scanned network. 13 | 14 | The tool queries the Active Directory (AD) for its objects' ACLs and then filters and analyzes the sensitive permissions of each one. 15 | The result is a list of most privileged accounts in the network (from the advanced ACLs perspective of the AD). 16 | You can run the scan with just any regular user, it could be a non-privileged user because it only performs legitimate read-only LDAP queries to the AD. 17 | 18 | Just run it and check the result. 19 | 20 | You should take care of all the privileged accounts that the tool discovers for you. 21 | Especially - take care of the Shadow Admins - those are accounts with direct sensitive ACLs assignments (as opposed of getting privileges as part of membership in known privileged groups). 22 | 23 | For scanning cloud environments and discover the most privileged entities in AWS and Azure, check the new open source tool - SkyArk: 24 | https://github.com/cyberark/SkyArk 25 | 26 | # ACLight2 27 | 28 | This is ACLight2 - the new version of ACLight scan. It’s much quicker, has a new scan architecture and better results. 29 | It solves scalability and performance issues from the previous version. 30 | 31 | In addition, ACLight2 is built on a recursive scan and provides multi-layered privileged accounts analysis. 32 | As a first step, the scan starts by building the first layer of privileged accounts. Those are the accounts who have direct privileges over the domain’s sensitive objects. Then, as a second step, the tool continues and scans the ACLs over those newly discovered privileged accounts from layer 1 and builds an optional second layer of new privileged accounts who have privileges over the accounts from the first layer. This second step is recursive, the tool keeps scanning for more optional layers of privileged accounts until all the privileged accounts chains are being enumerated. 33 | 34 | # Usage: 35 | Option 1: 36 | - Double click on "Execute-ACLight.bat". 37 | 38 | Option 2: 39 | - Open PowerShell (with -ExecutionPolicy Bypass) 40 | - Go to "ACLight2" main folder 41 | - “Import-Module '.\ACLight2.psm1'” 42 | - “Start-ACLsAnalysis” 43 | 44 | Choose the target domain: 45 | By default, ACLight automatically scans all the domains of the scanned network forest. You can use the “Domain” parameter if you are interested in scanning only one specific domain: 46 | - Start-ACLsAnalysis -domain "DomainName.com" 47 | 48 | **ACLight2 DEMO:** 49 | ![Demo](https://github.com/Hechtov/Photos/blob/master/ACLight/ACLight-v2.gif) 50 | 51 | # Reading the results files: 52 | 1) First, check the scan’s executive summary "Privileged Accounts - Layers Analysis.txt" - It's an important and straight-forward list of the most privileged accounts that were discovered in the scanned network. 53 | 2) "Privileged Accounts Permissions - Final Report.csv" - This is the final summary report, in this file you will find what are the exact sensitive permissions each account has. 54 | 4) "Privileged Accounts Permissions - Irregular Accounts.csv", similar to the final report with only the privileged accounts that have direct assignment of ACL permissions (not through their group membership). 55 | 56 | # References: 57 | The tool uses functions from the open source project PowerView by Will Schroeder ([@harmj0y](https://twitter.com/harmj0y)) - a great project. 58 | 59 | For more comments and questions, you can contact Asaf Hecht ([@Hechtov](https://twitter.com/Hechtov)) and CyberArk Labs. 60 | --------------------------------------------------------------------------------