Private/AD/Core/Get-ADLogonScripts.ps1
|
# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0 # https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/ # AI/LLM use: see AI-USAGE.md for required attribution function Get-ADLogonScripts { [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$Connection, [switch]$Quiet ) $result = @{ UserScripts = @() NetlogonPath = '' SysvolPath = '' NetlogonFiles = @() NetlogonPermissions = $null SysvolPermissions = $null ScriptAnalysis = @() Errors = @{} } $domainDN = $Connection.DomainDN $domainName = ($domainDN -replace '^DC=', '' -replace ',DC=', '.').ToLower() # ── 1. Query user logon scripts ───────────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Querying user logon script assignments' } try { $searchRoot = New-LdapSearchRoot -Connection $Connection -SearchBase $domainDN $scriptUsers = Invoke-LdapQuery -SearchRoot $searchRoot ` -Filter '(&(objectCategory=person)(objectClass=user)(scriptPath=*))' ` -Properties @('samaccountname', 'scriptpath', 'distinguishedname') # Group by script path and count users per script $scriptGroups = @{} foreach ($user in $scriptUsers) { $scriptPath = ($user['scriptpath'] ?? '').Trim() if ([string]::IsNullOrWhiteSpace($scriptPath)) { continue } if (-not $scriptGroups.ContainsKey($scriptPath)) { $scriptGroups[$scriptPath] = 0 } $scriptGroups[$scriptPath]++ } $userScripts = [System.Collections.Generic.List[hashtable]]::new() foreach ($kv in $scriptGroups.GetEnumerator()) { $userScripts.Add(@{ ScriptPath = $kv.Key UserCount = $kv.Value }) } $result.UserScripts = @($userScripts | Sort-Object { $_.UserCount } -Descending) if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message "Found $($userScripts.Count) unique logon script(s)" ` -Detail "referenced by $($scriptUsers.Count) user(s)" } } catch { Write-Warning "Failed to query user logon scripts: $_" $result.Errors['UserScripts'] = $_.Exception.Message } # ── 2. Determine share paths ──────────────────────────────────────── $result.NetlogonPath = "\\$domainName\NETLOGON" $result.SysvolPath = "\\$domainName\SYSVOL" # ── 3. Enumerate NETLOGON files ───────────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Enumerating NETLOGON share contents' } try { $netlogonPath = $result.NetlogonPath if (Test-Path -LiteralPath $netlogonPath -ErrorAction SilentlyContinue) { $netlogonFiles = [System.Collections.Generic.List[hashtable]]::new() $fileItems = Get-ChildItem -LiteralPath $netlogonPath -Recurse -File -ErrorAction SilentlyContinue foreach ($file in $fileItems) { $netlogonFiles.Add(@{ Path = $file.FullName RelativePath = $file.FullName.Substring($netlogonPath.Length).TrimStart('\') Extension = $file.Extension.ToLower() Size = $file.Length LastWriteTime = $file.LastWriteTime }) } $result.NetlogonFiles = @($netlogonFiles) if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message "Found $($netlogonFiles.Count) file(s) in NETLOGON" } } else { Write-Verbose "NETLOGON share not accessible: $netlogonPath" $result.Errors['NetlogonFiles'] = "NETLOGON share not accessible at $netlogonPath" } } catch { Write-Verbose "Failed to enumerate NETLOGON files: $_" $result.Errors['NetlogonFiles'] = $_.Exception.Message } # ── 4. NETLOGON and SYSVOL permissions ────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Reading share permissions' } try { if (Test-Path -LiteralPath $result.NetlogonPath -ErrorAction SilentlyContinue) { $netlogonAcl = Get-Acl -LiteralPath $result.NetlogonPath -ErrorAction SilentlyContinue if ($netlogonAcl) { $result.NetlogonPermissions = @{ Owner = $netlogonAcl.Owner AccessRules = @($netlogonAcl.Access | ForEach-Object { @{ Identity = $_.IdentityReference.Value Rights = $_.FileSystemRights.ToString() AccessType = $_.AccessControlType.ToString() IsInherited = $_.IsInherited } }) } } } } catch { Write-Verbose "Failed to read NETLOGON permissions: $_" $result.Errors['NetlogonPermissions'] = $_.Exception.Message } try { if (Test-Path -LiteralPath $result.SysvolPath -ErrorAction SilentlyContinue) { $sysvolAcl = Get-Acl -LiteralPath $result.SysvolPath -ErrorAction SilentlyContinue if ($sysvolAcl) { $result.SysvolPermissions = @{ Owner = $sysvolAcl.Owner AccessRules = @($sysvolAcl.Access | ForEach-Object { @{ Identity = $_.IdentityReference.Value Rights = $_.FileSystemRights.ToString() AccessType = $_.AccessControlType.ToString() IsInherited = $_.IsInherited } }) } } } } catch { Write-Verbose "Failed to read SYSVOL permissions: $_" $result.Errors['SysvolPermissions'] = $_.Exception.Message } # ── 5. Script content analysis ────────────────────────────────────── if (-not $Quiet) { Write-ProgressLine -Phase RECON -Message 'Analyzing logon script contents' } # Extensions eligible for content analysis $scriptExtensions = @('.bat', '.cmd', '.vbs', '.ps1', '.js', '.wsf', '.kix') # LOLBin patterns $lolbinPatterns = @( @{ Name = 'certutil'; Pattern = '\bcertutil\b' } @{ Name = 'bitsadmin'; Pattern = '\bbitsadmin\b' } @{ Name = 'mshta'; Pattern = '\bmshta\b' } @{ Name = 'regsvr32'; Pattern = '\bregsvr32\b' } @{ Name = 'rundll32'; Pattern = '\brundll32\b' } @{ Name = 'wscript'; Pattern = '\bwscript\b' } @{ Name = 'cscript'; Pattern = '\bcscript\b' } @{ Name = 'msiexec'; Pattern = '\bmsiexec\b' } @{ Name = 'powershell -enc'; Pattern = 'powershell[^|\r\n]*-enc' } @{ Name = 'powershell -nop'; Pattern = 'powershell[^|\r\n]*-nop' } @{ Name = 'powershell downloadstring'; Pattern = 'powershell[^|\r\n]*downloadstring' } @{ Name = 'Invoke-WebRequest'; Pattern = '\bInvoke-WebRequest\b' } @{ Name = 'Invoke-Expression'; Pattern = '\bInvoke-Expression\b' } @{ Name = 'iex'; Pattern = '\biex\b' } @{ Name = 'Start-BitsTransfer'; Pattern = '\bStart-BitsTransfer\b' } @{ Name = 'cmd /c'; Pattern = '\bcmd\b[^|\r\n]*/c\b' } @{ Name = 'wget'; Pattern = '\bwget\b' } @{ Name = 'curl'; Pattern = '\bcurl\b' } ) # Credential patterns $credentialPatterns = @( @{ Name = 'password='; Pattern = 'password\s*=' } @{ Name = 'passwd='; Pattern = 'passwd\s*=' } @{ Name = 'pwd='; Pattern = 'pwd\s*=' } @{ Name = '-Password with value'; Pattern = '-Password\s+[''\"]\S+' } @{ Name = 'ConvertTo-SecureString plaintext'; Pattern = 'ConvertTo-SecureString\s+-String' } @{ Name = 'net use /user:'; Pattern = 'net\s+use.*\/user:' } @{ Name = '/password:'; Pattern = '/password:' } @{ Name = '-Credential PSCredential'; Pattern = '-Credential.*PSCredential' } @{ Name = 'connection string password'; Pattern = '(pwd|password)\s*=\s*[^;''"]+' } ) # UNC path pattern $uncPathPattern = '\\\\[a-zA-Z0-9_.%-]+\\[a-zA-Z0-9$_.%-]+' # URL pattern $urlPattern = 'https?://[a-zA-Z0-9._%-]+(?:/[^\s''"]*)?' $maxFileSize = 1MB # 1 MB limit to avoid memory issues $scriptAnalysis = [System.Collections.Generic.List[hashtable]]::new() # Gather all script files from NETLOGON for analysis $scriptFiles = @($result.NetlogonFiles | Where-Object { $_.Extension -in $scriptExtensions -and $_.Size -le $maxFileSize -and $_.Size -gt 0 }) $analyzed = 0 foreach ($scriptFile in $scriptFiles) { $analyzed++ if (-not $Quiet -and ($analyzed % 25 -eq 0 -or $analyzed -eq 1)) { Write-ProgressLine -Phase RECON -Message 'Analyzing script' ` -Detail "$analyzed / $($scriptFiles.Count)" } $analysis = @{ FilePath = $scriptFile.Path RelativePath = $scriptFile.RelativePath Extension = $scriptFile.Extension Size = $scriptFile.Size LastWriteTime = $scriptFile.LastWriteTime HardcodedCredentials = $false CredentialMatches = @() PlaintextPasswords = $false PasswordMatches = @() LOLBinsUsage = $false LOLBinsFound = @() ExternalResources = $false ExternalResourceList = @() WorldWritable = $false UNCPaths = @() } try { $content = [System.IO.File]::ReadAllText($scriptFile.Path) $lines = $content -split '\r?\n' # Check for credential patterns $credMatches = [System.Collections.Generic.List[hashtable]]::new() foreach ($credPattern in $credentialPatterns) { for ($i = 0; $i -lt $lines.Count; $i++) { if ($lines[$i] -match $credPattern.Pattern) { $credMatches.Add(@{ Pattern = $credPattern.Name LineNumber = $i + 1 Line = $lines[$i].Trim().Substring(0, [Math]::Min($lines[$i].Trim().Length, 200)) }) } } } if ($credMatches.Count -gt 0) { $analysis.HardcodedCredentials = $true $analysis.CredentialMatches = @($credMatches) $analysis.PlaintextPasswords = $true $analysis.PasswordMatches = @($credMatches) } # Check for LOLBins $lolbinsFound = [System.Collections.Generic.List[string]]::new() foreach ($lolbin in $lolbinPatterns) { if ($content -match $lolbin.Pattern) { $lolbinsFound.Add($lolbin.Name) } } if ($lolbinsFound.Count -gt 0) { $analysis.LOLBinsUsage = $true $analysis.LOLBinsFound = @($lolbinsFound) } # Extract UNC paths $uncMatches = [regex]::Matches($content, $uncPathPattern) $uncPaths = @($uncMatches | ForEach-Object { $_.Value } | Sort-Object -Unique) $analysis.UNCPaths = $uncPaths # Extract URLs $urlMatches = [regex]::Matches($content, $urlPattern) $urls = @($urlMatches | ForEach-Object { $_.Value } | Sort-Object -Unique) # Determine external resources: UNC paths and URLs not pointing to domain controllers $externalResources = [System.Collections.Generic.List[string]]::new() foreach ($unc in $uncPaths) { # Extract the server portion from the UNC path if ($unc -match '^\\\\([^\\]+)') { $server = $Matches[1].ToLower() # Consider it external if it doesn't match the domain name or common domain patterns if ($server -ne $domainName -and $server -notmatch "^dc\d*\." -and $server -notmatch "\.$([regex]::Escape($domainName))$" -and $server -ne 'localhost' -and $server -ne '127.0.0.1') { $externalResources.Add($unc) } } } foreach ($url in $urls) { $externalResources.Add($url) } if ($externalResources.Count -gt 0) { $analysis.ExternalResources = $true $analysis.ExternalResourceList = @($externalResources) } # Check if file is world-writable try { $fileAcl = Get-Acl -LiteralPath $scriptFile.Path -ErrorAction SilentlyContinue if ($fileAcl) { foreach ($ace in $fileAcl.Access) { $identity = $ace.IdentityReference.Value $isWorldIdentity = $identity -match '\\Everyone$' -or $identity -eq 'Everyone' -or $identity -match '\\Users$' -or $identity -match 'BUILTIN\\Users' -or $identity -eq 'S-1-1-0' -or $identity -eq 'S-1-5-32-545' if ($isWorldIdentity -and $ace.AccessControlType -eq 'Allow' -and (($ace.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::Write) -or ($ace.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::Modify) -or ($ace.FileSystemRights -band [System.Security.AccessControl.FileSystemRights]::FullControl))) { $analysis.WorldWritable = $true break } } } } catch { Write-Verbose "Failed to read ACL for script $($scriptFile.Path): $_" } } catch { Write-Verbose "Failed to analyze script $($scriptFile.Path): $_" $result.Errors["ScriptAnalysis:$($scriptFile.RelativePath)"] = $_.Exception.Message } $scriptAnalysis.Add($analysis) } $result.ScriptAnalysis = @($scriptAnalysis) # ── Summary ───────────────────────────────────────────────────────── if (-not $Quiet) { $credCount = @($scriptAnalysis | Where-Object { $_.HardcodedCredentials }).Count $lolCount = @($scriptAnalysis | Where-Object { $_.LOLBinsUsage }).Count $wwCount = @($scriptAnalysis | Where-Object { $_.WorldWritable }).Count $summary = "Logon script analysis complete: $($scriptAnalysis.Count) script(s) analyzed" $details = @() if ($credCount -gt 0) { $details += "$credCount with credentials" } if ($lolCount -gt 0) { $details += "$lolCount with LOLBins" } if ($wwCount -gt 0) { $details += "$wwCount world-writable" } if ($details.Count -gt 0) { $summary += " ($($details -join ', '))" } Write-ProgressLine -Phase RECON -Message $summary } return $result } |