├── ScriptSentry.png ├── .gitignore ├── README.md └── Invoke-ScriptSentry.ps1 /ScriptSentry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techspence/ScriptSentry/HEAD/ScriptSentry.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Ignore/* 2 | .vs/* 3 | .vscode/* 4 | Examples/Output/* 5 | Releases/* 6 | Artefacts/* 7 | ReleasedUnpacked/* 8 | Sources/.vs 9 | Sources/*/.vs 10 | Sources/*/obj 11 | Sources/*/bin 12 | Sources/*/*/obj 13 | Sources/*/*/bin 14 | Sources/packages/* 15 | Lib/Default/* 16 | Lib/Standard/* 17 | Lib/Core/* 18 | Private/* 19 | *.csv -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScriptSentry 2 | ![ScriptSentry](ScriptSentry.png) 3 | 4 | ScriptSentry finds misconfigured and dangerous logon scripts. 5 | 6 | ### Read the blog post 7 | https://offsec.blog/hidden-menace-how-to-identify-misconfigured-and-dangerous-logon-scripts/ 8 | 9 | ### Usage 10 | ```PowerShell 11 | # Run ScriptSentry and display results on the console 12 | IEX(Invoke-WebRequest 'https://raw.githubusercontent.com/techspence/ScriptSentry/main/Invoke-ScriptSentry.ps1') 13 | Invoke-ScriptSentry 14 | 15 | # Run ScriptSentry and save output to a text file 16 | IEX(Invoke-WebRequest 'https://raw.githubusercontent.com/techspence/ScriptSentry/main/Invoke-ScriptSentry.ps1') 17 | Invoke-ScriptSentry | Out-File c:\temp\ScriptSentry.txt 18 | 19 | # Run ScriptSentry and save results to separate csv files in the current directory 20 | IEX(Invoke-WebRequest 'https://raw.githubusercontent.com/techspence/ScriptSentry/main/Invoke-ScriptSentry.ps1') 21 | Invoke-ScriptSentry -SaveOutput $true 22 | ``` 23 | 24 | ### Example Output 25 | ``` 26 | _______ _______ _______ _________ _______ _________ _______ _______ _ _________ _______ 27 | ( ____ \( ____ \( ____ )\__ __/( ____ )\__ __/( ____ \( ____ \( ( /|\__ __/( ____ )|\ /| 28 | | ( \/| ( \/| ( )| ) ( | ( )| ) ( | ( \/| ( \/| \ ( | ) ( | ( )|( \ / ) 29 | | (_____ | | | (____)| | | | (____)| | | | (_____ | (__ | \ | | | | | (____)| \ (_) / 30 | (_____ )| | | __) | | | _____) | | (_____ )| __) | (\ \) | | | | __) \ / 31 | ) || | | (\ ( | | | ( | | ) || ( | | \ | | | | (\ ( ) ( 32 | /\____) || (____/\| ) \ \_____) (___| ) | | /\____) || (____/\| ) \ | | | | ) \ \__ | | 33 | \_______)(_______/|/ \__/\_______/|/ )_( \_______)(_______/|/ )_) )_( |/ \__/ \_/ 34 | by: Spencer Alessi @techspence 35 | v0.6 36 | __,_______ 37 | / __.==---/ * * * * * * 38 | / (-' 39 | `-' 40 | Setting phasers to stun, please wait.. 41 | 42 | ########## Unsafe UNC folder permissions ########## 43 | 44 | Type File User Rights 45 | ---- ---- ---- ------ 46 | UnsafeUNCFolderPermission \\eureka-dc01\fileshare1 Everyone FullControl 47 | UnsafeUNCFolderPermission \\eureka-dc01\fileshare1\accounting Everyone FullControl 48 | UnsafeUNCFolderPermission \\eureka-dc01\fileshare1\IT Everyone FullControl 49 | 50 | 51 | ########## Unsafe logon script permissions ########## 52 | 53 | Type File User Rights 54 | ---- ---- ---- ------ 55 | UnsafeLogonScriptPermission \\eureka.local\sysvol\eureka.local\scripts\elevate.vbs NT AUTHORITY\Authenticated Users ReadAndExecute, Synchronize 56 | UnsafeLogonScriptPermission \\eureka.local\sysvol\eureka.local\scripts\run.vbs NT AUTHORITY\Authenticated Users ReadAndExecute, Synchronize 57 | UnsafeLogonScriptPermission \\eureka.local\sysvol\eureka.local\scripts\test.cmd EUREKA\Domain Users Modify, Synchronize 58 | 59 | 60 | ########## Unsafe GPO logon script permissions ########## 61 | 62 | Type File User Rights 63 | ---- ---- ---- ------ 64 | UnsafeGPOLogonScriptPermission \\eureka-dc01\fileshare1\run.bat EUREKA\testuser Write, ReadAndExecute, Synchronize 65 | UnsafeGPOLogonScriptPermission \\eureka-dc01\fileshare1\run.bat Everyone FullControl 66 | 67 | 68 | ########## Unsafe UNC file permissions ########## 69 | 70 | Type File User Rights 71 | ---- ---- ---- ------ 72 | UnsafeUNCFilePermission \\eureka-dc01\fileshare1\IT\securit360pentest.bat Everyone FullControl 73 | 74 | 75 | ########## Unsafe NETLOGON/SYSVOL permissions ########## 76 | 77 | Type Folder User Rights 78 | ---- ------ ---- ------ 79 | UnsafeNetlogonSysvol \\eureka.local\NETLOGON EUREKA\Domain Users Modify, Synchronize 80 | UnsafeNetlogonSysvol \\eureka.local\SYSVOL NT AUTHORITY\Authenticated Users Modify, Synchronize 81 | 82 | ########## Plaintext credentials ########## 83 | 84 | Type File Credential 85 | ---- ---- ---------- 86 | Credentials \\eureka.local\sysvol\eureka.local\scripts\ADCheck.ps1 $password = ConvertTo-SecureString -String "Password2468!" -AsPlainText -Force 87 | Credentials \\eureka.local\sysvol\eureka.local\scripts\shares.cmd net use f: \\eureka-dc01\fileshare1\it /user:itadmin Password2468! 88 | Credentials \\eureka.local\sysvol\eureka.local\scripts\test.cmd net use g: \\eureka-dc01\fileshare1 /user:user1 Password3355! 89 | Credentials \\eureka.local\sysvol\eureka.local\scripts\test.cmd net use h: \\eureka-dc01\fileshare1\accounting /user:userfoo Password5! 90 | Credentials \\eureka.local\sysvol\eureka.local\scripts\logon.kix Use X: "\\eureka-dc01\fileshare2" /USER:itadmin /P:Password2468! 91 | 92 | ########## Nonexistent Shares ########## 93 | 94 | Type Server Share Script DNS Exploitable Admins 95 | ---- ------ ----- ------ --- ----------- ------ 96 | NonexistentShare CUHOLDING \\CUHOLDING\QUICKBOOKS \\eureka.local\sysvol\eureka.local\scripts\marketing.bat No Potentially No 97 | NonexistentShare eureka-srvnotexist \\eureka-srvnotexist\NonExistingShare \\eureka.local\sysvol\eureka.local\scripts\test.cmd No Potentially No 98 | NonexistentShare NAS \\NAS\PUBLIC \\eureka.local\sysvol\eureka.local\scripts\main.bat No Potentially No 99 | NonexistentShare NAS \\NAS\SYMITAR \\eureka.local\sysvol\eureka.local\scripts\symregOLD.bat No Potentially No 100 | 101 | ########## Admins with logonscripts ########## 102 | 103 | Type User LogonScript 104 | ---- ---- ----------- 105 | AdminLogonScript LDAP://CN=Administrator,CN=Users,DC=eureka,DC=local run.vbs 106 | AdminLogonScript LDAP://CN=it admin,OU=Admins,OU=Eureka,DC=eureka,DC=local elevate.vbs 107 | 108 | ########## Admins with logonscripts mapped from nonexistent share ########## 109 | 110 | Type Server Share Script DNS Exploitable Admins 111 | ---- ------ ----- ------ --- ----------- ------ 112 | ExploitableLogonScript eureka-srvnotexist \\eureka-srvnotexist\NonExistingShare \\eureka.local\sysvol\eureka.local\scripts\test.cmd No Yes LDAP://eureka.local/CN=it admin,OU=Admins,OU=Eureka,DC=eureka,DC=local 113 | ExploitableLogonScript eureka-srvnotexist \\eureka-srvnotexist\NonExistingShare \\eureka.local\sysvol\eureka.local\scripts\test.cmd No Yes LDAP://eureka.local/CN=user1,OU=Users,OU=Eureka,DC=eureka,DC=local 114 | ``` -------------------------------------------------------------------------------- /Invoke-ScriptSentry.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-ScriptSentry{ 2 | <# 3 | .SYNOPSIS 4 | ScriptSentry finds misconfigured and dangerous logon scripts. 5 | 6 | .DESCRIPTION 7 | ScriptSentry searches the NETLOGON share & Group Policy to 8 | 1) identify plaintext credentials in logon scripts 9 | 2) identify admins that have logon script set 10 | 3) identify scripts and shares that may have dangerous permissions 11 | 12 | .EXAMPLE 13 | Invoke-ScriptSentry 14 | 15 | .EXAMPLE 16 | Invoke-ScriptSentry | Out-File c:\temp\ScriptSentry.txt 17 | 18 | .EXAMPLE 19 | Invoke-ScriptSentry -SaveOutput $true 20 | 21 | #> 22 | [CmdletBinding()] 23 | Param( 24 | [boolean]$SaveOutput = $false 25 | ) 26 | 27 | function Get-ForestDomains { 28 | [CmdletBinding()] 29 | param() 30 | 31 | $forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() 32 | $forest.Domains 33 | } 34 | function Get-Domain { 35 | [OutputType([System.DirectoryServices.ActiveDirectory.Domain])] 36 | [CmdletBinding()] 37 | Param( 38 | [Parameter(Position = 0, ValueFromPipeline = $True)] 39 | [ValidateNotNullOrEmpty()] 40 | [String] 41 | $Domain, 42 | 43 | [Management.Automation.PSCredential] 44 | [Management.Automation.CredentialAttribute()] 45 | $Credential = [Management.Automation.PSCredential]::Empty 46 | ) 47 | 48 | PROCESS { 49 | if ($PSBoundParameters['Credential']) { 50 | 51 | Write-Verbose '[Get-Domain] Using alternate credentials for Get-Domain' 52 | 53 | if ($PSBoundParameters['Domain']) { 54 | $TargetDomain = $Domain 55 | } 56 | else { 57 | # if no domain is supplied, extract the logon domain from the PSCredential passed 58 | $TargetDomain = $Credential.GetNetworkCredential().Domain 59 | Write-Verbose "[Get-Domain] Extracted domain '$TargetDomain' from -Credential" 60 | } 61 | 62 | $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $TargetDomain, $Credential.UserName, $Credential.GetNetworkCredential().Password) 63 | 64 | try { 65 | [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) 66 | } 67 | catch { 68 | Write-Verbose "[Get-Domain] The specified domain '$TargetDomain' does not exist, could not be contacted, there isn't an existing trust, or the specified credentials are invalid: $_" 69 | } 70 | } 71 | elseif ($PSBoundParameters['Domain']) { 72 | $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $Domain) 73 | try { 74 | [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) 75 | } 76 | catch { 77 | Write-Verbose "[Get-Domain] The specified domain '$Domain' does not exist, could not be contacted, or there isn't an existing trust : $_" 78 | } 79 | } 80 | else { 81 | try { 82 | [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() 83 | } 84 | catch { 85 | Write-Verbose "[Get-Domain] Error retrieving the current domain: $_" 86 | } 87 | } 88 | } 89 | } 90 | function Get-DomainSearcher { 91 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] 92 | [OutputType('System.DirectoryServices.DirectorySearcher')] 93 | [CmdletBinding()] 94 | Param( 95 | [Parameter(ValueFromPipeline = $True)] 96 | [ValidateNotNullOrEmpty()] 97 | [String] 98 | $Domain, 99 | 100 | [ValidateNotNullOrEmpty()] 101 | [Alias('Filter')] 102 | [String] 103 | $LDAPFilter, 104 | 105 | [ValidateNotNullOrEmpty()] 106 | [String[]] 107 | $Properties, 108 | 109 | [ValidateNotNullOrEmpty()] 110 | [Alias('ADSPath')] 111 | [String] 112 | $SearchBase, 113 | 114 | [ValidateNotNullOrEmpty()] 115 | [String] 116 | $SearchBasePrefix, 117 | 118 | [ValidateNotNullOrEmpty()] 119 | [Alias('DomainController')] 120 | [String] 121 | $Server, 122 | 123 | [ValidateSet('Base', 'OneLevel', 'Subtree')] 124 | [String] 125 | $SearchScope = 'Subtree', 126 | 127 | [ValidateRange(1, 10000)] 128 | [Int] 129 | $ResultPageSize = 200, 130 | 131 | [ValidateRange(1, 10000)] 132 | [Int] 133 | $ServerTimeLimit = 120, 134 | 135 | [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] 136 | [String] 137 | $SecurityMasks, 138 | 139 | [Switch] 140 | $Tombstone, 141 | 142 | [Management.Automation.PSCredential] 143 | [Management.Automation.CredentialAttribute()] 144 | $Credential = [Management.Automation.PSCredential]::Empty 145 | ) 146 | 147 | PROCESS { 148 | if ($PSBoundParameters['Domain']) { 149 | $TargetDomain = $Domain 150 | 151 | if ($ENV:USERDNSDOMAIN -and ($ENV:USERDNSDOMAIN.Trim() -ne '')) { 152 | # see if we can grab the user DNS logon domain from environment variables 153 | $UserDomain = $ENV:USERDNSDOMAIN 154 | if ($ENV:LOGONSERVER -and ($ENV:LOGONSERVER.Trim() -ne '') -and $UserDomain) { 155 | $BindServer = "$($ENV:LOGONSERVER -replace '\\','').$UserDomain" 156 | } 157 | } 158 | } 159 | elseif ($PSBoundParameters['Credential']) { 160 | # if not -Domain is specified, but -Credential is, try to retrieve the current domain name with Get-Domain 161 | $DomainObject = Get-Domain -Credential $Credential 162 | $BindServer = ($DomainObject.PdcRoleOwner).Name 163 | $TargetDomain = $DomainObject.Name 164 | } 165 | elseif ($ENV:USERDNSDOMAIN -and ($ENV:USERDNSDOMAIN.Trim() -ne '')) { 166 | # see if we can grab the user DNS logon domain from environment variables 167 | $TargetDomain = $ENV:USERDNSDOMAIN 168 | if ($ENV:LOGONSERVER -and ($ENV:LOGONSERVER.Trim() -ne '') -and $TargetDomain) { 169 | $BindServer = "$($ENV:LOGONSERVER -replace '\\','').$TargetDomain" 170 | } 171 | } 172 | else { 173 | # otherwise, resort to Get-Domain to retrieve the current domain object 174 | write-verbose "get-domain" 175 | $DomainObject = Get-Domain 176 | $BindServer = ($DomainObject.PdcRoleOwner).Name 177 | $TargetDomain = $DomainObject.Name 178 | } 179 | 180 | if ($PSBoundParameters['Server']) { 181 | # if there's not a specified server to bind to, try to pull a logon server from ENV variables 182 | $BindServer = $Server 183 | } 184 | 185 | $SearchString = 'LDAP://' 186 | 187 | if ($BindServer -and ($BindServer.Trim() -ne '')) { 188 | $SearchString += $BindServer 189 | if ($TargetDomain) { 190 | $SearchString += '/' 191 | } 192 | } 193 | 194 | if ($PSBoundParameters['SearchBasePrefix']) { 195 | $SearchString += $SearchBasePrefix + ',' 196 | } 197 | 198 | if ($PSBoundParameters['SearchBase']) { 199 | if ($SearchBase -Match '^GC://') { 200 | # if we're searching the global catalog, get the path in the right format 201 | $DN = $SearchBase.ToUpper().Trim('/') 202 | $SearchString = '' 203 | } 204 | else { 205 | if ($SearchBase -match '^LDAP://') { 206 | if ($SearchBase -match "LDAP://.+/.+") { 207 | $SearchString = '' 208 | $DN = $SearchBase 209 | } 210 | else { 211 | $DN = $SearchBase.SubString(7) 212 | } 213 | } 214 | else { 215 | $DN = $SearchBase 216 | } 217 | } 218 | } 219 | else { 220 | # transform the target domain name into a distinguishedName if an ADS search base is not specified 221 | if ($TargetDomain -and ($TargetDomain.Trim() -ne '')) { 222 | $DN = "DC=$($TargetDomain.Replace('.', ',DC='))" 223 | } 224 | } 225 | 226 | $SearchString += $DN 227 | Write-Verbose "[Get-DomainSearcher] search base: $SearchString" 228 | 229 | if ($Credential -ne [Management.Automation.PSCredential]::Empty) { 230 | Write-Verbose "[Get-DomainSearcher] Using alternate credentials for LDAP connection" 231 | # bind to the inital search object using alternate credentials 232 | $DomainObject = New-Object DirectoryServices.DirectoryEntry($SearchString, $Credential.UserName, $Credential.GetNetworkCredential().Password) 233 | $Searcher = New-Object System.DirectoryServices.DirectorySearcher($DomainObject) 234 | } 235 | else { 236 | # bind to the inital object using the current credentials 237 | $Searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]$SearchString) 238 | } 239 | 240 | $Searcher.PageSize = $ResultPageSize 241 | $Searcher.SearchScope = $SearchScope 242 | $Searcher.CacheResults = $False 243 | $Searcher.ReferralChasing = [System.DirectoryServices.ReferralChasingOption]::All 244 | 245 | if ($PSBoundParameters['ServerTimeLimit']) { 246 | $Searcher.ServerTimeLimit = $ServerTimeLimit 247 | } 248 | 249 | if ($PSBoundParameters['Tombstone']) { 250 | $Searcher.Tombstone = $True 251 | } 252 | 253 | if ($PSBoundParameters['LDAPFilter']) { 254 | $Searcher.filter = $LDAPFilter 255 | } 256 | 257 | if ($PSBoundParameters['SecurityMasks']) { 258 | $Searcher.SecurityMasks = Switch ($SecurityMasks) { 259 | 'Dacl' { [System.DirectoryServices.SecurityMasks]::Dacl } 260 | 'Group' { [System.DirectoryServices.SecurityMasks]::Group } 261 | 'None' { [System.DirectoryServices.SecurityMasks]::None } 262 | 'Owner' { [System.DirectoryServices.SecurityMasks]::Owner } 263 | 'Sacl' { [System.DirectoryServices.SecurityMasks]::Sacl } 264 | } 265 | } 266 | 267 | if ($PSBoundParameters['Properties']) { 268 | # handle an array of properties to load w/ the possibility of comma-separated strings 269 | $PropertiesToLoad = $Properties| ForEach-Object { $_.Split(',') } 270 | $Null = $Searcher.PropertiesToLoad.AddRange(($PropertiesToLoad)) 271 | } 272 | 273 | $Searcher 274 | } 275 | } 276 | function Get-DomainGroupMember { 277 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] 278 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 279 | [OutputType('StrongView.GroupMember')] 280 | [CmdletBinding(DefaultParameterSetName = 'None')] 281 | Param( 282 | [Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] 283 | [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] 284 | [String[]] 285 | $Identity, 286 | 287 | [ValidateNotNullOrEmpty()] 288 | [String] 289 | $Domain, 290 | 291 | [Parameter(ParameterSetName = 'ManualRecurse')] 292 | [Switch] 293 | $Recurse, 294 | 295 | [Parameter(ParameterSetName = 'RecurseUsingMatchingRule')] 296 | [Switch] 297 | $RecurseUsingMatchingRule, 298 | 299 | [ValidateNotNullOrEmpty()] 300 | [Alias('Filter')] 301 | [String] 302 | $LDAPFilter, 303 | 304 | [ValidateNotNullOrEmpty()] 305 | [Alias('ADSPath')] 306 | [String] 307 | $SearchBase, 308 | 309 | [ValidateNotNullOrEmpty()] 310 | [Alias('DomainController')] 311 | [String] 312 | $Server, 313 | 314 | [ValidateSet('Base', 'OneLevel', 'Subtree')] 315 | [String] 316 | $SearchScope = 'Subtree', 317 | 318 | [ValidateRange(1, 10000)] 319 | [Int] 320 | $ResultPageSize = 200, 321 | 322 | [ValidateRange(1, 10000)] 323 | [Int] 324 | $ServerTimeLimit, 325 | 326 | [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] 327 | [String] 328 | $SecurityMasks, 329 | 330 | [Switch] 331 | $Tombstone, 332 | 333 | [Management.Automation.PSCredential] 334 | [Management.Automation.CredentialAttribute()] 335 | $Credential = [Management.Automation.PSCredential]::Empty 336 | ) 337 | 338 | BEGIN { 339 | $SearcherArguments = @{ 340 | 'Properties' = 'member,samaccountname,distinguishedname' 341 | } 342 | if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } 343 | if ($PSBoundParameters['LDAPFilter']) { $SearcherArguments['LDAPFilter'] = $LDAPFilter } 344 | if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } 345 | if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } 346 | if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } 347 | if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } 348 | if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } 349 | if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } 350 | if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } 351 | 352 | $ADNameArguments = @{} 353 | if ($PSBoundParameters['Domain']) { $ADNameArguments['Domain'] = $Domain } 354 | if ($PSBoundParameters['Server']) { $ADNameArguments['Server'] = $Server } 355 | if ($PSBoundParameters['Credential']) { $ADNameArguments['Credential'] = $Credential } 356 | } 357 | 358 | PROCESS { 359 | $GroupSearcher = Get-DomainSearcher @SearcherArguments 360 | if ($GroupSearcher) { 361 | if ($PSBoundParameters['RecurseUsingMatchingRule']) { 362 | $SearcherArguments['Identity'] = $Identity 363 | $SearcherArguments['Raw'] = $True 364 | $Group = Get-DomainGroup @SearcherArguments 365 | 366 | if (-not $Group) { 367 | Write-Warning "[Get-DomainGroupMember] Error searching for group with identity: $Identity" 368 | } 369 | else { 370 | $GroupFoundName = $Group.properties.item('samaccountname')[0] 371 | $GroupFoundDN = $Group.properties.item('distinguishedname')[0] 372 | 373 | if ($PSBoundParameters['Domain']) { 374 | $GroupFoundDomain = $Domain 375 | } 376 | else { 377 | # if a domain isn't passed, try to extract it from the found group distinguished name 378 | if ($GroupFoundDN) { 379 | $GroupFoundDomain = $GroupFoundDN.SubString($GroupFoundDN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' 380 | } 381 | } 382 | Write-Verbose "[Get-DomainGroupMember] Using LDAP matching rule to recurse on '$GroupFoundDN', only user accounts will be returned." 383 | $GroupSearcher.filter = "(&(samAccountType=805306368)(memberof:1.2.840.113556.1.4.1941:=$GroupFoundDN))" 384 | $GroupSearcher.PropertiesToLoad.AddRange(('distinguishedName')) 385 | $Members = $GroupSearcher.FindAll() | ForEach-Object {$_.Properties.distinguishedname[0]} 386 | } 387 | $Null = $SearcherArguments.Remove('Raw') 388 | } 389 | else { 390 | $IdentityFilter = '' 391 | $Filter = '' 392 | $Identity | Where-Object {$_} | ForEach-Object { 393 | $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') 394 | if ($IdentityInstance -match '^S-1-') { 395 | $IdentityFilter += "(objectsid=$IdentityInstance)" 396 | } 397 | elseif ($IdentityInstance -match '^CN=') { 398 | $IdentityFilter += "(distinguishedname=$IdentityInstance)" 399 | if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { 400 | # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname 401 | # and rebuild the domain searcher 402 | $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' 403 | Write-Verbose "[Get-DomainGroupMember] Extracted domain '$IdentityDomain' from '$IdentityInstance'" 404 | $SearcherArguments['Domain'] = $IdentityDomain 405 | $GroupSearcher = Get-DomainSearcher @SearcherArguments 406 | if (-not $GroupSearcher) { 407 | Write-Warning "[Get-DomainGroupMember] Unable to retrieve domain searcher for '$IdentityDomain'" 408 | } 409 | } 410 | } 411 | elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { 412 | $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' 413 | $IdentityFilter += "(objectguid=$GuidByteString)" 414 | } 415 | elseif ($IdentityInstance.Contains('\')) { 416 | $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical 417 | if ($ConvertedIdentityInstance) { 418 | $GroupDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) 419 | $GroupName = $IdentityInstance.Split('\')[1] 420 | $IdentityFilter += "(samAccountName=$GroupName)" 421 | $SearcherArguments['Domain'] = $GroupDomain 422 | Write-Verbose "[Get-DomainGroupMember] Extracted domain '$GroupDomain' from '$IdentityInstance'" 423 | $GroupSearcher = Get-DomainSearcher @SearcherArguments 424 | } 425 | } 426 | else { 427 | $IdentityFilter += "(samAccountName=$IdentityInstance)" 428 | } 429 | } 430 | 431 | if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { 432 | $Filter += "(|$IdentityFilter)" 433 | } 434 | 435 | if ($PSBoundParameters['LDAPFilter']) { 436 | Write-Verbose "[Get-DomainGroupMember] Using additional LDAP filter: $LDAPFilter" 437 | $Filter += "$LDAPFilter" 438 | } 439 | 440 | $GroupSearcher.filter = "(&(objectCategory=group)$Filter)" 441 | Write-Verbose "[Get-DomainGroupMember] Get-DomainGroupMember filter string: $($GroupSearcher.filter)" 442 | try { 443 | $Result = $GroupSearcher.FindOne() 444 | } 445 | catch { 446 | Write-Warning "[Get-DomainGroupMember] Error searching for group with identity '$Identity': $_" 447 | $Members = @() 448 | } 449 | 450 | $GroupFoundName = '' 451 | $GroupFoundDN = '' 452 | 453 | if ($Result) { 454 | $Members = $Result.properties.item('member') 455 | 456 | if ($Members.count -eq 0) { 457 | # ranged searching, thanks @meatballs__ ! 458 | $Finished = $False 459 | $Bottom = 0 460 | $Top = 0 461 | 462 | while (-not $Finished) { 463 | $Top = $Bottom + 1499 464 | $MemberRange="member;range=$Bottom-$Top" 465 | $Bottom += 1500 466 | $Null = $GroupSearcher.PropertiesToLoad.Clear() 467 | $Null = $GroupSearcher.PropertiesToLoad.Add("$MemberRange") 468 | $Null = $GroupSearcher.PropertiesToLoad.Add('samaccountname') 469 | $Null = $GroupSearcher.PropertiesToLoad.Add('distinguishedname') 470 | 471 | try { 472 | $Result = $GroupSearcher.FindOne() 473 | $RangedProperty = $Result.Properties.PropertyNames -like "member;range=*" 474 | $Members += $Result.Properties.item($RangedProperty) 475 | $GroupFoundName = $Result.properties.item('samaccountname')[0] 476 | $GroupFoundDN = $Result.properties.item('distinguishedname')[0] 477 | 478 | if ($Members.count -eq 0) { 479 | $Finished = $True 480 | } 481 | } 482 | catch [System.Management.Automation.MethodInvocationException] { 483 | $Finished = $True 484 | } 485 | } 486 | } 487 | else { 488 | $GroupFoundName = $Result.properties.item('samaccountname')[0] 489 | $GroupFoundDN = $Result.properties.item('distinguishedname')[0] 490 | $Members += $Result.Properties.item($RangedProperty) 491 | } 492 | 493 | if ($PSBoundParameters['Domain']) { 494 | $GroupFoundDomain = $Domain 495 | } 496 | else { 497 | # if a domain isn't passed, try to extract it from the found group distinguished name 498 | if ($GroupFoundDN) { 499 | $GroupFoundDomain = $GroupFoundDN.SubString($GroupFoundDN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' 500 | } 501 | } 502 | } 503 | } 504 | 505 | ForEach ($Member in $Members) { 506 | if ($Recurse -and $UseMatchingRule) { 507 | $Properties = $_.Properties 508 | } 509 | else { 510 | $ObjectSearcherArguments = $SearcherArguments.Clone() 511 | $ObjectSearcherArguments['Identity'] = $Member 512 | $ObjectSearcherArguments['Raw'] = $True 513 | $ObjectSearcherArguments['Properties'] = 'distinguishedname,cn,samaccountname,objectsid,objectclass' 514 | $Object = Get-DomainObject @ObjectSearcherArguments 515 | $Properties = $Object.Properties 516 | } 517 | 518 | if ($Properties) { 519 | $GroupMember = New-Object PSObject 520 | $GroupMember | Add-Member Noteproperty 'GroupDomain' $GroupFoundDomain 521 | $GroupMember | Add-Member Noteproperty 'GroupName' $GroupFoundName 522 | $GroupMember | Add-Member Noteproperty 'GroupDistinguishedName' $GroupFoundDN 523 | 524 | if ($Properties.objectsid) { 525 | $MemberSID = ((New-Object System.Security.Principal.SecurityIdentifier $Properties.objectsid[0], 0).Value) 526 | } 527 | else { 528 | $MemberSID = $Null 529 | } 530 | 531 | try { 532 | $MemberDN = $Properties.distinguishedname[0] 533 | if ($MemberDN -match 'ForeignSecurityPrincipals|S-1-5-21') { 534 | try { 535 | if (-not $MemberSID) { 536 | $MemberSID = $Properties.cn[0] 537 | } 538 | $MemberSimpleName = Convert-ADName -Identity $MemberSID -OutputType 'DomainSimple' @ADNameArguments 539 | 540 | if ($MemberSimpleName) { 541 | $MemberDomain = $MemberSimpleName.Split('@')[1] 542 | } 543 | else { 544 | Write-Warning "[Get-DomainGroupMember] Error converting $MemberDN" 545 | $MemberDomain = $Null 546 | } 547 | } 548 | catch { 549 | Write-Warning "[Get-DomainGroupMember] Error converting $MemberDN" 550 | $MemberDomain = $Null 551 | } 552 | } 553 | else { 554 | # extract the FQDN from the Distinguished Name 555 | $MemberDomain = $MemberDN.SubString($MemberDN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' 556 | } 557 | } 558 | catch { 559 | $MemberDN = $Null 560 | $MemberDomain = $Null 561 | } 562 | 563 | if ($Properties.samaccountname) { 564 | # forest users have the samAccountName set 565 | $MemberName = $Properties.samaccountname[0] 566 | } 567 | else { 568 | # external trust users have a SID, so convert it 569 | try { 570 | $MemberName = ConvertFrom-SID -ObjectSID $Properties.cn[0] @ADNameArguments 571 | } 572 | catch { 573 | # if there's a problem contacting the domain to resolve the SID 574 | $MemberName = $Properties.cn[0] 575 | } 576 | } 577 | 578 | if ($Properties.objectclass -match 'computer') { 579 | $MemberObjectClass = 'computer' 580 | } 581 | elseif ($Properties.objectclass -match 'group') { 582 | $MemberObjectClass = 'group' 583 | } 584 | elseif ($Properties.objectclass -match 'user') { 585 | $MemberObjectClass = 'user' 586 | } 587 | else { 588 | $MemberObjectClass = $Null 589 | } 590 | $GroupMember | Add-Member Noteproperty 'MemberDomain' $MemberDomain 591 | $GroupMember | Add-Member Noteproperty 'MemberName' $MemberName 592 | $GroupMember | Add-Member Noteproperty 'MemberDistinguishedName' $MemberDN 593 | $GroupMember | Add-Member Noteproperty 'MemberObjectClass' $MemberObjectClass 594 | $GroupMember | Add-Member Noteproperty 'MemberSID' $MemberSID 595 | $GroupMember.PSObject.TypeNames.Insert(0, 'StrongView.GroupMember') 596 | $GroupMember 597 | 598 | # if we're doing manual recursion 599 | if ($PSBoundParameters['Recurse'] -and $MemberDN -and ($MemberObjectClass -match 'group')) { 600 | Write-Verbose "[Get-DomainGroupMember] Manually recursing on group: $MemberDN" 601 | $SearcherArguments['Identity'] = $MemberDN 602 | $Null = $SearcherArguments.Remove('Properties') 603 | Get-DomainGroupMember @SearcherArguments 604 | } 605 | } 606 | } 607 | $GroupSearcher.dispose() 608 | } 609 | } 610 | } 611 | function Get-DomainUser { 612 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 613 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] 614 | [OutputType('StrongView.User')] 615 | [OutputType('StrongView.User.Raw')] 616 | [CmdletBinding(DefaultParameterSetName = 'AllowDelegation')] 617 | Param( 618 | [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] 619 | [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] 620 | [String[]] 621 | $Identity, 622 | 623 | [Switch] 624 | $SPN, 625 | 626 | [Switch] 627 | $AdminCount, 628 | 629 | [Parameter(ParameterSetName = 'AllowDelegation')] 630 | [Switch] 631 | $AllowDelegation, 632 | 633 | [Parameter(ParameterSetName = 'DisallowDelegation')] 634 | [Switch] 635 | $DisallowDelegation, 636 | 637 | [Switch] 638 | $TrustedToAuth, 639 | 640 | [Alias('KerberosPreauthNotRequired', 'NoPreauth')] 641 | [Switch] 642 | $PreauthNotRequired, 643 | 644 | [ValidateNotNullOrEmpty()] 645 | [String] 646 | $Domain, 647 | 648 | [ValidateNotNullOrEmpty()] 649 | [Alias('Filter')] 650 | [String] 651 | $LDAPFilter, 652 | 653 | [ValidateNotNullOrEmpty()] 654 | [String[]] 655 | $Properties, 656 | 657 | [ValidateNotNullOrEmpty()] 658 | [Alias('ADSPath')] 659 | [String] 660 | $SearchBase, 661 | 662 | [ValidateNotNullOrEmpty()] 663 | [Alias('DomainController')] 664 | [String] 665 | $Server, 666 | 667 | [ValidateSet('Base', 'OneLevel', 'Subtree')] 668 | [String] 669 | $SearchScope = 'Subtree', 670 | 671 | [ValidateRange(1, 10000)] 672 | [Int] 673 | $ResultPageSize = 200, 674 | 675 | [ValidateRange(1, 10000)] 676 | [Int] 677 | $ServerTimeLimit, 678 | 679 | [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] 680 | [String] 681 | $SecurityMasks, 682 | 683 | [Switch] 684 | $Tombstone, 685 | 686 | [Alias('ReturnOne')] 687 | [Switch] 688 | $FindOne, 689 | 690 | [Management.Automation.PSCredential] 691 | [Management.Automation.CredentialAttribute()] 692 | $Credential = [Management.Automation.PSCredential]::Empty, 693 | 694 | [Switch] 695 | $Raw 696 | ) 697 | BEGIN { 698 | $SearcherArguments = @{} 699 | if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } 700 | if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } 701 | if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } 702 | if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } 703 | if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } 704 | if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } 705 | if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } 706 | if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } 707 | if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } 708 | if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } 709 | $UserSearcher = Get-DomainSearcher @SearcherArguments 710 | } 711 | 712 | PROCESS { 713 | if ($UserSearcher) { 714 | $IdentityFilter = '' 715 | $Filter = '' 716 | $Identity | Where-Object {$_} | ForEach-Object { 717 | $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') 718 | if ($IdentityInstance -match '^S-1-') { 719 | $IdentityFilter += "(objectsid=$IdentityInstance)" 720 | } 721 | elseif ($IdentityInstance -match '^CN=') { 722 | $IdentityFilter += "(distinguishedname=$IdentityInstance)" 723 | if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { 724 | # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname 725 | # and rebuild the domain searcher 726 | $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' 727 | Write-Verbose "[Get-DomainUser] Extracted domain '$IdentityDomain' from '$IdentityInstance'" 728 | $SearcherArguments['Domain'] = $IdentityDomain 729 | $UserSearcher = Get-DomainSearcher @SearcherArguments 730 | if (-not $UserSearcher) { 731 | Write-Warning "[Get-DomainUser] Unable to retrieve domain searcher for '$IdentityDomain'" 732 | } 733 | } 734 | } 735 | elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { 736 | $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' 737 | $IdentityFilter += "(objectguid=$GuidByteString)" 738 | } 739 | elseif ($IdentityInstance.Contains('\')) { 740 | $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical 741 | if ($ConvertedIdentityInstance) { 742 | $UserDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) 743 | $UserName = $IdentityInstance.Split('\')[1] 744 | $IdentityFilter += "(samAccountName=$UserName)" 745 | $SearcherArguments['Domain'] = $UserDomain 746 | Write-Verbose "[Get-DomainUser] Extracted domain '$UserDomain' from '$IdentityInstance'" 747 | $UserSearcher = Get-DomainSearcher @SearcherArguments 748 | } 749 | } 750 | else { 751 | $IdentityFilter += "(samAccountName=$IdentityInstance)" 752 | } 753 | } 754 | 755 | if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { 756 | $Filter += "(|$IdentityFilter)" 757 | } 758 | 759 | if ($PSBoundParameters['SPN']) { 760 | Write-Verbose '[Get-DomainUser] Searching for non-null service principal names' 761 | $Filter += '(servicePrincipalName=*)' 762 | } 763 | if ($PSBoundParameters['AllowDelegation']) { 764 | Write-Verbose '[Get-DomainUser] Searching for users who can be delegated' 765 | # negation of "Accounts that are sensitive and not trusted for delegation" 766 | $Filter += '(!(userAccountControl:1.2.840.113556.1.4.803:=1048574))' 767 | } 768 | if ($PSBoundParameters['DisallowDelegation']) { 769 | Write-Verbose '[Get-DomainUser] Searching for users who are sensitive and not trusted for delegation' 770 | $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=1048574)' 771 | } 772 | if ($PSBoundParameters['AdminCount']) { 773 | Write-Verbose '[Get-DomainUser] Searching for adminCount=1' 774 | $Filter += '(admincount=1)' 775 | } 776 | if ($PSBoundParameters['TrustedToAuth']) { 777 | Write-Verbose '[Get-DomainUser] Searching for users that are trusted to authenticate for other principals' 778 | $Filter += '(msds-allowedtodelegateto=*)' 779 | } 780 | if ($PSBoundParameters['PreauthNotRequired']) { 781 | Write-Verbose '[Get-DomainUser] Searching for user accounts that do not require kerberos preauthenticate' 782 | $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=4194304)' 783 | } 784 | if ($PSBoundParameters['LDAPFilter']) { 785 | Write-Verbose "[Get-DomainUser] Using additional LDAP filter: $LDAPFilter" 786 | $Filter += "$LDAPFilter" 787 | } 788 | 789 | # build the LDAP filter for the dynamic UAC filter value 790 | $UACFilter | Where-Object {$_} | ForEach-Object { 791 | if ($_ -match 'NOT_.*') { 792 | $UACField = $_.Substring(4) 793 | $UACValue = [Int]($UACEnum::$UACField) 794 | $Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=$UACValue))" 795 | } 796 | else { 797 | $UACValue = [Int]($UACEnum::$_) 798 | $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=$UACValue)" 799 | } 800 | } 801 | 802 | $UserSearcher.filter = "(&(samAccountType=805306368)$Filter)" 803 | Write-Verbose "[Get-DomainUser] filter string: $($UserSearcher.filter)" 804 | 805 | if ($PSBoundParameters['FindOne']) { $Results = $UserSearcher.FindOne() } 806 | else { $Results = $UserSearcher.FindAll() } 807 | $Results | Where-Object {$_} | ForEach-Object { 808 | if ($PSBoundParameters['Raw']) { 809 | # return raw result objects 810 | $User = $_ 811 | $User.PSObject.TypeNames.Insert(0, 'StrongView.User.Raw') 812 | } 813 | else { 814 | $User = Convert-LDAPProperty -Properties $_.Properties 815 | $User.PSObject.TypeNames.Insert(0, 'StrongView.User') 816 | } 817 | $User 818 | } 819 | if ($Results) { 820 | try { $Results.dispose() } 821 | catch { 822 | Write-Verbose "[Get-DomainUser] Error disposing of the Results object: $_" 823 | } 824 | } 825 | $UserSearcher.dispose() 826 | } 827 | } 828 | } 829 | function Get-DomainObject { 830 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] 831 | [OutputType('StrongView.ADObject')] 832 | [OutputType('StrongView.ADObject.Raw')] 833 | [CmdletBinding()] 834 | Param( 835 | [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] 836 | [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] 837 | [String[]] 838 | $Identity, 839 | 840 | [ValidateNotNullOrEmpty()] 841 | [String] 842 | $Domain, 843 | 844 | [ValidateNotNullOrEmpty()] 845 | [Alias('Filter')] 846 | [String] 847 | $LDAPFilter, 848 | 849 | [ValidateNotNullOrEmpty()] 850 | [String[]] 851 | $Properties, 852 | 853 | [ValidateNotNullOrEmpty()] 854 | [Alias('ADSPath')] 855 | [String] 856 | $SearchBase, 857 | 858 | [ValidateNotNullOrEmpty()] 859 | [Alias('DomainController')] 860 | [String] 861 | $Server, 862 | 863 | [ValidateSet('Base', 'OneLevel', 'Subtree')] 864 | [String] 865 | $SearchScope = 'Subtree', 866 | 867 | [ValidateRange(1, 10000)] 868 | [Int] 869 | $ResultPageSize = 200, 870 | 871 | [ValidateRange(1, 10000)] 872 | [Int] 873 | $ServerTimeLimit, 874 | 875 | [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] 876 | [String] 877 | $SecurityMasks, 878 | 879 | [Switch] 880 | $Tombstone, 881 | 882 | [Alias('ReturnOne')] 883 | [Switch] 884 | $FindOne, 885 | 886 | [Management.Automation.PSCredential] 887 | [Management.Automation.CredentialAttribute()] 888 | $Credential = [Management.Automation.PSCredential]::Empty, 889 | 890 | [Switch] 891 | $Raw 892 | ) 893 | 894 | BEGIN { 895 | $SearcherArguments = @{} 896 | if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } 897 | if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } 898 | if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } 899 | if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } 900 | if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } 901 | if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } 902 | if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } 903 | if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } 904 | if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } 905 | if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } 906 | $ObjectSearcher = Get-DomainSearcher @SearcherArguments 907 | } 908 | 909 | PROCESS { 910 | if ($ObjectSearcher) { 911 | $IdentityFilter = '' 912 | $Filter = '' 913 | $Identity | Where-Object {$_} | ForEach-Object { 914 | $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') 915 | if ($IdentityInstance -match '^S-1-') { 916 | $IdentityFilter += "(objectsid=$IdentityInstance)" 917 | } 918 | elseif ($IdentityInstance -match '^(CN|OU|DC)=') { 919 | $IdentityFilter += "(distinguishedname=$IdentityInstance)" 920 | if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { 921 | # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname 922 | # and rebuild the domain searcher 923 | $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' 924 | Write-Verbose "[Get-DomainObject] Extracted domain '$IdentityDomain' from '$IdentityInstance'" 925 | $SearcherArguments['Domain'] = $IdentityDomain 926 | $ObjectSearcher = Get-DomainSearcher @SearcherArguments 927 | if (-not $ObjectSearcher) { 928 | Write-Warning "[Get-DomainObject] Unable to retrieve domain searcher for '$IdentityDomain'" 929 | } 930 | } 931 | } 932 | elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { 933 | $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' 934 | $IdentityFilter += "(objectguid=$GuidByteString)" 935 | } 936 | elseif ($IdentityInstance.Contains('\')) { 937 | $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical 938 | if ($ConvertedIdentityInstance) { 939 | $ObjectDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) 940 | $ObjectName = $IdentityInstance.Split('\')[1] 941 | $IdentityFilter += "(samAccountName=$ObjectName)" 942 | $SearcherArguments['Domain'] = $ObjectDomain 943 | Write-Verbose "[Get-DomainObject] Extracted domain '$ObjectDomain' from '$IdentityInstance'" 944 | $ObjectSearcher = Get-DomainSearcher @SearcherArguments 945 | } 946 | } 947 | elseif ($IdentityInstance.Contains('.')) { 948 | $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance)(dnshostname=$IdentityInstance))" 949 | } 950 | else { 951 | $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance)(displayname=$IdentityInstance))" 952 | } 953 | } 954 | if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { 955 | $Filter += "(|$IdentityFilter)" 956 | } 957 | 958 | if ($PSBoundParameters['LDAPFilter']) { 959 | Write-Verbose "[Get-DomainObject] Using additional LDAP filter: $LDAPFilter" 960 | $Filter += "$LDAPFilter" 961 | } 962 | 963 | # build the LDAP filter for the dynamic UAC filter value 964 | $UACFilter | Where-Object {$_} | ForEach-Object { 965 | if ($_ -match 'NOT_.*') { 966 | $UACField = $_.Substring(4) 967 | $UACValue = [Int]($UACEnum::$UACField) 968 | $Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=$UACValue))" 969 | } 970 | else { 971 | $UACValue = [Int]($UACEnum::$_) 972 | $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=$UACValue)" 973 | } 974 | } 975 | 976 | if ($Filter -and $Filter -ne '') { 977 | $ObjectSearcher.filter = "(&$Filter)" 978 | } 979 | Write-Verbose "[Get-DomainObject] Get-DomainObject filter string: $($ObjectSearcher.filter)" 980 | 981 | if ($PSBoundParameters['FindOne']) { $Results = $ObjectSearcher.FindOne() } 982 | else { $Results = $ObjectSearcher.FindAll() } 983 | $Results | Where-Object {$_} | ForEach-Object { 984 | if ($PSBoundParameters['Raw']) { 985 | # return raw result objects 986 | $Object = $_ 987 | $Object.PSObject.TypeNames.Insert(0, 'StrongView.ADObject.Raw') 988 | } 989 | else { 990 | $Object = Convert-LDAPProperty -Properties $_.Properties 991 | $Object.PSObject.TypeNames.Insert(0, 'StrongView.ADObject') 992 | } 993 | $Object 994 | } 995 | if ($Results) { 996 | try { $Results.dispose() } 997 | catch { 998 | Write-Verbose "[Get-DomainObject] Error disposing of the Results object: $_" 999 | } 1000 | } 1001 | $ObjectSearcher.dispose() 1002 | } 1003 | } 1004 | } 1005 | function Get-LogonScripts { 1006 | [CmdletBinding()] 1007 | param() 1008 | 1009 | # Get the current domain name from the environment 1010 | # $currentDomain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() 1011 | $Domains = Get-ForestDomains 1012 | 1013 | foreach ($Domain in $Domains) { 1014 | # $SysvolScripts = '\\' + (Get-ADDomain).DNSRoot + '\sysvol\' + (Get-ADDomain).DNSRoot + '\scripts' 1015 | $SysvolScripts = "\\$($Domain.Name)\sysvol\$($Domain.Name)\scripts" 1016 | $ExtensionList = '.bat|.vbs|.ps1|.cmd|.kix' 1017 | $LogonScripts = try { Get-ChildItem -Path $SysvolScripts -Recurse | Where-Object {$_.Extension -match $ExtensionList} } catch {} 1018 | Write-Verbose "[+] Logon scripts:" 1019 | $LogonScripts | ForEach-Object { 1020 | Write-Verbose -Message "$($_.fullName)" 1021 | } 1022 | $LogonScripts | Sort-Object -Unique 1023 | } 1024 | } 1025 | function Get-GPOLogonScripts { 1026 | [CmdletBinding()] 1027 | param() 1028 | 1029 | # Get the current domain name from the environment 1030 | # $currentDomain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() 1031 | $Domains = Get-ForestDomains 1032 | 1033 | foreach ($Domain in $Domains) { 1034 | $Policies = Get-ChildItem "\\$($Domain.Name)\SysVol\$($Domain.Name)\Policies" -ErrorAction SilentlyContinue 1035 | $Policies | ForEach-Object { 1036 | $GPOLogonScripts = Get-Content -Path "$($_.FullName)\User\Scripts\scripts.ini" -ErrorAction SilentlyContinue | Select-String -Pattern "\\\\.*\.\w+" | ForEach-Object { $_.Matches.Value } 1037 | Write-Verbose "[+] GPO Logon scripts:" 1038 | $GPOLogonScripts | ForEach-Object { 1039 | Write-Verbose -Message "$($_.fullName)" 1040 | } 1041 | if ($GPOLogonScripts) { 1042 | Get-Item -Path $GPOLogonScripts | Sort-Object -Unique 1043 | } 1044 | } 1045 | } 1046 | } 1047 | function Get-NetlogonSysvol { 1048 | [CmdletBinding()] 1049 | param() 1050 | 1051 | $Domains = Get-ForestDomains 1052 | foreach ($Domain in $Domains){ 1053 | "\\$($Domain.Name)\NETLOGON" 1054 | "\\$($Domain.Name)\SYSVOL" 1055 | } 1056 | } 1057 | function Get-Art($Version) { 1058 | " 1059 | _______ _______ _______ _________ _______ _________ _______ _______ _ _________ _______ 1060 | ( ____ \( ____ \( ____ )\__ __/( ____ )\__ __/( ____ \( ____ \( ( /|\__ __/( ____ )|\ /| 1061 | | ( \/| ( \/| ( )| ) ( | ( )| ) ( | ( \/| ( \/| \ ( | ) ( | ( )|( \ / ) 1062 | | (_____ | | | (____)| | | | (____)| | | | (_____ | (__ | \ | | | | | (____)| \ (_) / 1063 | (_____ )| | | __) | | | _____) | | (_____ )| __) | (\ \) | | | | __) \ / 1064 | ) || | | (\ ( | | | ( | | ) || ( | | \ | | | | (\ ( ) ( 1065 | /\____) || (____/\| ) \ \_____) (___| ) | | /\____) || (____/\| ) \ | | | | ) \ \__ | | 1066 | \_______)(_______/|/ \__/\_______/|/ )_( \_______)(_______/|/ )_) )_( |/ \__/ \_/ 1067 | by: Spencer Alessi @techspence 1068 | v$Version 1069 | __,_______ 1070 | / __.==---/ * * * * * * 1071 | / (-' 1072 | `-' 1073 | Setting phasers to stun, please wait.. 1074 | " 1075 | } 1076 | function Convert-LDAPProperty { 1077 | <# 1078 | .SYNOPSIS 1079 | 1080 | Helper that converts specific LDAP property result fields and outputs 1081 | a custom psobject. 1082 | 1083 | Author: Will Schroeder (@harmj0y) 1084 | License: BSD 3-Clause 1085 | Required Dependencies: None 1086 | 1087 | .DESCRIPTION 1088 | 1089 | Converts a set of raw LDAP properties results from ADSI/LDAP searches 1090 | into a proper PSObject. Used by several of the Get-Domain* function. 1091 | 1092 | .PARAMETER Properties 1093 | 1094 | Properties object to extract out LDAP fields for display. 1095 | 1096 | .OUTPUTS 1097 | 1098 | System.Management.Automation.PSCustomObject 1099 | 1100 | A custom PSObject with LDAP hashtable properties translated. 1101 | #> 1102 | 1103 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] 1104 | [OutputType('System.Management.Automation.PSCustomObject')] 1105 | [CmdletBinding()] 1106 | Param( 1107 | [Parameter(Mandatory = $True, ValueFromPipeline = $True)] 1108 | [ValidateNotNullOrEmpty()] 1109 | $Properties 1110 | ) 1111 | 1112 | $ObjectProperties = @{} 1113 | 1114 | $Properties.PropertyNames | ForEach-Object { 1115 | if ($_ -ne 'adspath') { 1116 | if (($_ -eq 'objectsid') -or ($_ -eq 'sidhistory')) { 1117 | # convert all listed sids (i.e. if multiple are listed in sidHistory) 1118 | #$ObjectProperties[$_] = $Properties[$_] | ForEach-Object { (New-Object System.Security.Principal.SecurityIdentifier($_, 0)).Value } 1119 | } 1120 | elseif ($_ -eq 'grouptype') { 1121 | #$ObjectProperties[$_] = $Properties[$_][0] -as $GroupTypeEnum 1122 | } 1123 | elseif ($_ -eq 'samaccounttype') { 1124 | #$ObjectProperties[$_] = $Properties[$_][0] -as $SamAccountTypeEnum 1125 | } 1126 | elseif ($_ -eq 'objectguid') { 1127 | # convert the GUID to a string 1128 | #$ObjectProperties[$_] = (New-Object Guid (,$Properties[$_][0])).Guid 1129 | } 1130 | elseif ($_ -eq 'useraccountcontrol') { 1131 | #$ObjectProperties[$_] = $Properties[$_][0] -as $UACEnum 1132 | } 1133 | elseif ($_ -eq 'ntsecuritydescriptor') { 1134 | # $ObjectProperties[$_] = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Properties[$_][0], 0 1135 | $Descriptor = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Properties[$_][0], 0 1136 | if ($Descriptor.Owner) { 1137 | $ObjectProperties['Owner'] = $Descriptor.Owner 1138 | } 1139 | if ($Descriptor.Group) { 1140 | $ObjectProperties['Group'] = $Descriptor.Group 1141 | } 1142 | if ($Descriptor.DiscretionaryAcl) { 1143 | $ObjectProperties['DiscretionaryAcl'] = $Descriptor.DiscretionaryAcl 1144 | } 1145 | if ($Descriptor.SystemAcl) { 1146 | $ObjectProperties['SystemAcl'] = $Descriptor.SystemAcl 1147 | } 1148 | } 1149 | elseif ($_ -eq 'accountexpires') { 1150 | if ($Properties[$_][0] -gt [DateTime]::MaxValue.Ticks) { 1151 | $ObjectProperties[$_] = "NEVER" 1152 | } 1153 | else { 1154 | $ObjectProperties[$_] = [datetime]::fromfiletime($Properties[$_][0]) 1155 | } 1156 | } 1157 | elseif ( ($_ -eq 'lastlogon') -or ($_ -eq 'lastlogontimestamp') -or ($_ -eq 'pwdlastset') -or ($_ -eq 'lastlogoff') -or ($_ -eq 'badPasswordTime') ) { 1158 | # convert timestamps 1159 | if ($Properties[$_][0] -is [System.MarshalByRefObject]) { 1160 | # if we have a System.__ComObject 1161 | $Temp = $Properties[$_][0] 1162 | [Int32]$High = $Temp.GetType().InvokeMember('HighPart', [System.Reflection.BindingFlags]::GetProperty, $Null, $Temp, $Null) 1163 | [Int32]$Low = $Temp.GetType().InvokeMember('LowPart', [System.Reflection.BindingFlags]::GetProperty, $Null, $Temp, $Null) 1164 | $ObjectProperties[$_] = ([datetime]::FromFileTime([Int64]("0x{0:x8}{1:x8}" -f $High, $Low))) 1165 | } 1166 | else { 1167 | # otherwise just a string 1168 | $ObjectProperties[$_] = ([datetime]::FromFileTime(($Properties[$_][0]))) 1169 | } 1170 | } 1171 | elseif ($Properties[$_][0] -is [System.MarshalByRefObject]) { 1172 | # try to convert misc com objects 1173 | $Prop = $Properties[$_] 1174 | try { 1175 | $Temp = $Prop[$_][0] 1176 | [Int32]$High = $Temp.GetType().InvokeMember('HighPart', [System.Reflection.BindingFlags]::GetProperty, $Null, $Temp, $Null) 1177 | [Int32]$Low = $Temp.GetType().InvokeMember('LowPart', [System.Reflection.BindingFlags]::GetProperty, $Null, $Temp, $Null) 1178 | $ObjectProperties[$_] = [Int64]("0x{0:x8}{1:x8}" -f $High, $Low) 1179 | } 1180 | catch { 1181 | Write-Verbose "[Convert-LDAPProperty] error: $_" 1182 | $ObjectProperties[$_] = $Prop[$_] 1183 | } 1184 | } 1185 | elseif ($Properties[$_].count -eq 1) { 1186 | $ObjectProperties[$_] = $Properties[$_][0] 1187 | } 1188 | else { 1189 | $ObjectProperties[$_] = $Properties[$_] 1190 | } 1191 | } 1192 | } 1193 | try { 1194 | New-Object -TypeName PSObject -Property $ObjectProperties 1195 | } 1196 | catch { 1197 | Write-Warning "[Convert-LDAPProperty] Error parsing LDAP properties : $_" 1198 | } 1199 | } 1200 | function Find-AdminLogonScripts { 1201 | [CmdletBinding()] 1202 | param ( 1203 | [array]$AdminUsers 1204 | ) 1205 | # Enabled user accounts 1206 | Foreach ($Admin in $AdminUsers) { 1207 | $AdminLogonScripts = Get-DomainUser -Identity $Admin.MemberName | Where-Object { $_.scriptPath -ne $null} 1208 | 1209 | # "`n[!] Admins found with logon scripts" 1210 | $AdminLogonScripts | Foreach-object { 1211 | $Results = [ordered] @{ 1212 | Type = 'AdminLogonScript' 1213 | User = $_.distinguishedname 1214 | LogonScript = $_.scriptpath 1215 | } 1216 | [pscustomobject] $Results 1217 | } 1218 | } 1219 | } 1220 | function Find-LogonScriptCredentials { 1221 | [CmdletBinding()] 1222 | param( 1223 | [Parameter(Mandatory = $true)] 1224 | [array]$LogonScripts 1225 | ) 1226 | foreach ($script in $LogonScripts) { 1227 | # Write-Verbose -Message "Checking $($Script.FullName) for credentials.." 1228 | $Credentials = Get-Content -Path $script.FullName -ErrorAction SilentlyContinue | Select-String -Pattern "/user:","-AsPlainText" -AllMatches 1229 | if ($Credentials) { 1230 | # "`n[!] CREDENTIALS FOUND!" 1231 | $Credentials | ForEach-Object { 1232 | $Results = [ordered] @{ 1233 | Type = 'Credentials' 1234 | File = $script.FullName 1235 | Credential = $_ 1236 | } 1237 | [pscustomobject] $Results | Sort-Object -Unique 1238 | } 1239 | } 1240 | } 1241 | } 1242 | function Find-UNCScripts { 1243 | [CmdletBinding()] 1244 | param( 1245 | [Parameter(Mandatory = $true)] 1246 | [array]$LogonScripts 1247 | ) 1248 | 1249 | $ExcludedMatches = "copy|&|/command|%WINDIR%|-i|\*" 1250 | $UNCFiles = @() 1251 | [Array] $UNCFiles = foreach ($script in $LogonScripts) { 1252 | $MatchingUNCFiles = Get-Content $script.FullName -ErrorAction SilentlyContinue | Select-String -Pattern '\\\\.*\.\w+' | ForEach-Object { $_.Matches.Value } 1253 | $MatchingUNCFiles | Foreach-object { 1254 | if ($_ -match $ExcludedMatches) { 1255 | # don't collect 1256 | } else { 1257 | $_ 1258 | } 1259 | } 1260 | } 1261 | Write-Verbose "[+] UNC scripts:" 1262 | $UNCFiles | ForEach-Object { 1263 | Write-Verbose -Message "$_" 1264 | } 1265 | 1266 | $UNCFiles | Sort-Object -Unique 1267 | } 1268 | function Find-MappedDrives { 1269 | [CmdletBinding()] 1270 | param( 1271 | [Parameter(Mandatory = $true)] 1272 | [array]$LogonScripts 1273 | ) 1274 | 1275 | $Shares = @() 1276 | [Array] $Shares = foreach ($script in $LogonScripts) { 1277 | $temp = Get-Content $script.FullName -ErrorAction SilentlyContinue | Select-String -Pattern '.*net use.*','New-SmbMapping','.MapNetworkDrive' | ForEach-Object { $_.Matches.Value } 1278 | $temp = $temp | Select-String -Pattern '\\\\[\w\.\-]+\\[\w\-_\\.]+' | ForEach-Object { $_.Matches.Value } 1279 | $temp | ForEach-Object { 1280 | try { 1281 | $Path = "$_" 1282 | # Live servers we have access to 1283 | (Get-Item $Path -ErrorAction Stop).FullName 1284 | } catch [System.UnauthorizedAccessException] { 1285 | # Servers we either don't have access to or do not exist 1286 | Write-Verbose "$_ : You do not have access to $Directory`n" 1287 | } 1288 | catch { 1289 | Write-Verbose "An error occurred: $($_.Exception.Message)" 1290 | } 1291 | } 1292 | } 1293 | 1294 | Write-Verbose "[+] Mapped drives:" 1295 | $Shares | Sort-Object -Unique | ForEach-Object { 1296 | Write-Verbose -Message "$_" 1297 | } 1298 | 1299 | $Shares | Sort-Object -Unique 1300 | } 1301 | function Find-NonexistentShares { 1302 | [CmdletBinding()] 1303 | param ( 1304 | [array]$LogonScripts, 1305 | [array]$AdminUsers 1306 | ) 1307 | $LogonScriptShares = @() 1308 | [Array] $LogonScriptShares = foreach ($script in $LogonScripts) { 1309 | $temp = Get-Content $script.FullName -ErrorAction SilentlyContinue | Select-String -Pattern '.*net use.*','New-SmbMapping','.MapNetworkDrive' | ForEach-Object { $_.Matches.Value } 1310 | $temp = $temp | Select-String -Pattern '\\\\[\w\.\-]+\\[\w\-_\\.]+' | ForEach-Object { $_.Matches.Value } 1311 | $temp | ForEach-Object { 1312 | $ServerList = [ordered] @{ 1313 | Server = $_ -split '\\' | Where-Object {$_ -ne ""} | Select-Object -First 1 1314 | Share = $_ 1315 | Script = $Script.FullName 1316 | } 1317 | [pscustomobject] $ServerList 1318 | } 1319 | } 1320 | 1321 | $LogonScriptShares = $LogonScriptShares #| Sort-Object -Property Share -Unique 1322 | $AdminLogonScripts = Find-AdminLogonScripts -AdminUsers $AdminUsers 1323 | $Admins = 'No' 1324 | $Exploitable = 'No' 1325 | 1326 | $NonExistentShares = @() 1327 | [Array] $NonExistentShares = foreach ($LogonScriptShare in $LogonScriptShares) { 1328 | try { 1329 | $DNSEntry = [System.Net.DNS]::GetHostByName($LogonScriptShare.Server) 1330 | } catch { 1331 | $ServerWithoutDNS = $LogonScriptShare 1332 | } 1333 | 1334 | if ($ServerWithoutDNS) { 1335 | foreach ($AdminScript in $AdminLogonScripts) { 1336 | if ((Get-Item $ServerWithoutDNS.Script).Name -match $AdminScript.LogonScript){ 1337 | $Admins = $AdminScript.User 1338 | $Exploitable = 'Yes' 1339 | $Results = [ordered] @{ 1340 | Type = 'ExploitableLogonScript' 1341 | Server = $ServerWithoutDNS.Server 1342 | Share = $ServerWithoutDNS.Share 1343 | Script = $ServerWithoutDNS.Script 1344 | DNS = 'No' 1345 | Exploitable = $Exploitable 1346 | Admins = $Admins 1347 | } 1348 | } else { 1349 | $Admins = 'No' 1350 | $Exploitable = 'Potentially' 1351 | $Results = [ordered] @{ 1352 | Type = 'NonexistentShare' 1353 | Server = $ServerWithoutDNS.Server 1354 | Share = $ServerWithoutDNS.Share 1355 | Script = $ServerWithoutDNS.Script 1356 | DNS = 'No' 1357 | Exploitable = $Exploitable 1358 | Admins = $Admins 1359 | } 1360 | } 1361 | [pscustomobject] $Results 1362 | } 1363 | } 1364 | } 1365 | 1366 | $NonExistentShares 1367 | } 1368 | function Find-UnsafeLogonScriptPermissions { 1369 | [CmdletBinding()] 1370 | param( 1371 | [Parameter(Mandatory = $true)] 1372 | [array]$LogonScripts, 1373 | [Parameter(Mandatory = $true)] 1374 | [array]$SafeUsersList 1375 | ) 1376 | 1377 | $UnsafeRights = 'FullControl|Modify|Write' 1378 | $SafeUsers = $SafeUsersList 1379 | foreach ($script in $LogonScripts){ 1380 | # Write-Verbose -Message "Checking $($script.FullName) for unsafe permissions.." 1381 | $ACL = (Get-Acl $script.FullName -ErrorAction SilentlyContinue).Access 1382 | foreach ($entry in $ACL) { 1383 | if ($entry.FileSystemRights -match $UnsafeRights ` 1384 | -and $entry.AccessControlType -eq "Allow" ` 1385 | -and $entry.IdentityReference -notmatch $SafeUsers 1386 | ){ 1387 | $Results = [ordered] @{ 1388 | Type = 'UnsafeLogonScriptPermission' 1389 | File = $script.FullName 1390 | User = $entry.IdentityReference.Value 1391 | Rights = $entry.FileSystemRights 1392 | } 1393 | [pscustomobject] $Results | Sort-Object -Unique 1394 | } 1395 | } 1396 | } 1397 | } 1398 | function Find-UnsafeUNCPermissions { 1399 | [CmdletBinding()] 1400 | param( 1401 | [Parameter(Mandatory = $true)] 1402 | [array]$UNCScripts, 1403 | [Parameter(Mandatory = $true)] 1404 | [array]$SafeUsersList 1405 | ) 1406 | 1407 | $UnsafeRights = 'FullControl|Modify|Write' 1408 | $SafeUsers = $SafeUsersList 1409 | foreach ($script in $UNCScripts){ 1410 | # "Checking $script for unsafe permissions.." 1411 | $ACL = (Get-Acl $script -ErrorAction SilentlyContinue).Access 1412 | foreach ($entry in $ACL) { 1413 | if ($entry.FileSystemRights -match $UnsafeRights ` 1414 | -and $entry.AccessControlType -eq "Allow" ` 1415 | -and $entry.IdentityReference -notmatch $SafeUsers 1416 | ){ 1417 | if ($script -match 'NETLOGON|SYSVOL') { 1418 | $Type = 'UnsafeNetlogonSysvol' 1419 | $Results = [ordered] @{ 1420 | Type = $Type 1421 | Folder = $script 1422 | User = $entry.IdentityReference.Value 1423 | Rights = $entry.FileSystemRights 1424 | } 1425 | [pscustomobject] $Results | Sort-Object -Unique 1426 | } elseif ($script -match '\.') { 1427 | $Type = 'UnsafeUNCFilePermission' 1428 | $Results = [ordered] @{ 1429 | Type = $Type 1430 | File = $script 1431 | User = $entry.IdentityReference.Value 1432 | Rights = $entry.FileSystemRights 1433 | } 1434 | [pscustomobject] $Results | Sort-Object -Unique 1435 | } else { 1436 | $Type = 'UnsafeUNCFolderPermission' 1437 | $Results = [ordered] @{ 1438 | Type = $Type 1439 | Folder = $script 1440 | User = $entry.IdentityReference.Value 1441 | Rights = $entry.FileSystemRights 1442 | } 1443 | [pscustomobject] $Results | Sort-Object -Unique 1444 | } 1445 | } 1446 | } 1447 | } 1448 | } 1449 | function Find-UnsafeLogonScriptPermissions { 1450 | [CmdletBinding()] 1451 | param( 1452 | [Parameter(Mandatory = $true)] 1453 | [array]$LogonScripts, 1454 | [Parameter(Mandatory = $true)] 1455 | [array]$SafeUsersList 1456 | ) 1457 | 1458 | $UnsafeRights = 'FullControl|Modify|Write' 1459 | $SafeUsers = $SafeUsersList 1460 | foreach ($script in $LogonScripts){ 1461 | # Write-Verbose -Message "Checking $($script.FullName) for unsafe permissions.." 1462 | $ACL = try { (Get-Acl $script.FullName -ErrorAction SilentlyContinue).Access } catch{} 1463 | foreach ($entry in $ACL) { 1464 | if ($entry.FileSystemRights -match $UnsafeRights ` 1465 | -and $entry.AccessControlType -eq "Allow" ` 1466 | -and $entry.IdentityReference -notmatch $SafeUsers 1467 | ){ 1468 | $Results = [ordered] @{ 1469 | Type = 'UnsafeLogonScriptPermission' 1470 | File = $script.FullName 1471 | User = $entry.IdentityReference.Value 1472 | Rights = $entry.FileSystemRights 1473 | } 1474 | [pscustomobject] $Results | Sort-Object -Unique 1475 | } 1476 | } 1477 | } 1478 | } 1479 | function Find-UnsafeGPOLogonScriptPermissions { 1480 | [CmdletBinding()] 1481 | param( 1482 | [Parameter(Mandatory = $true)] 1483 | [array]$GPOLogonScripts, 1484 | [Parameter(Mandatory = $true)] 1485 | [array]$SafeUsersList 1486 | ) 1487 | 1488 | $UnsafeRights = 'FullControl|Modify|Write' 1489 | $SafeUsers = $SafeUsersList 1490 | foreach ($script in $GPOLogonScripts){ 1491 | # Write-Verbose -Message "Checking $($script.FullName) for unsafe permissions.." 1492 | $ACL = (Get-Acl $script.FullName -ErrorAction SilentlyContinue).Access 1493 | foreach ($entry in $ACL) { 1494 | if ($entry.FileSystemRights -match $UnsafeRights ` 1495 | -and $entry.AccessControlType -eq "Allow" ` 1496 | -and $entry.IdentityReference -notmatch $SafeUsers 1497 | ){ 1498 | $Results = [ordered] @{ 1499 | Type = 'UnsafeGPOLogonScriptPermission' 1500 | File = $script.FullName 1501 | User = $entry.IdentityReference.Value 1502 | Rights = $entry.FileSystemRights 1503 | } 1504 | [pscustomobject] $Results | Sort-Object -Unique 1505 | } 1506 | } 1507 | } 1508 | } 1509 | 1510 | function Show-Results { 1511 | [CmdletBinding()] 1512 | param( 1513 | [Parameter(Mandatory = $true)] 1514 | [array]$Results 1515 | ) 1516 | 1517 | $IssueTable = @{ 1518 | Credentials = 'Plaintext credentials' 1519 | NonexistentShare = 'Nonexistent Shares' 1520 | ExploitableLogonScript = 'Admins with logonscripts mapped from nonexistent share' 1521 | AdminLogonScript = 'Admins with logonscripts' 1522 | UnsafeNetlogonSysvol = 'Unsafe NETLOGON/SYSVOL permissions' 1523 | UnsafeUNCFilePermission = 'Unsafe UNC file permissions' 1524 | UnsafeUNCFolderPermission = 'Unsafe UNC folder permissions' 1525 | UnsafeLogonScriptPermission = 'Unsafe logon script permissions' 1526 | UnsafeGPOLogonScriptPermission = 'Unsafe GPO logon script permissions' 1527 | } 1528 | 1529 | if ($null -ne $Results) { 1530 | $UniqueResults = $Results.Type | Sort-Object -Unique 1531 | Write-Host "########## $($IssueTable[$UniqueResults]) ##########" 1532 | # $Results | Format-List 1533 | $Results | Format-Table -Wrap 1534 | } 1535 | } 1536 | 1537 | Get-Art -Version '0.6' 1538 | 1539 | $SafeUsers = 'NT AUTHORITY\\SYSTEM|Administrator|NT SERVICE\\TrustedInstaller|Domain Admins|Server Operators|Enterprise Admins|CREATOR OWNER' 1540 | $AdminGroups = @("Account Operators", "Administrators", "Backup Operators", "Cryptographic Operators", "Distributed COM Users", "Domain Admins", "Domain Controllers", "Enterprise Admins", "Print Operators", "Schema Admins", "Server Operators") 1541 | $AdminUsers = $AdminGroups | ForEach-Object { (Get-DomainGroupMember -Identity $_ -Recurse | Where-Object {$_.MemberObjectClass -eq 'user'})} | Sort-Object -Property MemberName -Unique 1542 | $AdminUsers | ForEach-Object { $SafeUsers = $SafeUsers + '|' + $_.MemberName } 1543 | 1544 | # Get a list of all logon scripts 1545 | $LogonScripts = Get-LogonScripts 1546 | 1547 | # Get a list of all GPO logon scripts 1548 | $GPOLogonScripts = Get-GPOLogonScripts 1549 | 1550 | if ($LogonScripts) { 1551 | # Find logon scripts (.bat, .vbs, .cmd, .ps1, .kix) that contain unc paths (e.g. \\srv01\fileshare1) 1552 | $UNCScripts = Find-UNCScripts -LogonScripts $LogonScripts 1553 | 1554 | # Find mapped drives (e.g. \\srv01\fileshare1, \\srv02\fileshare2\accounting) 1555 | $MappedDrives = Find-MappedDrives -LogonScripts $LogonScripts 1556 | 1557 | # Find nonexistent shares 1558 | $NonExistentSharesScripts = Find-NonexistentShares -LogonScripts $LogonScripts -AdminUsers $AdminUsers 1559 | $NonExistentShares = $NonExistentSharesScripts | Where-Object {$_.Exploitable -eq 'Potentially'} | Sort-Object -Property Share -Unique 1560 | 1561 | # Find unsafe permissions on logon scripts 1562 | $UnsafeLogonScripts = Find-UnsafeLogonScriptPermissions -LogonScripts $LogonScripts -SafeUsersList $SafeUsers 1563 | 1564 | # Find credentials in logon scripts 1565 | $Credentials = Find-LogonScriptCredentials -LogonScripts $LogonScripts 1566 | } else { 1567 | Write-Host "[i] No logon scripts found!`n" -ForegroundColor Cyan 1568 | } 1569 | 1570 | if ($NonExistentShares) { 1571 | # Find Exploitable logon scripts 1572 | $ExploitableLogonScripts = $NonExistentSharesScripts | Where-Object {$_.Exploitable -eq 'Yes'} 1573 | } else { 1574 | Write-Host "[i] No non-existent shares found!`n" -ForegroundColor Cyan 1575 | } 1576 | 1577 | if ($UNCScripts) { 1578 | # Find unsafe permissions for unc files found in logon scripts 1579 | $UnsafeUNCPermissions = Find-UnsafeUNCPermissions -UNCScripts $UNCScripts -SafeUsersList $SafeUsers 1580 | } else { 1581 | Write-Host "[i] No UNC files found!`n" -ForegroundColor Cyan 1582 | } 1583 | 1584 | if ($MappedDrives) { 1585 | # Find unsafe permissions for unc paths found in logon scripts 1586 | $UnsafeMappedDrives = Find-UnsafeUNCPermissions -UNCScripts $MappedDrives -SafeUsersList $SafeUsers 1587 | } else { 1588 | Write-Host "[i] No mapped drives found!`n" -ForegroundColor Cyan 1589 | } 1590 | 1591 | # Find unsafe NETLOGON & SYSVOL share permissions 1592 | $NetlogonSysvol = Get-NetlogonSysvol 1593 | $UnsafeNetlogonSysvol = Find-UnsafeUNCPermissions -UNCScripts $NetlogonSysvol -SafeUsersList $SafeUsers 1594 | 1595 | if ($GPOLogonScripts) { 1596 | # Find unsafe permissions on GPO logon scripts 1597 | $UnsafeGPOLogonScripts = Find-UnsafeGPOLogonScriptPermissions -GPOLogonScripts $GPOLogonScripts -SafeUsersList $SafeUsers 1598 | } else { 1599 | Write-Host "[i] No GPO logon scripts found!`n" -ForegroundColor Cyan 1600 | } 1601 | 1602 | # Find admins that have logon scripts assigned 1603 | $AdminLogonScripts = Find-AdminLogonScripts -AdminUsers $AdminUsers 1604 | 1605 | # Show all results 1606 | if ($UnsafeMappedDrives) {Show-Results $UnsafeMappedDrives} 1607 | if ($UnsafeLogonScripts) {Show-Results $UnsafeLogonScripts} 1608 | if ($UnsafeGPOLogonScripts) {Show-Results $UnsafeGPOLogonScripts} 1609 | if ($UnsafeUNCPermissions) {Show-Results $UnsafeUNCPermissions} 1610 | if ($UnsafeNetlogonSysvol) {Show-Results $UnsafeNetlogonSysvol} 1611 | if ($Credentials) {Show-Results $Credentials} 1612 | if ($NonExistentShares) {Show-Results $NonExistentShares} 1613 | if ($AdminLogonScripts) {Show-Results $AdminLogonScripts} 1614 | if ($ExploitableLogonScripts) {Show-Results $ExploitableLogonScripts} 1615 | 1616 | if ($SaveOutput) { 1617 | if ($UnsafeMappedDrives) { 1618 | Write-Host "[i] Saving UnsafeMappedDrives.csv to the current directory" -ForegroundColor Cyan 1619 | $UnsafeMappedDrives | Export-CSV -NoTypeInformation UnsafeMappedDrives.csv 1620 | } 1621 | if ($UnsafeLogonScripts) { 1622 | Write-Host "[i] Saving UnsafeLogonScripts.csv to the current directory" -ForegroundColor Cyan 1623 | $UnsafeLogonScripts | Export-CSV -NoTypeInformation UnsafeLogonScripts.csv 1624 | } 1625 | if ($UnsafeGPOLogonScripts) { 1626 | Write-Host "[i] Saving UnsafeGPOLogonScripts.csv to the current directory" -ForegroundColor Cyan 1627 | $UnsafeGPOLogonScripts | Export-Csv -NoTypeInformation UnsafeGPOLogonScripts.csv 1628 | } 1629 | if ($UnsafeUNCPermissions) { 1630 | Write-Host "[i] Saving UnsafeUNCPermissions.csv to the current directory" -ForegroundColor Cyan 1631 | $UnsafeUNCPermissions | Export-CSV -NoTypeInformation UnsafeUNCPermissions.csv 1632 | } 1633 | if ($UnsafeNetlogonSysvol) { 1634 | Write-Host "[i] Saving UnsafeNetlogonSysvol.csv to the current directory" -ForegroundColor Cyan 1635 | $UnsafeNetlogonSysvol | Export-Csv -NoTypeInformation UnsafeNetlogonSysvol.csv 1636 | } 1637 | if ($AdminLogonScripts) { 1638 | Write-Host "[i] Saving AdminLogonScripts.csv to the current directory" -ForegroundColor Cyan 1639 | $AdminLogonScripts | Export-CSV -NoTypeInformation AdminLogonScripts.csv 1640 | } 1641 | if ($Credentials) { 1642 | Write-Host "[i] Saving Credentials.csv to the current directory" -ForegroundColor Cyan 1643 | $Credentials | Export-CSV -NoTypeInformation Credentials.csv 1644 | } 1645 | if ($NonExistentShares) { 1646 | Write-Host "[i] Saving NonExistentShares.csv to the current directory" -ForegroundColor Cyan 1647 | $NonExistentShares | Export-CSV -NoTypeInformation NonExistentShares.csv 1648 | } 1649 | if ($ExploitableLogonScripts) { 1650 | Write-Host "[i] Saving ExploitableLogonScripts.csv to the current directory" -ForegroundColor Cyan 1651 | $ExploitableLogonScripts | Export-CSV -NoTypeInformation ExploitableLogonScripts.csv 1652 | } 1653 | 1654 | Get-ChildItem -Filter "*.csv" -File 1655 | } 1656 | } --------------------------------------------------------------------------------