diff --git a/Invoke-ScriptSentry.ps1 b/Invoke-ScriptSentry.ps1 index 0f8ebb0..ab7424c 100644 --- a/Invoke-ScriptSentry.ps1 +++ b/Invoke-ScriptSentry.ps1 @@ -4,9 +4,9 @@ function Invoke-ScriptSentry{ ScriptSentry finds misconfigured and dangerous logon scripts. .DESCRIPTION -ScriptSentry searches the NETLOGON share & Group Policy to +ScriptSentry searches the NETLOGON share & Group Policy to 1) identify plaintext credentials in logon scripts - 2) identify admins that have logon script set + 2) identify admins that have logon script set 3) identify scripts and shares that may have dangerous permissions .EXAMPLE @@ -18,39 +18,34 @@ Invoke-ScriptSentry | Out-File c:\temp\ScriptSentry.txt .EXAMPLE Invoke-ScriptSentry -SaveOutput $true +.EXAMPLE +Invoke-ScriptSentry -SaveOutput $true -OutputDirectory c:\ScriptSentry + #> [CmdletBinding(DefaultParameterSetName = 'Default')] Param( [boolean]$SaveOutput = $false, - + [string]$OutputDirectory = '', + [Management.Automation.PSCredential] [Management.Automation.CredentialAttribute()] $Credential = [Management.Automation.PSCredential]::Empty, - [Parameter(Mandatory = $true, - ParameterSetName = 'socks')] - [boolean] - $socks, - - [Parameter(Mandatory = $true, - ParameterSetName = 'socks')] - [string] - $Server, - - [Parameter(Mandatory = $true, - ParameterSetName = 'socks')] - [string] - $Domain + [Parameter(Mandatory = $true, ParameterSetName = 'Server')] + [string]$Server, + + [Parameter(Mandatory = $true, ParameterSetName = 'Server')] + [string]$Domain ) - -function Get-ForestDomains { + +function Get-ScriptSentryForestDomains { [CmdletBinding()] param() $forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() $forest.Domains } -function Get-Domain { +function Get-ScriptSentryDomain { [OutputType([System.DirectoryServices.ActiveDirectory.Domain])] [CmdletBinding()] Param( @@ -67,7 +62,7 @@ function Get-Domain { PROCESS { if ($PSBoundParameters['Credential']) { - Write-Verbose '[Get-Domain] Using alternate credentials for Get-Domain' + Write-Verbose '[Get-ScriptSentryDomain] Using alternate credentials for Get-ScriptSentryDomain' if ($PSBoundParameters['Domain']) { $TargetDomain = $Domain @@ -75,7 +70,7 @@ function Get-Domain { else { # if no domain is supplied, extract the logon domain from the PSCredential passed $TargetDomain = $Credential.GetNetworkCredential().Domain - Write-Verbose "[Get-Domain] Extracted domain '$TargetDomain' from -Credential" + Write-Verbose "[Get-ScriptSentryDomain] Extracted domain '$TargetDomain' from -Credential" } $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $TargetDomain, $Credential.UserName, $Credential.GetNetworkCredential().Password) @@ -84,7 +79,7 @@ function Get-Domain { [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) } catch { - 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: $_" + Write-Verbose "[Get-ScriptSentryDomain] The specified domain '$TargetDomain' does not exist, could not be contacted, there isn't an existing trust, or the specified credentials are invalid: $_" } } elseif ($PSBoundParameters['Domain']) { @@ -93,7 +88,7 @@ function Get-Domain { [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) } catch { - Write-Verbose "[Get-Domain] The specified domain '$Domain' does not exist, could not be contacted, or there isn't an existing trust : $_" + Write-Verbose "[Get-ScriptSentryDomain] The specified domain '$Domain' does not exist, could not be contacted, or there isn't an existing trust : $_" } } else { @@ -101,12 +96,12 @@ function Get-Domain { [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() } catch { - Write-Verbose "[Get-Domain] Error retrieving the current domain: $_" + Write-Verbose "[Get-ScriptSentryDomain] Error retrieving the current domain: $_" } } } } -function Get-DomainSearcher { +function Get-ScriptSentryDomainSearcher { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] [OutputType('System.DirectoryServices.DirectorySearcher')] [CmdletBinding()] @@ -176,8 +171,8 @@ function Get-DomainSearcher { } } elseif ($PSBoundParameters['Credential']) { - # if not -Domain is specified, but -Credential is, try to retrieve the current domain name with Get-Domain - $DomainObject = Get-Domain -Credential $Credential + # if not -Domain is specified, but -Credential is, try to retrieve the current domain name with Get-ScriptSentryDomain + $DomainObject = Get-ScriptSentryDomain -Credential $Credential $BindServer = ($DomainObject.PdcRoleOwner).Name $TargetDomain = $DomainObject.Name } @@ -189,9 +184,9 @@ function Get-DomainSearcher { } } else { - # otherwise, resort to Get-Domain to retrieve the current domain object - write-verbose "get-domain" - $DomainObject = Get-Domain + # otherwise, resort to Get-ScriptSentryDomain to retrieve the current domain object + write-verbose "Get-ScriptSentryDomain" + $DomainObject = Get-ScriptSentryDomain $BindServer = ($DomainObject.PdcRoleOwner).Name $TargetDomain = $DomainObject.Name } @@ -243,10 +238,10 @@ function Get-DomainSearcher { } $SearchString += $DN - Write-Verbose "[Get-DomainSearcher] search base: $SearchString" + Write-Verbose "[Get-ScriptSentryDomainSearcher] search base: $SearchString" if ($Credential -ne [Management.Automation.PSCredential]::Empty) { - Write-Verbose "[Get-DomainSearcher] Using alternate credentials for LDAP connection" + Write-Verbose "[Get-ScriptSentryDomainSearcher] Using alternate credentials for LDAP connection" # bind to the inital search object using alternate credentials $DomainObject = New-Object DirectoryServices.DirectoryEntry($SearchString, $Credential.UserName, $Credential.GetNetworkCredential().Password) $Searcher = New-Object System.DirectoryServices.DirectorySearcher($DomainObject) @@ -292,10 +287,10 @@ function Get-DomainSearcher { $Searcher } } -function Get-DomainGroupMember { +function Get-ScriptSentryDomainGroupMember { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] - [OutputType('StrongView.GroupMember')] + [OutputType('ScriptSentry.GroupMember')] [CmdletBinding(DefaultParameterSetName = 'None')] Param( [Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] @@ -375,7 +370,7 @@ function Get-DomainGroupMember { } PROCESS { - $GroupSearcher = Get-DomainSearcher @SearcherArguments + $GroupSearcher = Get-ScriptSentryDomainSearcher @SearcherArguments if ($GroupSearcher) { if ($PSBoundParameters['RecurseUsingMatchingRule']) { $SearcherArguments['Identity'] = $Identity @@ -383,7 +378,7 @@ function Get-DomainGroupMember { $Group = Get-DomainGroup @SearcherArguments if (-not $Group) { - Write-Warning "[Get-DomainGroupMember] Error searching for group with identity: $Identity" + Write-Warning "[Get-ScriptSentryDomainGroupMember] Error searching for group with identity: $Identity" } else { $GroupFoundName = $Group.properties.item('samaccountname')[0] @@ -398,7 +393,7 @@ function Get-DomainGroupMember { $GroupFoundDomain = $GroupFoundDN.SubString($GroupFoundDN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' } } - Write-Verbose "[Get-DomainGroupMember] Using LDAP matching rule to recurse on '$GroupFoundDN', only user accounts will be returned." + Write-Verbose "[Get-ScriptSentryDomainGroupMember] Using LDAP matching rule to recurse on '$GroupFoundDN', only user accounts will be returned." $GroupSearcher.filter = "(&(samAccountType=805306368)(memberof:1.2.840.113556.1.4.1941:=$GroupFoundDN))" $GroupSearcher.PropertiesToLoad.AddRange(('distinguishedName')) $Members = $GroupSearcher.FindAll() | ForEach-Object {$_.Properties.distinguishedname[0]} @@ -419,11 +414,11 @@ function Get-DomainGroupMember { # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname # and rebuild the domain searcher $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' - Write-Verbose "[Get-DomainGroupMember] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + Write-Verbose "[Get-ScriptSentryDomainGroupMember] Extracted domain '$IdentityDomain' from '$IdentityInstance'" $SearcherArguments['Domain'] = $IdentityDomain - $GroupSearcher = Get-DomainSearcher @SearcherArguments + $GroupSearcher = Get-ScriptSentryDomainSearcher @SearcherArguments if (-not $GroupSearcher) { - Write-Warning "[Get-DomainGroupMember] Unable to retrieve domain searcher for '$IdentityDomain'" + Write-Warning "[Get-ScriptSentryDomainGroupMember] Unable to retrieve domain searcher for '$IdentityDomain'" } } } @@ -438,8 +433,8 @@ function Get-DomainGroupMember { $GroupName = $IdentityInstance.Split('\')[1] $IdentityFilter += "(samAccountName=$GroupName)" $SearcherArguments['Domain'] = $GroupDomain - Write-Verbose "[Get-DomainGroupMember] Extracted domain '$GroupDomain' from '$IdentityInstance'" - $GroupSearcher = Get-DomainSearcher @SearcherArguments + Write-Verbose "[Get-ScriptSentryDomainGroupMember] Extracted domain '$GroupDomain' from '$IdentityInstance'" + $GroupSearcher = Get-ScriptSentryDomainSearcher @SearcherArguments } } else { @@ -452,17 +447,17 @@ function Get-DomainGroupMember { } if ($PSBoundParameters['LDAPFilter']) { - Write-Verbose "[Get-DomainGroupMember] Using additional LDAP filter: $LDAPFilter" + Write-Verbose "[Get-ScriptSentryDomainGroupMember] Using additional LDAP filter: $LDAPFilter" $Filter += "$LDAPFilter" } $GroupSearcher.filter = "(&(objectCategory=group)$Filter)" - Write-Verbose "[Get-DomainGroupMember] Get-DomainGroupMember filter string: $($GroupSearcher.filter)" + Write-Verbose "[Get-ScriptSentryDomainGroupMember] Get-ScriptSentryDomainGroupMember filter string: $($GroupSearcher.filter)" try { $Result = $GroupSearcher.FindOne() } catch { - Write-Warning "[Get-DomainGroupMember] Error searching for group with identity '$Identity': $_" + Write-Warning "[Get-ScriptSentryDomainGroupMember] Error searching for group with identity '$Identity': $_" $Members = @() } @@ -530,7 +525,7 @@ function Get-DomainGroupMember { $ObjectSearcherArguments['Identity'] = $Member $ObjectSearcherArguments['Raw'] = $True $ObjectSearcherArguments['Properties'] = 'distinguishedname,cn,samaccountname,objectsid,objectclass' - $Object = Get-DomainObject @ObjectSearcherArguments + $Object = Get-ScriptSentryDomainObject @ObjectSearcherArguments $Properties = $Object.Properties } @@ -560,12 +555,12 @@ function Get-DomainGroupMember { $MemberDomain = $MemberSimpleName.Split('@')[1] } else { - Write-Warning "[Get-DomainGroupMember] Error converting $MemberDN" + Write-Warning "[Get-ScriptSentryDomainGroupMember] Error converting $MemberDN" $MemberDomain = $Null } } catch { - Write-Warning "[Get-DomainGroupMember] Error converting $MemberDN" + Write-Warning "[Get-ScriptSentryDomainGroupMember] Error converting $MemberDN" $MemberDomain = $Null } } @@ -611,15 +606,15 @@ function Get-DomainGroupMember { $GroupMember | Add-Member Noteproperty 'MemberDistinguishedName' $MemberDN $GroupMember | Add-Member Noteproperty 'MemberObjectClass' $MemberObjectClass $GroupMember | Add-Member Noteproperty 'MemberSID' $MemberSID - $GroupMember.PSObject.TypeNames.Insert(0, 'StrongView.GroupMember') + $GroupMember.PSObject.TypeNames.Insert(0, 'ScriptSentry.GroupMember') $GroupMember # if we're doing manual recursion if ($PSBoundParameters['Recurse'] -and $MemberDN -and ($MemberObjectClass -match 'group')) { - Write-Verbose "[Get-DomainGroupMember] Manually recursing on group: $MemberDN" + Write-Verbose "[Get-ScriptSentryDomainGroupMember] Manually recursing on group: $MemberDN" $SearcherArguments['Identity'] = $MemberDN $Null = $SearcherArguments.Remove('Properties') - Get-DomainGroupMember @SearcherArguments + Get-ScriptSentryDomainGroupMember @SearcherArguments } } } @@ -627,89 +622,89 @@ function Get-DomainGroupMember { } } } -function Get-DomainUser { +function Get-ScriptSentryDomainUser { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] - [OutputType('StrongView.User')] - [OutputType('StrongView.User.Raw')] + [OutputType('ScriptSentry.User')] + [OutputType('ScriptSentry.User.Raw')] [CmdletBinding(DefaultParameterSetName = 'AllowDelegation')] Param( [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] [String[]] $Identity, - + [Switch] $SPN, - + [Switch] $AdminCount, - + [Parameter(ParameterSetName = 'AllowDelegation')] [Switch] $AllowDelegation, - + [Parameter(ParameterSetName = 'DisallowDelegation')] [Switch] $DisallowDelegation, - + [Switch] $TrustedToAuth, - + [Alias('KerberosPreauthNotRequired', 'NoPreauth')] [Switch] $PreauthNotRequired, - + [ValidateNotNullOrEmpty()] [String] $Domain, - + [ValidateNotNullOrEmpty()] [Alias('Filter')] [String] $LDAPFilter, - + [ValidateNotNullOrEmpty()] [String[]] $Properties, - + [ValidateNotNullOrEmpty()] [Alias('ADSPath')] [String] $SearchBase, - + [ValidateNotNullOrEmpty()] [Alias('DomainController')] [String] $Server, - + [ValidateSet('Base', 'OneLevel', 'Subtree')] [String] $SearchScope = 'Subtree', - + [ValidateRange(1, 10000)] [Int] $ResultPageSize = 200, - + [ValidateRange(1, 10000)] [Int] $ServerTimeLimit, - + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] [String] $SecurityMasks, - + [Switch] $Tombstone, - + [Alias('ReturnOne')] [Switch] $FindOne, - + [Management.Automation.PSCredential] [Management.Automation.CredentialAttribute()] $Credential = [Management.Automation.PSCredential]::Empty, - + [Switch] $Raw ) @@ -725,9 +720,9 @@ function Get-DomainUser { if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } - $UserSearcher = Get-DomainSearcher @SearcherArguments + $UserSearcher = Get-ScriptSentryDomainSearcher @SearcherArguments } - + PROCESS { if ($UserSearcher) { $IdentityFilter = '' @@ -743,11 +738,11 @@ function Get-DomainUser { # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname # and rebuild the domain searcher $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' - Write-Verbose "[Get-DomainUser] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + Write-Verbose "[Get-ScriptSentryDomainUser] Extracted domain '$IdentityDomain' from '$IdentityInstance'" $SearcherArguments['Domain'] = $IdentityDomain - $UserSearcher = Get-DomainSearcher @SearcherArguments + $UserSearcher = Get-ScriptSentryDomainSearcher @SearcherArguments if (-not $UserSearcher) { - Write-Warning "[Get-DomainUser] Unable to retrieve domain searcher for '$IdentityDomain'" + Write-Warning "[Get-ScriptSentryDomainUser] Unable to retrieve domain searcher for '$IdentityDomain'" } } } @@ -762,49 +757,49 @@ function Get-DomainUser { $UserName = $IdentityInstance.Split('\')[1] $IdentityFilter += "(samAccountName=$UserName)" $SearcherArguments['Domain'] = $UserDomain - Write-Verbose "[Get-DomainUser] Extracted domain '$UserDomain' from '$IdentityInstance'" - $UserSearcher = Get-DomainSearcher @SearcherArguments + Write-Verbose "[Get-ScriptSentryDomainUser] Extracted domain '$UserDomain' from '$IdentityInstance'" + $UserSearcher = Get-ScriptSentryDomainSearcher @SearcherArguments } } else { $IdentityFilter += "(samAccountName=$IdentityInstance)" } } - + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { $Filter += "(|$IdentityFilter)" } - + if ($PSBoundParameters['SPN']) { - Write-Verbose '[Get-DomainUser] Searching for non-null service principal names' + Write-Verbose '[Get-ScriptSentryDomainUser] Searching for non-null service principal names' $Filter += '(servicePrincipalName=*)' } if ($PSBoundParameters['AllowDelegation']) { - Write-Verbose '[Get-DomainUser] Searching for users who can be delegated' + Write-Verbose '[Get-ScriptSentryDomainUser] Searching for users who can be delegated' # negation of "Accounts that are sensitive and not trusted for delegation" $Filter += '(!(userAccountControl:1.2.840.113556.1.4.803:=1048574))' } if ($PSBoundParameters['DisallowDelegation']) { - Write-Verbose '[Get-DomainUser] Searching for users who are sensitive and not trusted for delegation' + Write-Verbose '[Get-ScriptSentryDomainUser] Searching for users who are sensitive and not trusted for delegation' $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=1048574)' } if ($PSBoundParameters['AdminCount']) { - Write-Verbose '[Get-DomainUser] Searching for adminCount=1' + Write-Verbose '[Get-ScriptSentryDomainUser] Searching for adminCount=1' $Filter += '(admincount=1)' } if ($PSBoundParameters['TrustedToAuth']) { - Write-Verbose '[Get-DomainUser] Searching for users that are trusted to authenticate for other principals' + Write-Verbose '[Get-ScriptSentryDomainUser] Searching for users that are trusted to authenticate for other principals' $Filter += '(msds-allowedtodelegateto=*)' } if ($PSBoundParameters['PreauthNotRequired']) { - Write-Verbose '[Get-DomainUser] Searching for user accounts that do not require kerberos preauthenticate' + Write-Verbose '[Get-ScriptSentryDomainUser] Searching for user accounts that do not require kerberos preauthenticate' $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=4194304)' } if ($PSBoundParameters['LDAPFilter']) { - Write-Verbose "[Get-DomainUser] Using additional LDAP filter: $LDAPFilter" + Write-Verbose "[Get-ScriptSentryDomainUser] Using additional LDAP filter: $LDAPFilter" $Filter += "$LDAPFilter" } - + # build the LDAP filter for the dynamic UAC filter value $UACFilter | Where-Object {$_} | ForEach-Object { if ($_ -match 'NOT_.*') { @@ -817,38 +812,38 @@ function Get-DomainUser { $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=$UACValue)" } } - + $UserSearcher.filter = "(&(samAccountType=805306368)$Filter)" - Write-Verbose "[Get-DomainUser] filter string: $($UserSearcher.filter)" - + Write-Verbose "[Get-ScriptSentryDomainUser] filter string: $($UserSearcher.filter)" + if ($PSBoundParameters['FindOne']) { $Results = $UserSearcher.FindOne() } else { $Results = $UserSearcher.FindAll() } $Results | Where-Object {$_} | ForEach-Object { if ($PSBoundParameters['Raw']) { # return raw result objects $User = $_ - $User.PSObject.TypeNames.Insert(0, 'StrongView.User.Raw') + $User.PSObject.TypeNames.Insert(0, 'ScriptSentry.User.Raw') } else { $User = Convert-LDAPProperty -Properties $_.Properties - $User.PSObject.TypeNames.Insert(0, 'StrongView.User') + $User.PSObject.TypeNames.Insert(0, 'ScriptSentry.User') } $User } if ($Results) { try { $Results.dispose() } catch { - Write-Verbose "[Get-DomainUser] Error disposing of the Results object: $_" + Write-Verbose "[Get-ScriptSentryDomainUser] Error disposing of the Results object: $_" } } $UserSearcher.dispose() } } } -function Get-DomainObject { +function Get-ScriptSentryDomainObject { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] - [OutputType('StrongView.ADObject')] - [OutputType('StrongView.ADObject.Raw')] + [OutputType('ScriptSentry.ADObject')] + [OutputType('ScriptSentry.ADObject.Raw')] [CmdletBinding()] Param( [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] @@ -922,7 +917,7 @@ function Get-DomainObject { if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } - $ObjectSearcher = Get-DomainSearcher @SearcherArguments + $ObjectSearcher = Get-ScriptSentryDomainSearcher @SearcherArguments } PROCESS { @@ -940,11 +935,11 @@ function Get-DomainObject { # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname # and rebuild the domain searcher $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' - Write-Verbose "[Get-DomainObject] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + Write-Verbose "[Get-ScriptSentryDomainObject] Extracted domain '$IdentityDomain' from '$IdentityInstance'" $SearcherArguments['Domain'] = $IdentityDomain - $ObjectSearcher = Get-DomainSearcher @SearcherArguments + $ObjectSearcher = Get-ScriptSentryDomainSearcher @SearcherArguments if (-not $ObjectSearcher) { - Write-Warning "[Get-DomainObject] Unable to retrieve domain searcher for '$IdentityDomain'" + Write-Warning "[Get-ScriptSentryDomainObject] Unable to retrieve domain searcher for '$IdentityDomain'" } } } @@ -959,8 +954,8 @@ function Get-DomainObject { $ObjectName = $IdentityInstance.Split('\')[1] $IdentityFilter += "(samAccountName=$ObjectName)" $SearcherArguments['Domain'] = $ObjectDomain - Write-Verbose "[Get-DomainObject] Extracted domain '$ObjectDomain' from '$IdentityInstance'" - $ObjectSearcher = Get-DomainSearcher @SearcherArguments + Write-Verbose "[Get-ScriptSentryDomainObject] Extracted domain '$ObjectDomain' from '$IdentityInstance'" + $ObjectSearcher = Get-ScriptSentryDomainSearcher @SearcherArguments } } elseif ($IdentityInstance.Contains('.')) { @@ -975,7 +970,7 @@ function Get-DomainObject { } if ($PSBoundParameters['LDAPFilter']) { - Write-Verbose "[Get-DomainObject] Using additional LDAP filter: $LDAPFilter" + Write-Verbose "[Get-ScriptSentryDomainObject] Using additional LDAP filter: $LDAPFilter" $Filter += "$LDAPFilter" } @@ -995,7 +990,7 @@ function Get-DomainObject { if ($Filter -and $Filter -ne '') { $ObjectSearcher.filter = "(&$Filter)" } - Write-Verbose "[Get-DomainObject] Get-DomainObject filter string: $($ObjectSearcher.filter)" + Write-Verbose "[Get-ScriptSentryDomainObject] Get-ScriptSentryDomainObject filter string: $($ObjectSearcher.filter)" if ($PSBoundParameters['FindOne']) { $Results = $ObjectSearcher.FindOne() } else { $Results = $ObjectSearcher.FindAll() } @@ -1003,18 +998,18 @@ function Get-DomainObject { if ($PSBoundParameters['Raw']) { # return raw result objects $Object = $_ - $Object.PSObject.TypeNames.Insert(0, 'StrongView.ADObject.Raw') + $Object.PSObject.TypeNames.Insert(0, 'ScriptSentry.ADObject.Raw') } else { $Object = Convert-LDAPProperty -Properties $_.Properties - $Object.PSObject.TypeNames.Insert(0, 'StrongView.ADObject') + $Object.PSObject.TypeNames.Insert(0, 'ScriptSentry.ADObject') } $Object } if ($Results) { try { $Results.dispose() } catch { - Write-Verbose "[Get-DomainObject] Error disposing of the Results object: $_" + Write-Verbose "[Get-ScriptSentryDomainObject] Error disposing of the Results object: $_" } } $ObjectSearcher.dispose() @@ -1040,18 +1035,22 @@ function Get-LogonScripts { if ($Domain) { $Domains = $Domain } else { - $Domains = Get-ForestDomains + $Domains = Get-ScriptSentryForestDomains } foreach ($Domain in $Domains) { + $DomainName = if ($Domain -is [string]) { $Domain } else { $Domain.Name } # $SysvolScripts = '\\' + (Get-ADDomain).DNSRoot + '\sysvol\' + (Get-ADDomain).DNSRoot + '\scripts' if ($Server) { - Get-ChildItem "\\$Server\sysvol\$($Domain.Name)\scripts" -ErrorAction SilentlyContinue + $SysvolScripts = "\\$Server\sysvol\$DomainName\scripts" } else { - $SysvolScripts = Get-ChildItem "\\$($Domain.Name)\sysvol\$($Domain.Name)\scripts" -ErrorAction SilentlyContinue + $SysvolScripts = "\\$DomainName\sysvol\$DomainName\scripts" } $ExtensionList = '.bat|.vbs|.ps1|.cmd|.kix' - $LogonScripts = try { Get-ChildItem -Path $SysvolScripts -Recurse | Where-Object {$_.Extension -match $ExtensionList} } catch {} + $LogonScripts = try { + Get-ChildItem -LiteralPath $SysvolScripts -Recurse -File -ErrorAction Stop | + Where-Object { $_.Extension -match $ExtensionList } + } catch {} Write-Verbose "[+] Logon scripts:" $LogonScripts | ForEach-Object { Write-Verbose -Message "$($_.fullName)" @@ -1078,16 +1077,17 @@ function Get-GPOLogonScripts { if ($Domain) { $Domains = $Domain } else { - $Domains = Get-ForestDomains + $Domains = Get-ScriptSentryForestDomains } foreach ($Domain in $Domains) { + $DomainName = if ($Domain -is [string]) { $Domain } else { $Domain.Name } if ($Server) { - $Policies = Get-ChildItem "\\$Server\SYSVOL\$($Domain.Name)\POLICIES\" -ErrorAction SilentlyContinue + $Policies = Get-ChildItem "\\$Server\SYSVOL\$DomainName\POLICIES\" -ErrorAction SilentlyContinue } else { - $Policies = Get-ChildItem "\\$($Domain.Name)\SysVol\$($Domain.Name)\Policies" -ErrorAction SilentlyContinue + $Policies = Get-ChildItem "\\$DomainName\SysVol\$DomainName\Policies" -ErrorAction SilentlyContinue } - $Policies | ForEach-Object { + $Policies | ForEach-Object { $GPOLogonScripts = Get-Content -Path "$($_.FullName)\User\Scripts\scripts.ini" -ErrorAction SilentlyContinue | Select-String -Pattern "\\\\.*\.\w+" | ForEach-Object { $_.Matches.Value } Write-Verbose "[+] GPO Logon scripts:" $GPOLogonScripts | ForEach-Object { @@ -1112,30 +1112,31 @@ function Get-NetlogonSysvol { if ($Domain) { $Domains = $Domain } else { - $Domains = Get-ForestDomains + $Domains = Get-ScriptSentryForestDomains } foreach ($Domain in $Domains){ + $DomainName = if ($Domain -is [string]) { $Domain } else { $Domain.Name } if ($Server) { "\\$Server\NETLOGON" "\\$Server\SYSVOL" } else { - "\\$($Domain.Name)\NETLOGON" - "\\$($Domain.Name)\SYSVOL" + "\\$DomainName\NETLOGON" + "\\$DomainName\SYSVOL" } } } function Get-Art($Version) { " - _______ _______ _______ _________ _______ _________ _______ _______ _ _________ _______ + _______ _______ _______ _________ _______ _________ _______ _______ _ _________ _______ ( ____ \( ____ \( ____ )\__ __/( ____ )\__ __/( ____ \( ____ \( ( /|\__ __/( ____ )|\ /| | ( \/| ( \/| ( )| ) ( | ( )| ) ( | ( \/| ( \/| \ ( | ) ( | ( )|( \ / ) -| (_____ | | | (____)| | | | (____)| | | | (_____ | (__ | \ | | | | | (____)| \ (_) / -(_____ )| | | __) | | | _____) | | (_____ )| __) | (\ \) | | | | __) \ / - ) || | | (\ ( | | | ( | | ) || ( | | \ | | | | (\ ( ) ( -/\____) || (____/\| ) \ \_____) (___| ) | | /\____) || (____/\| ) \ | | | | ) \ \__ | | -\_______)(_______/|/ \__/\_______/|/ )_( \_______)(_______/|/ )_) )_( |/ \__/ \_/ - by: Spencer Alessi @techspence - v$Version +| (_____ | | | (____)| | | | (____)| | | | (_____ | (__ | \ | | | | | (____)| \ (_) / +(_____ )| | | __) | | | _____) | | (_____ )| __) | (\ \) | | | | __) \ / + ) || | | (\ ( | | | ( | | ) || ( | | \ | | | | (\ ( ) ( +/\____) || (____/\| ) \ \_____) (___| ) | | /\____) || (____/\| ) \ | | | | ) \ \__ | | +\_______)(_______/|/ \__/\_______/|/ )_( \_______)(_______/|/ )_) )_( |/ \__/ \_/ + by: Spencer Alessi @techspence + v$Version __,_______ / __.==---/ * * * * * * / (-' @@ -1150,14 +1151,14 @@ function Convert-LDAPProperty { Helper that converts specific LDAP property result fields and outputs a custom psobject. -Author: Will Schroeder (@harmj0y) -License: BSD 3-Clause -Required Dependencies: None +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None .DESCRIPTION Converts a set of raw LDAP properties results from ADSI/LDAP searches -into a proper PSObject. Used by several of the Get-Domain* function. +into a proper PSObject. Used by several of the Get-ScriptSentryDomain* function. .PARAMETER Properties @@ -1181,92 +1182,92 @@ A custom PSObject with LDAP hashtable properties translated. PROCESS { $ObjectProperties = @{} - + $Properties.PropertyNames | ForEach-Object { if ($_ -ne 'adspath') { if (($_ -eq 'objectsid') -or ($_ -eq 'sidhistory')) { # convert all listed sids (i.e. if multiple are listed in sidHistory) #$ObjectProperties[$_] = $Properties[$_] | ForEach-Object { (New-Object System.Security.Principal.SecurityIdentifier($_, 0)).Value } } - elseif ($_ -eq 'grouptype') { - #$ObjectProperties[$_] = $Properties[$_][0] -as $GroupTypeEnum + elseif ($_ -eq 'grouptype') { + #$ObjectProperties[$_] = $Properties[$_][0] -as $GroupTypeEnum + } + elseif ($_ -eq 'samaccounttype') { + #$ObjectProperties[$_] = $Properties[$_][0] -as $SamAccountTypeEnum + } + elseif ($_ -eq 'objectguid') { + # convert the GUID to a string + #$ObjectProperties[$_] = (New-Object Guid (,$Properties[$_][0])).Guid + } + elseif ($_ -eq 'useraccountcontrol') { + #$ObjectProperties[$_] = $Properties[$_][0] -as $UACEnum + } + elseif ($_ -eq 'ntsecuritydescriptor') { + # $ObjectProperties[$_] = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Properties[$_][0], 0 + $Descriptor = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Properties[$_][0], 0 + if ($Descriptor.Owner) { + $ObjectProperties['Owner'] = $Descriptor.Owner } - elseif ($_ -eq 'samaccounttype') { - #$ObjectProperties[$_] = $Properties[$_][0] -as $SamAccountTypeEnum + if ($Descriptor.Group) { + $ObjectProperties['Group'] = $Descriptor.Group } - elseif ($_ -eq 'objectguid') { - # convert the GUID to a string - #$ObjectProperties[$_] = (New-Object Guid (,$Properties[$_][0])).Guid + if ($Descriptor.DiscretionaryAcl) { + $ObjectProperties['DiscretionaryAcl'] = $Descriptor.DiscretionaryAcl } - elseif ($_ -eq 'useraccountcontrol') { - #$ObjectProperties[$_] = $Properties[$_][0] -as $UACEnum + if ($Descriptor.SystemAcl) { + $ObjectProperties['SystemAcl'] = $Descriptor.SystemAcl } - elseif ($_ -eq 'ntsecuritydescriptor') { - # $ObjectProperties[$_] = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Properties[$_][0], 0 - $Descriptor = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Properties[$_][0], 0 - if ($Descriptor.Owner) { - $ObjectProperties['Owner'] = $Descriptor.Owner - } - if ($Descriptor.Group) { - $ObjectProperties['Group'] = $Descriptor.Group - } - if ($Descriptor.DiscretionaryAcl) { - $ObjectProperties['DiscretionaryAcl'] = $Descriptor.DiscretionaryAcl - } - if ($Descriptor.SystemAcl) { - $ObjectProperties['SystemAcl'] = $Descriptor.SystemAcl - } + } + elseif ($_ -eq 'accountexpires') { + if ($Properties[$_][0] -gt [DateTime]::MaxValue.Ticks) { + $ObjectProperties[$_] = "NEVER" } - elseif ($_ -eq 'accountexpires') { - if ($Properties[$_][0] -gt [DateTime]::MaxValue.Ticks) { - $ObjectProperties[$_] = "NEVER" - } - else { - $ObjectProperties[$_] = [datetime]::fromfiletime($Properties[$_][0]) - } + else { + $ObjectProperties[$_] = [datetime]::fromfiletime($Properties[$_][0]) } - elseif ( ($_ -eq 'lastlogon') -or ($_ -eq 'lastlogontimestamp') -or ($_ -eq 'pwdlastset') -or ($_ -eq 'lastlogoff') -or ($_ -eq 'badPasswordTime') ) { - # convert timestamps - if ($Properties[$_][0] -is [System.MarshalByRefObject]) { - # if we have a System.__ComObject - $Temp = $Properties[$_][0] - [Int32]$High = $Temp.GetType().InvokeMember('HighPart', [System.Reflection.BindingFlags]::GetProperty, $Null, $Temp, $Null) - [Int32]$Low = $Temp.GetType().InvokeMember('LowPart', [System.Reflection.BindingFlags]::GetProperty, $Null, $Temp, $Null) - $ObjectProperties[$_] = ([datetime]::FromFileTime([Int64]("0x{0:x8}{1:x8}" -f $High, $Low))) - } - else { - # otherwise just a string - $ObjectProperties[$_] = ([datetime]::FromFileTime(($Properties[$_][0]))) - } + } + elseif ( ($_ -eq 'lastlogon') -or ($_ -eq 'lastlogontimestamp') -or ($_ -eq 'pwdlastset') -or ($_ -eq 'lastlogoff') -or ($_ -eq 'badPasswordTime') ) { + # convert timestamps + if ($Properties[$_][0] -is [System.MarshalByRefObject]) { + # if we have a System.__ComObject + $Temp = $Properties[$_][0] + [Int32]$High = $Temp.GetType().InvokeMember('HighPart', [System.Reflection.BindingFlags]::GetProperty, $Null, $Temp, $Null) + [Int32]$Low = $Temp.GetType().InvokeMember('LowPart', [System.Reflection.BindingFlags]::GetProperty, $Null, $Temp, $Null) + $ObjectProperties[$_] = ([datetime]::FromFileTime([Int64]("0x{0:x8}{1:x8}" -f $High, $Low))) } - elseif ($Properties[$_][0] -is [System.MarshalByRefObject]) { - # try to convert misc com objects - $Prop = $Properties[$_] - try { - $Temp = $Prop[$_][0] - [Int32]$High = $Temp.GetType().InvokeMember('HighPart', [System.Reflection.BindingFlags]::GetProperty, $Null, $Temp, $Null) - [Int32]$Low = $Temp.GetType().InvokeMember('LowPart', [System.Reflection.BindingFlags]::GetProperty, $Null, $Temp, $Null) - $ObjectProperties[$_] = [Int64]("0x{0:x8}{1:x8}" -f $High, $Low) - } - catch { - Write-Verbose "[Convert-LDAPProperty] error: $_" - $ObjectProperties[$_] = $Prop[$_] - } + else { + # otherwise just a string + $ObjectProperties[$_] = ([datetime]::FromFileTime(($Properties[$_][0]))) } - elseif ($Properties[$_].count -eq 1) { - $ObjectProperties[$_] = $Properties[$_][0] + } + elseif ($Properties[$_][0] -is [System.MarshalByRefObject]) { + # try to convert misc com objects + $Prop = $Properties[$_] + try { + $Temp = $Prop[$_][0] + [Int32]$High = $Temp.GetType().InvokeMember('HighPart', [System.Reflection.BindingFlags]::GetProperty, $Null, $Temp, $Null) + [Int32]$Low = $Temp.GetType().InvokeMember('LowPart', [System.Reflection.BindingFlags]::GetProperty, $Null, $Temp, $Null) + $ObjectProperties[$_] = [Int64]("0x{0:x8}{1:x8}" -f $High, $Low) } - else { - $ObjectProperties[$_] = $Properties[$_] + catch { + Write-Verbose "[Convert-LDAPProperty] error: $_" + $ObjectProperties[$_] = $Prop[$_] } + } + elseif ($Properties[$_].count -eq 1) { + $ObjectProperties[$_] = $Properties[$_][0] + } + else { + $ObjectProperties[$_] = $Properties[$_] + } } } - try { - New-Object -TypeName PSObject -Property $ObjectProperties - } - catch { - Write-Warning "[Convert-LDAPProperty] Error parsing LDAP properties : $_" - } + try { + New-Object -TypeName PSObject -Property $ObjectProperties + } + catch { + Write-Warning "[Convert-LDAPProperty] Error parsing LDAP properties : $_" + } } } function Find-AdminLogonScripts { @@ -1276,18 +1277,28 @@ function Find-AdminLogonScripts { [Management.Automation.PSCredential] [Management.Automation.CredentialAttribute()] $Credential = [Management.Automation.PSCredential]::Empty - ) + ) # Enabled user accounts Foreach ($Admin in $AdminUsers) { - $AdminLogonScripts = Get-DomainUser -Identity $Admin.MemberName | Where-Object { $null -ne $_.scriptPath } - + $UserArguments = @{ Identity = $Admin.MemberName } + if ($Credential -ne [Management.Automation.PSCredential]::Empty) { + $UserArguments.Credential = $Credential + } + $AdminLogonScripts = Get-ScriptSentryDomainUser @UserArguments | Where-Object { $null -ne $_.scriptPath } + # "`n[!] Admins found with logon scripts" $AdminLogonScripts | Foreach-object { - $Results = [ordered] @{ + <# $Results = [ordered] @{ Type = 'AdminLogonScript' User = $_.distinguishedname LogonScript = $_.scriptpath + }#> + $Results = [ordered] @{ + Misconfiguration = 'LSM-Admins-1' + Description = "Admins with logonscripts" + Details = "$($_.distinguishedname) - $($_.scriptpath)" } + [pscustomobject] $Results } } @@ -1307,10 +1318,15 @@ function Find-LogonScriptCredentials { if ($Credentials) { # "`n[!] CREDENTIALS FOUND!" $Credentials | ForEach-Object { - $Results = [ordered] @{ + <#$Results = [ordered] @{ Type = 'Credentials' File = $script.FullName Credential = $_ + }#> + $Results = [ordered] @{ + Misconfiguration = 'LSM-Creds' + Description = "Plaintext credentials within a logon script" + Details = "$($script.FullName) - $_" } [pscustomobject] $Results | Sort-Object -Unique } @@ -1330,7 +1346,9 @@ function Find-UNCScripts { $ExcludedMatches = "copy|&|/command|%WINDIR%|-i|\*" $UNCFiles = @() [Array] $UNCFiles = foreach ($script in $LogonScripts) { - $MatchingUNCFiles = Get-Content $script.FullName -ErrorAction SilentlyContinue | Select-String -Pattern '\\\\.*\.\w+' | ForEach-Object { $_.Matches.Value } + $MatchingUNCFiles = Get-Content $script.FullName -ErrorAction SilentlyContinue | + Select-String -AllMatches -Pattern '\\\\[\w.\-]+\\[^\s"'']+?\.[A-Za-z0-9]{1,10}(?=[\s"'']|$)' | + ForEach-Object { $_.Matches.Value } $MatchingUNCFiles | Foreach-object { if ($_ -match $ExcludedMatches) { # don't collect @@ -1343,7 +1361,7 @@ function Find-UNCScripts { $UNCFiles | ForEach-Object { Write-Verbose -Message "$_" } - + $UNCFiles | Sort-Object -Unique } function Find-MappedDrives { @@ -1358,21 +1376,12 @@ function Find-MappedDrives { $Shares = @() [Array] $Shares = foreach ($script in $LogonScripts) { - $temp = Get-Content $script.FullName -ErrorAction SilentlyContinue | Select-String -Pattern '.*net use.*','New-SmbMapping','.MapNetworkDrive' | ForEach-Object { $_.Matches.Value } - $temp = $temp | Select-String -Pattern '\\\\[\w\.\-]+\\[\w\-_\\.]+' | ForEach-Object { $_.Matches.Value } - $temp | ForEach-Object { - try { - $Path = "$_" - # Live servers we have access to - (Get-Item $Path -ErrorAction Stop).FullName - } catch [System.UnauthorizedAccessException] { - # Servers we either don't have access to or do not exist - Write-Verbose "$_ : You do not have access to $Directory`n" - } - catch { - Write-Verbose "An error occurred: $($_.Exception.Message)" + Get-Content $script.FullName -ErrorAction SilentlyContinue | + Select-String -Pattern 'net\s+use|New-SmbMapping|MapNetworkDrive' | + ForEach-Object { + [regex]::Matches($_.Line, '\\\\[\w.\-]+\\[\w$_.\-]+') | + ForEach-Object Value } - } } Write-Verbose "[+] Mapped drives:" @@ -1386,7 +1395,6 @@ function Find-NonexistentShares { [CmdletBinding()] param ( [array]$LogonScripts, - [array]$AdminUsers, [Management.Automation.PSCredential] [Management.Automation.CredentialAttribute()] $Credential = [Management.Automation.PSCredential]::Empty @@ -1397,7 +1405,7 @@ function Find-NonexistentShares { $temp = $temp | Select-String -Pattern '\\\\[\w\.\-]+\\[\w\-_\\.]+' | ForEach-Object { $_.Matches.Value } $temp | ForEach-Object { $ServerList = [ordered] @{ - Server = $_ -split '\\' | Where-Object {$_ -ne ""} | Select-Object -First 1 + Server = (($_ -split '\\') | Where-Object {$_ -ne ""})[0] Share = $_ Script = $Script.FullName } @@ -1405,85 +1413,57 @@ function Find-NonexistentShares { } } - $LogonScriptShares = $LogonScriptShares #| Sort-Object -Property Share -Unique - $AdminLogonScripts = Find-AdminLogonScripts -AdminUsers $AdminUsers - $Admins = 'No' - $Exploitable = 'No' + $LogonScriptShares = $LogonScriptShares | Sort-Object -Property Server -Unique $NonExistentShares = @() [Array] $NonExistentShares = foreach ($LogonScriptShare in $LogonScriptShares) { - try { + $ServerWithoutDNS = $null + try { $DNSEntry = [System.Net.DNS]::GetHostByName($LogonScriptShare.Server) } catch { $ServerWithoutDNS = $LogonScriptShare } - if ($ServerWithoutDNS) { - foreach ($AdminScript in $AdminLogonScripts) { - if ((Get-Item $ServerWithoutDNS.Script).Name -match $AdminScript.LogonScript){ - $Admins = $AdminScript.User - $Exploitable = 'Yes' - $Results = [ordered] @{ - Type = 'ExploitableLogonScript' - Server = $ServerWithoutDNS.Server - Share = $ServerWithoutDNS.Share - Script = $ServerWithoutDNS.Script - DNS = 'No' - Exploitable = $Exploitable - Admins = $Admins - } - } else { - $Admins = 'No' - $Exploitable = 'Potentially' - $Results = [ordered] @{ - Type = 'NonexistentShare' - Server = $ServerWithoutDNS.Server - Share = $ServerWithoutDNS.Share - Script = $ServerWithoutDNS.Script - DNS = 'No' - Exploitable = $Exploitable - Admins = $Admins - } - } - [pscustomobject] $Results + $Results = [ordered] @{ + Misconfiguration = 'LSM-Shares' + Description = "Non-existent shares" + Details = "$($ServerWithoutDNS.Share) mapped in $($ServerWithoutDNS.Script)" } + [pscustomobject] $Results } } - $NonExistentShares } -function Find-UnsafeLogonScriptPermissions { +function Find-AdminsNonexistentShares { [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [array]$LogonScripts, - [Parameter(Mandatory = $true)] - [array]$SafeUsersList, + param ( + [array]$NonExistentShares, + [array]$AdminUsers, [Management.Automation.PSCredential] [Management.Automation.CredentialAttribute()] $Credential = [Management.Automation.PSCredential]::Empty ) - - $UnsafeRights = 'FullControl|Modify|Write' - $SafeUsers = $SafeUsersList - foreach ($script in $LogonScripts){ - # Write-Verbose -Message "Checking $($script.FullName) for unsafe permissions.." - $ACL = (Get-Acl $script.FullName -ErrorAction SilentlyContinue).Access - foreach ($entry in $ACL) { - if ($entry.FileSystemRights -match $UnsafeRights ` - -and $entry.AccessControlType -eq "Allow" ` - -and $entry.IdentityReference -notmatch $SafeUsers - ){ + $AdminLogonScripts = Find-AdminLogonScripts -AdminUsers $AdminUsers -Credential $Credential + # $Share = ($NonExistentShares.Details | Select-String '\\\\[\w\.\-]+\\[\w\-_\\.]+').Matches.Value + # $ShareScript = (($NonExistentShares.Details | Select-String "\s.*$").Matches.Value).Replace('mapped in ','').TrimStart() | Sort-Object -Unique + $AdminsNonexistentShares = @() + [Array] $AdminsNonexistentShares = foreach ($Admin in $AdminLogonScripts) { + $AdminDN = (($Admin.Details | Select-String "(CN=.*)\s").Matches.Value).Trim(' - ') + $LogonScript = (($Admin.Details | Select-String "\s-\s.*$").Matches.Value).Trim(' - ') + foreach ($Share in $NonExistentShares) { + if ($Share.Details -match $LogonScript) { + $SharePath = ($Share.Details | Select-String '\\\\[\w\.\-]+\\[\w\-_\\.]+').Matches.Value $Results = [ordered] @{ - Type = 'UnsafeLogonScriptPermission' - File = $script.FullName - User = $entry.IdentityReference.Value - Rights = $entry.FileSystemRights + Misconfiguration = 'LSM-Admins-2' + Description = "Admins with logon scripts mapped from nonexistent share" + Details = "$AdminDN - $LogonScript mapping $SharePath" } - [pscustomobject] $Results | Sort-Object -Unique + [pscustomobject] $Results } } } + + $AdminsNonexistentShares } function Find-UnsafeUNCPermissions { [CmdletBinding()] @@ -1500,8 +1480,22 @@ function Find-UnsafeUNCPermissions { $UnsafeRights = 'FullControl|Modify|Write' $SafeUsers = $SafeUsersList foreach ($script in $UNCScripts){ + if ([string]::IsNullOrWhiteSpace([string]$script)) { + continue + } + if (-not (Test-Path -LiteralPath $script -ErrorAction SilentlyContinue)) { + Write-Verbose "Path does not exist or is not accessible: '$script'" + continue + } + # Write-Host $script # "Checking $script for unsafe permissions.." - $ACL = (Get-Acl $script -ErrorAction SilentlyContinue).Access + try { + $ACL = (Get-Acl -LiteralPath $script -ErrorAction Stop).Access + } + catch { + Write-Verbose "Unable to read permissions for '$script': $($_.Exception.Message)" + continue + } foreach ($entry in $ACL) { if ($entry.FileSystemRights -match $UnsafeRights ` -and $entry.AccessControlType -eq "Allow" ` @@ -1509,29 +1503,54 @@ function Find-UnsafeUNCPermissions { ){ if ($script -match 'NETLOGON|SYSVOL') { $Type = 'UnsafeNetlogonSysvol' - $Results = [ordered] @{ + <#$Results = [ordered] @{ Type = $Type Folder = $script User = $entry.IdentityReference.Value Rights = $entry.FileSystemRights + }#> + $Folder = $script + $User = $entry.IdentityReference.Value + $Rights = $entry.FileSystemRights + $Results = [ordered] @{ + Misconfiguration = 'LSM-Access-3' + Description = "Unsafe NETLOGON/SYSVOL permissions" + Details = "$User with $Rights on $Folder" } [pscustomobject] $Results | Sort-Object -Unique - } elseif ($script -match '\.') { + } + elseif ($script -match '\.') { $Type = 'UnsafeUNCFilePermission' - $Results = [ordered] @{ + <#$Results = [ordered] @{ Type = $Type File = $script User = $entry.IdentityReference.Value Rights = $entry.FileSystemRights + }#> + $File = $script + $User = $entry.IdentityReference.Value + $Rights = $entry.FileSystemRights + $Results = [ordered] @{ + Misconfiguration = 'LSM-Access-2' + Description = "Unsafe UNC file permissions" + Details = "$User with $Rights on $File" } [pscustomobject] $Results | Sort-Object -Unique } else { $Type = 'UnsafeUNCFolderPermission' - $Results = [ordered] @{ + <#$Results = [ordered] @{ Type = $Type Folder = $script User = $entry.IdentityReference.Value Rights = $entry.FileSystemRights + }#> + $Folder = $script + $User = $entry.IdentityReference.Value + $Rights = $entry.FileSystemRights + $Results = [ordered] @{ + Misconfiguration = 'LSM-Access-1' + Description = "Unsafe UNC folder permissions" + Details = "$User with $Rights on $Folder" } [pscustomobject] $Results | Sort-Object -Unique } @@ -1561,11 +1580,18 @@ function Find-UnsafeLogonScriptPermissions { -and $entry.AccessControlType -eq "Allow" ` -and $entry.IdentityReference -notmatch $SafeUsers ){ - $Results = [ordered] @{ + <#$Results = [ordered] @{ Type = 'UnsafeLogonScriptPermission' File = $script.FullName User = $entry.IdentityReference.Value Rights = $entry.FileSystemRights + }#> + $User = $entry.IdentityReference.Value + $Rights = $entry.FileSystemRights + $Results = [ordered] @{ + Misconfiguration = 'LSM-Access-4' + Description = "Unsafe logon script permissions" + Details = "$User with $Rights on $($script.FullName)" } [pscustomobject] $Results | Sort-Object -Unique } @@ -1594,18 +1620,24 @@ function Find-UnsafeGPOLogonScriptPermissions { -and $entry.AccessControlType -eq "Allow" ` -and $entry.IdentityReference -notmatch $SafeUsers ){ - $Results = [ordered] @{ + <#$Results = [ordered] @{ Type = 'UnsafeGPOLogonScriptPermission' File = $script.FullName User = $entry.IdentityReference.Value Rights = $entry.FileSystemRights + }#> + $User = $entry.IdentityReference.Value + $Rights = $entry.FileSystemRights + $Results = [ordered] @{ + Misconfiguration = 'LSM-Access-5' + Description = "Unsafe GPO logon script permissions" + Details = "$User with $Rights on $($script.FullName)" } [pscustomobject] $Results | Sort-Object -Unique } } } } - function Show-Results { [CmdletBinding()] param( @@ -1614,220 +1646,214 @@ function Show-Results { ) $IssueTable = @{ - Credentials = 'Plaintext credentials' - NonexistentShare = 'Nonexistent Shares' - ExploitableLogonScript = 'Admins with logonscripts mapped from nonexistent share' - AdminLogonScript = 'Admins with logonscripts' - UnsafeNetlogonSysvol = 'Unsafe NETLOGON/SYSVOL permissions' - UnsafeUNCFilePermission = 'Unsafe UNC file permissions' - UnsafeUNCFolderPermission = 'Unsafe UNC folder permissions' - UnsafeLogonScriptPermission = 'Unsafe logon script permissions' - UnsafeGPOLogonScriptPermission = 'Unsafe GPO logon script permissions' + 'LSM-Creds' = 'Plaintext credentials' + 'LSM-Shares' = 'Nonexistent Shares' + 'LSM-Access-1' = 'Unsafe UNC folder permissions' + 'LSM-Access-2' = 'Unsafe UNC file permissions' + 'LSM-Access-3' = 'Unsafe NETLOGON/SYSVOL permissions' + 'LSM-Access-4' = 'Unsafe logon script permissions' + 'LSM-Access-5' = 'Unsafe GPO logon script permissions' + 'LSM-Admins-1' = 'Admins with logonscripts' + 'LSM-Admins-2' = 'Admins with logonscripts mapped from nonexistent share' } if ($null -ne $Results) { - $UniqueResults = $Results.Type | Sort-Object -Unique + $UniqueResults = $Results.Misconfiguration | Sort-Object -Unique Write-Host "########## $($IssueTable[$UniqueResults]) ##########" # $Results | Format-List $Results | Format-Table -Wrap } } -Get-Art -Version '0.7-dev' +Get-Art -Version '0.10-2026' $SafeUsers = 'NT AUTHORITY\\SYSTEM|Administrator|NT SERVICE\\TrustedInstaller|Domain Admins|Server Operators|Enterprise Admins|CREATOR OWNER' $AdminGroups = @("Account Operators", "Administrators", "Backup Operators", "Cryptographic Operators", "Distributed COM Users", "Domain Admins", "Domain Controllers", "Enterprise Admins", "Print Operators", "Schema Admins", "Server Operators") -if ($Credential.UserName) { - Write-Host "[i] Credentials detected. Running ScriptSentry as: $($Credential.UserName)" -} else { - # nothing, no creds supplied -} +$TargetArguments = @{} +$CredentialArguments = @{} +$LocationArguments = @{} if ($Credential.UserName) { - if ($socks) { - $AdminUsers = $AdminGroups | ForEach-Object { (Get-DomainGroupMember -Identity $_ -Recurse -Credential $Credential -Server $Server -Domain $Domain | Where-Object {$_.MemberObjectClass -eq 'user'})} | Sort-Object -Property MemberName -Unique - } else { - $AdminUsers = $AdminGroups | ForEach-Object { (Get-DomainGroupMember -Identity $_ -Recurse -Credential $Credential | Where-Object {$_.MemberObjectClass -eq 'user'})} | Sort-Object -Property MemberName -Unique - } - $AdminUsers | ForEach-Object { $SafeUsers = $SafeUsers + '|' + $_.MemberName } - - # Get a list of all logon scripts - if ($socks) { - $LogonScripts = Get-LogonScripts -Credential $Credential -Domain $Domain -Server $Server - } else { - $LogonScripts = Get-LogonScripts -Credential $Credential - } - - # Get a list of all GPO logon scripts - if ($socks) { - $GPOLogonScripts = Get-GPOLogonScripts -Credential $Credential -Domain $Domain -Server $Server - } else { - $GPOLogonScripts = Get-GPOLogonScripts -Credential $Credential -Domain $Domain -Server $Server - } - - if ($LogonScripts) { - # Find logon scripts (.bat, .vbs, .cmd, .ps1, .kix) that contain unc paths (e.g. \\srv01\fileshare1) - $UNCScripts = Find-UNCScripts -LogonScripts $LogonScripts -Credential $Credential - - # Find mapped drives (e.g. \\srv01\fileshare1, \\srv02\fileshare2\accounting) - $MappedDrives = Find-MappedDrives -LogonScripts $LogonScripts -Credential $Credential - - # Find nonexistent shares - $NonExistentSharesScripts = Find-NonexistentShares -LogonScripts $LogonScripts -AdminUsers $AdminUsers -Credential $Credential - $NonExistentShares = $NonExistentSharesScripts | Where-Object {$_.Exploitable -eq 'Potentially'} | Sort-Object -Property Share -Unique - - # Find unsafe permissions on logon scripts - $UnsafeLogonScripts = Find-UnsafeLogonScriptPermissions -LogonScripts $LogonScripts -SafeUsersList $SafeUsers -Credential $Credential - - # Find credentials in logon scripts - $Credentials = Find-LogonScriptCredentials -LogonScripts $LogonScripts -Credential $Credential - } else { - Write-Host "[i] No logon scripts found!`n" -ForegroundColor Cyan - } - - if ($UNCScripts) { - # Find unsafe permissions for unc files found in logon scripts - $UnsafeUNCPermissions = Find-UnsafeUNCPermissions -UNCScripts $UNCScripts -SafeUsersList $SafeUsers -Credential $Credential - } else { - Write-Host "[i] No UNC files found!`n" -ForegroundColor Cyan - } - - if ($MappedDrives) { - # Find unsafe permissions for unc paths found in logon scripts - $UnsafeMappedDrives = Find-UnsafeUNCPermissions -UNCScripts $MappedDrives -SafeUsersList $SafeUsers -Credential $Credential - } else { - Write-Host "[i] No mapped drives found!`n" -ForegroundColor Cyan - } + Write-Host "[i] Credentials detected. Running ScriptSentry as: $($Credential.UserName)" + $TargetArguments.Credential = $Credential + $CredentialArguments.Credential = $Credential +} - # Find unsafe NETLOGON & SYSVOL share permissions - $NetlogonSysvol = Get-NetlogonSysvol - $UnsafeNetlogonSysvol = Find-UnsafeUNCPermissions -UNCScripts $NetlogonSysvol -SafeUsersList $SafeUsers -Credential $Credential +if ($PSBoundParameters.ContainsKey('Server')) { + $TargetArguments.Server = $Server + $TargetArguments.Domain = $Domain + $LocationArguments.Server = $Server + $LocationArguments.Domain = $Domain +} - if ($GPOLogonScripts) { - # Find unsafe permissions on GPO logon scripts - $UnsafeGPOLogonScripts = Find-UnsafeGPOLogonScriptPermissions -GPOLogonScripts $GPOLogonScripts -SafeUsersList $SafeUsers -Credential $Credential - } else { - Write-Host "[i] No GPO logon scripts found!`n" -ForegroundColor Cyan +$AdminUsers = $AdminGroups | ForEach-Object { + $GroupArguments = @{ Identity = $_; Recurse = $true } + foreach ($Name in $TargetArguments.Keys) { + $GroupArguments[$Name] = $TargetArguments[$Name] } - - # Find admins that have logon scripts assigned - $AdminLogonScripts = Find-AdminLogonScripts -AdminUsers $AdminUsers -Credential $Credential + Get-ScriptSentryDomainGroupMember @GroupArguments | Where-Object { $_.MemberObjectClass -eq 'user' } +} | Sort-Object -Property MemberName -Unique +$AdminUsers | ForEach-Object { $SafeUsers = $SafeUsers + '|' + $_.MemberName } + +$LogonScripts = Get-LogonScripts @TargetArguments +$GPOLogonScripts = Get-GPOLogonScripts @TargetArguments + +if ($LogonScripts) { + $UNCScripts = Find-UNCScripts -LogonScripts $LogonScripts @CredentialArguments + $MappedDrives = Find-MappedDrives -LogonScripts $LogonScripts @CredentialArguments + $NonExistentShares = Find-NonexistentShares -LogonScripts $LogonScripts @CredentialArguments + $AdminsNonExistentShares = Find-AdminsNonexistentShares -NonExistentShares $NonExistentShares -AdminUsers $AdminUsers @CredentialArguments + $UnsafeLogonScripts = Find-UnsafeLogonScriptPermissions -LogonScripts $LogonScripts -SafeUsersList $SafeUsers @CredentialArguments + $Credentials = Find-LogonScriptCredentials -LogonScripts $LogonScripts @CredentialArguments } else { - $AdminUsers = $AdminGroups | ForEach-Object { (Get-DomainGroupMember -Domain (Get-Domain).Name -Identity $_ -Recurse | Where-Object {$_.MemberObjectClass -eq 'user'})} | Sort-Object -Property MemberName -Unique - $AdminUsers | ForEach-Object { $SafeUsers = $SafeUsers + '|' + $_.MemberName } - - # Get a list of all logon scripts - $LogonScripts = Get-LogonScripts - - # Get a list of all GPO logon scripts - $GPOLogonScripts = Get-GPOLogonScripts - - if ($LogonScripts) { - # Find logon scripts (.bat, .vbs, .cmd, .ps1, .kix) that contain unc paths (e.g. \\srv01\fileshare1) - $UNCScripts = Find-UNCScripts -LogonScripts $LogonScripts - - # Find mapped drives (e.g. \\srv01\fileshare1, \\srv02\fileshare2\accounting) - $MappedDrives = Find-MappedDrives -LogonScripts $LogonScripts - - # Find nonexistent shares - $NonExistentSharesScripts = Find-NonexistentShares -LogonScripts $LogonScripts -AdminUsers $AdminUsers - $NonExistentShares = $NonExistentSharesScripts | Where-Object {$_.Exploitable -eq 'Potentially'} | Sort-Object -Property Share -Unique - - # Find unsafe permissions on logon scripts - $UnsafeLogonScripts = Find-UnsafeLogonScriptPermissions -LogonScripts $LogonScripts -SafeUsersList $SafeUsers + Write-Host "[i] No logon scripts found!`n" -ForegroundColor Cyan +} - # Find credentials in logon scripts - $Credentials = Find-LogonScriptCredentials -LogonScripts $LogonScripts - } else { - Write-Host "[i] No logon scripts found!`n" -ForegroundColor Cyan - } +if ($UNCScripts) { + $UnsafeUNCPermissions = Find-UnsafeUNCPermissions -UNCScripts $UNCScripts -SafeUsersList $SafeUsers @CredentialArguments +} else { + Write-Host "[i] No direct UNC file references found!`n" -ForegroundColor Cyan +} - if ($UNCScripts) { - # Find unsafe permissions for unc files found in logon scripts - $UnsafeUNCPermissions = Find-UnsafeUNCPermissions -UNCScripts $UNCScripts -SafeUsersList $SafeUsers - } else { - Write-Host "[i] No UNC files found!`n" -ForegroundColor Cyan - } - - if ($MappedDrives) { - # Find unsafe permissions for unc paths found in logon scripts - $UnsafeMappedDrives = Find-UnsafeUNCPermissions -UNCScripts $MappedDrives -SafeUsersList $SafeUsers - } else { - Write-Host "[i] No mapped drives found!`n" -ForegroundColor Cyan - } +if ($MappedDrives) { + $UnsafeMappedDrives = Find-UnsafeUNCPermissions -UNCScripts $MappedDrives -SafeUsersList $SafeUsers @CredentialArguments +} else { + Write-Host "[i] No mapped drives found!`n" -ForegroundColor Cyan +} - # Find unsafe NETLOGON & SYSVOL share permissions - $NetlogonSysvol = Get-NetlogonSysvol - $UnsafeNetlogonSysvol = Find-UnsafeUNCPermissions -UNCScripts $NetlogonSysvol -SafeUsersList $SafeUsers +$NetlogonSysvol = Get-NetlogonSysvol @LocationArguments +$UnsafeNetlogonSysvol = Find-UnsafeUNCPermissions -UNCScripts $NetlogonSysvol -SafeUsersList $SafeUsers @CredentialArguments - if ($GPOLogonScripts) { - # Find unsafe permissions on GPO logon scripts - $UnsafeGPOLogonScripts = Find-UnsafeGPOLogonScriptPermissions -GPOLogonScripts $GPOLogonScripts -SafeUsersList $SafeUsers - } else { - Write-Host "[i] No GPO logon scripts found!`n" -ForegroundColor Cyan - } - - # Find admins that have logon scripts assigned - $AdminLogonScripts = Find-AdminLogonScripts -AdminUsers $AdminUsers +if ($GPOLogonScripts) { + $UnsafeGPOLogonScripts = Find-UnsafeGPOLogonScriptPermissions -GPOLogonScripts $GPOLogonScripts -SafeUsersList $SafeUsers @CredentialArguments +} else { + Write-Host "[i] No GPO logon scripts found!`n" -ForegroundColor Cyan } +$AdminLogonScripts = Find-AdminLogonScripts -AdminUsers $AdminUsers @CredentialArguments + if ($NonExistentShares) { # Find Exploitable logon scripts - $ExploitableLogonScripts = $NonExistentSharesScripts | Where-Object {$_.Exploitable -eq 'Yes'} + # $ExploitableLogonScripts = $NonExistentSharesScripts | Where-Object {$_.Exploitable -eq 'Yes'} } else { Write-Host "[i] No non-existent shares found!`n" -ForegroundColor Cyan } # Show all results +if ($Credentials) {Show-Results $Credentials} if ($UnsafeMappedDrives) {Show-Results $UnsafeMappedDrives} -if ($UnsafeLogonScripts) {Show-Results $UnsafeLogonScripts} -if ($UnsafeGPOLogonScripts) {Show-Results $UnsafeGPOLogonScripts} if ($UnsafeUNCPermissions) {Show-Results $UnsafeUNCPermissions} if ($UnsafeNetlogonSysvol) {Show-Results $UnsafeNetlogonSysvol} -if ($Credentials) {Show-Results $Credentials} +if ($UnsafeLogonScripts) {Show-Results $UnsafeLogonScripts} +if ($UnsafeGPOLogonScripts) {Show-Results $UnsafeGPOLogonScripts} if ($NonExistentShares) {Show-Results $NonExistentShares} if ($AdminLogonScripts) {Show-Results $AdminLogonScripts} -if ($ExploitableLogonScripts) {Show-Results $ExploitableLogonScripts} +if ($AdminsNonExistentShares) {Show-Results $AdminsNonExistentShares} +# if ($ExploitableLogonScripts) {Show-Results $ExploitableLogonScripts} if ($SaveOutput) { - if ($UnsafeMappedDrives) { - Write-Host "[i] Saving UnsafeMappedDrives.csv to the current directory" -ForegroundColor Cyan - $UnsafeMappedDrives | Export-CSV -NoTypeInformation UnsafeMappedDrives.csv - } - if ($UnsafeLogonScripts) { - Write-Host "[i] Saving UnsafeLogonScripts.csv to the current directory" -ForegroundColor Cyan - $UnsafeLogonScripts | Export-CSV -NoTypeInformation UnsafeLogonScripts.csv - } - if ($UnsafeGPOLogonScripts) { - Write-Host "[i] Saving UnsafeGPOLogonScripts.csv to the current directory" -ForegroundColor Cyan - $UnsafeGPOLogonScripts | Export-Csv -NoTypeInformation UnsafeGPOLogonScripts.csv - } - if ($UnsafeUNCPermissions) { - Write-Host "[i] Saving UnsafeUNCPermissions.csv to the current directory" -ForegroundColor Cyan - $UnsafeUNCPermissions | Export-CSV -NoTypeInformation UnsafeUNCPermissions.csv - } - if ($UnsafeNetlogonSysvol) { - Write-Host "[i] Saving UnsafeNetlogonSysvol.csv to the current directory" -ForegroundColor Cyan - $UnsafeNetlogonSysvol | Export-Csv -NoTypeInformation UnsafeNetlogonSysvol.csv - } - if ($AdminLogonScripts) { - Write-Host "[i] Saving AdminLogonScripts.csv to the current directory" -ForegroundColor Cyan - $AdminLogonScripts | Export-CSV -NoTypeInformation AdminLogonScripts.csv - } - if ($Credentials) { - Write-Host "[i] Saving Credentials.csv to the current directory" -ForegroundColor Cyan - $Credentials | Export-CSV -NoTypeInformation Credentials.csv - } - if ($NonExistentShares) { - Write-Host "[i] Saving NonExistentShares.csv to the current directory" -ForegroundColor Cyan - $NonExistentShares | Export-CSV -NoTypeInformation NonExistentShares.csv - } - if ($ExploitableLogonScripts) { - Write-Host "[i] Saving ExploitableLogonScripts.csv to the current directory" -ForegroundColor Cyan - $ExploitableLogonScripts | Export-CSV -NoTypeInformation ExploitableLogonScripts.csv - } + if ($OutputDirectory){ + if ($UnsafeMappedDrives) { + # Write-Host "[i] Saving UnsafeMappedDrives.csv to the current directory" -ForegroundColor Cyan + # $UnsafeMappedDrives | Export-CSV -NoTypeInformation UnsafeMappedDrives.csv + $UnsafeMappedDrives | Export-CSV -NoTypeInformation $OutputDirectory\ScriptSentryResults.csv -Append + } + if ($UnsafeLogonScripts) { + # Write-Host "[i] Saving UnsafeLogonScripts.csv to the current directory" -ForegroundColor Cyan + # $UnsafeLogonScripts | Export-CSV -NoTypeInformation UnsafeLogonScripts.csv + $UnsafeLogonScripts | Export-CSV -NoTypeInformation $OutputDirectory\ScriptSentryResults.csv -Append + } + if ($UnsafeGPOLogonScripts) { + # Write-Host "[i] Saving UnsafeGPOLogonScripts.csv to the current directory" -ForegroundColor Cyan + # $UnsafeGPOLogonScripts | Export-Csv -NoTypeInformation UnsafeGPOLogonScripts.csv + $UnsafeGPOLogonScripts | Export-Csv -NoTypeInformation $OutputDirectory\ScriptSentryResults.csv -Append + } + if ($UnsafeUNCPermissions) { + # Write-Host "[i] Saving UnsafeUNCPermissions.csv to the current directory" -ForegroundColor Cyan + # $UnsafeUNCPermissions | Export-CSV -NoTypeInformation UnsafeUNCPermissions.csv + $UnsafeUNCPermissions | Export-CSV -NoTypeInformation $OutputDirectory\ScriptSentryResults.csv -Append + } + if ($UnsafeNetlogonSysvol) { + # Write-Host "[i] Saving UnsafeNetlogonSysvol.csv to the current directory" -ForegroundColor Cyan + # $UnsafeNetlogonSysvol | Export-Csv -NoTypeInformation UnsafeNetlogonSysvol.csv + $UnsafeNetlogonSysvol | Export-Csv -NoTypeInformation $OutputDirectory\ScriptSentryResults.csv -Append + } + if ($AdminLogonScripts) { + # Write-Host "[i] Saving AdminLogonScripts.csv to the current directory" -ForegroundColor Cyan + # $AdminLogonScripts | Export-CSV -NoTypeInformation AdminLogonScripts.csv + $AdminLogonScripts | Export-CSV -NoTypeInformation $OutputDirectory\ScriptSentryResults.csv -Append + } + if ($Credentials) { + # Write-Host "[i] Saving Credentials.csv to the current directory" -ForegroundColor Cyan + # $Credentials | Export-CSV -NoTypeInformation Credentials.csv + $Credentials | Export-CSV -NoTypeInformation $OutputDirectory\ScriptSentryResults.csv -Append + } + if ($NonExistentShares) { + # Write-Host "[i] Saving NonExistentShares.csv to the current directory" -ForegroundColor Cyan + # $NonExistentShares | Export-CSV -NoTypeInformation NonExistentShares.csv + $NonExistentShares | Export-CSV -NoTypeInformation $OutputDirectory\ScriptSentryResults.csv -Append + } + if ($ExploitableLogonScripts) { + # Write-Host "[i] Saving ExploitableLogonScripts.csv to the current directory" -ForegroundColor Cyan + # $ExploitableLogonScripts | Export-CSV -NoTypeInformation ExploitableLogonScripts.csv + $ExploitableLogonScripts | Export-CSV -NoTypeInformation $OutputDirectory\ScriptSentryResults.csv -Append + } + if ($AdminsNonExistentShares) { + $AdminsNonExistentShares | Export-CSV -NoTypeInformation $OutputDirectory\ScriptSentryResults.csv -Append + } + } else { + if ($UnsafeMappedDrives) { + # Write-Host "[i] Saving UnsafeMappedDrives.csv to the current directory" -ForegroundColor Cyan + # $UnsafeMappedDrives | Export-CSV -NoTypeInformation UnsafeMappedDrives.csv + $UnsafeMappedDrives | Export-CSV -NoTypeInformation ScriptSentryResults.csv -Append + } + if ($UnsafeLogonScripts) { + # Write-Host "[i] Saving UnsafeLogonScripts.csv to the current directory" -ForegroundColor Cyan + # $UnsafeLogonScripts | Export-CSV -NoTypeInformation UnsafeLogonScripts.csv + $UnsafeLogonScripts | Export-CSV -NoTypeInformation ScriptSentryResults.csv -Append + } + if ($UnsafeGPOLogonScripts) { + # Write-Host "[i] Saving UnsafeGPOLogonScripts.csv to the current directory" -ForegroundColor Cyan + # $UnsafeGPOLogonScripts | Export-Csv -NoTypeInformation UnsafeGPOLogonScripts.csv + $UnsafeGPOLogonScripts | Export-Csv -NoTypeInformation ScriptSentryResults.csv -Append + } + if ($UnsafeUNCPermissions) { + # Write-Host "[i] Saving UnsafeUNCPermissions.csv to the current directory" -ForegroundColor Cyan + # $UnsafeUNCPermissions | Export-CSV -NoTypeInformation UnsafeUNCPermissions.csv + $UnsafeUNCPermissions | Export-CSV -NoTypeInformation ScriptSentryResults.csv -Append + } + if ($UnsafeNetlogonSysvol) { + # Write-Host "[i] Saving UnsafeNetlogonSysvol.csv to the current directory" -ForegroundColor Cyan + # $UnsafeNetlogonSysvol | Export-Csv -NoTypeInformation UnsafeNetlogonSysvol.csv + $UnsafeNetlogonSysvol | Export-Csv -NoTypeInformation ScriptSentryResults.csv -Append + } + if ($AdminLogonScripts) { + # Write-Host "[i] Saving AdminLogonScripts.csv to the current directory" -ForegroundColor Cyan + # $AdminLogonScripts | Export-CSV -NoTypeInformation AdminLogonScripts.csv + $AdminLogonScripts | Export-CSV -NoTypeInformation ScriptSentryResults.csv -Append + } + if ($Credentials) { + # Write-Host "[i] Saving Credentials.csv to the current directory" -ForegroundColor Cyan + # $Credentials | Export-CSV -NoTypeInformation Credentials.csv + $Credentials | Export-CSV -NoTypeInformation ScriptSentryResults.csv -Append + } + if ($NonExistentShares) { + # Write-Host "[i] Saving NonExistentShares.csv to the current directory" -ForegroundColor Cyan + # $NonExistentShares | Export-CSV -NoTypeInformation NonExistentShares.csv + $NonExistentShares | Export-CSV -NoTypeInformation ScriptSentryResults.csv -Append + } + if ($ExploitableLogonScripts) { + # Write-Host "[i] Saving ExploitableLogonScripts.csv to the current directory" -ForegroundColor Cyan + # $ExploitableLogonScripts | Export-CSV -NoTypeInformation ExploitableLogonScripts.csv + $ExploitableLogonScripts | Export-CSV -NoTypeInformation ScriptSentryResults.csv -Append + } + + if ($AdminsNonExistentShares) { + $AdminsNonExistentShares | Export-CSV -NoTypeInformation ScriptSentryResults.csv -Append + } + } Get-ChildItem -Filter "*.csv" -File } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68e8cf9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,46 @@ +"Commons Clause" License Condition v1.0 + +The Software is provided to you by the Licensor under the License, as defined +below, subject to the following condition. + +Without limiting other conditions in the License, the grant of rights under +the License will not include, and the License does not grant to you, the right +to Sell the Software. + +For purposes of the foregoing, "Sell" means practicing any or all of the +rights granted to you under the License to provide to third parties, for a fee +or other consideration (including without limitation fees for hosting or +consulting/ support services related to the Software), a product or service +whose value derives, entirely or substantially, from the functionality of the +Software. Any license notice or attribution required by the License must also +include this Commons Clause License Condition notice. + +Software: ScriptSentry + +License: MIT + +Licensor: Spencer Alessi, Ethical Threat + +--- + +MIT License + +Copyright (c) 2025 - 2026 Spencer Alessi, Ethical Threat + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index ad6ead3..ba02b19 100644 --- a/README.md +++ b/README.md @@ -16,99 +16,79 @@ Invoke-ScriptSentry IEX(Invoke-WebRequest 'https://raw.githubusercontent.com/techspence/ScriptSentry/main/Invoke-ScriptSentry.ps1') Invoke-ScriptSentry | Out-File c:\temp\ScriptSentry.txt -# Run ScriptSentry and save results to separate csv files in the current directory +# Run ScriptSentry and save results to ScriptSentryResults.csv in the current directory IEX(Invoke-WebRequest 'https://raw.githubusercontent.com/techspence/ScriptSentry/main/Invoke-ScriptSentry.ps1') Invoke-ScriptSentry -SaveOutput $true -``` - -### Example Output -``` - _______ _______ _______ _________ _______ _________ _______ _______ _ _________ _______ -( ____ \( ____ \( ____ )\__ __/( ____ )\__ __/( ____ \( ____ \( ( /|\__ __/( ____ )|\ /| -| ( \/| ( \/| ( )| ) ( | ( )| ) ( | ( \/| ( \/| \ ( | ) ( | ( )|( \ / ) -| (_____ | | | (____)| | | | (____)| | | | (_____ | (__ | \ | | | | | (____)| \ (_) / -(_____ )| | | __) | | | _____) | | (_____ )| __) | (\ \) | | | | __) \ / - ) || | | (\ ( | | | ( | | ) || ( | | \ | | | | (\ ( ) ( -/\____) || (____/\| ) \ \_____) (___| ) | | /\____) || (____/\| ) \ | | | | ) \ \__ | | -\_______)(_______/|/ \__/\_______/|/ )_( \_______)(_______/|/ )_) )_( |/ \__/ \_/ - by: Spencer Alessi @techspence - v0.6 - __,_______ - / __.==---/ * * * * * * - / (-' - `-' - Setting phasers to stun, please wait.. -########## Unsafe UNC folder permissions ########## +# Save results to a specific directory +Invoke-ScriptSentry -SaveOutput $true -OutputDirectory C:\Temp\ScriptSentry -Type File User Rights ----- ---- ---- ------ -UnsafeUNCFolderPermission \\eureka-dc01\fileshare1 Everyone FullControl -UnsafeUNCFolderPermission \\eureka-dc01\fileshare1\accounting Everyone FullControl -UnsafeUNCFolderPermission \\eureka-dc01\fileshare1\IT Everyone FullControl +# Use alternate credentials for LDAP queries +$Credential = Get-Credential +Invoke-ScriptSentry -Credential $Credential +# Query a specific domain controller and domain +Invoke-ScriptSentry -Server DC01.contoso.com -Domain contoso.com -########## Unsafe logon script permissions ########## +# Query a specific domain controller with alternate credentials +Invoke-ScriptSentry -Server DC01.contoso.com -Domain contoso.com -Credential $Credential +``` -Type File User Rights ----- ---- ---- ------ -UnsafeLogonScriptPermission \\eureka.local\sysvol\eureka.local\scripts\elevate.vbs NT AUTHORITY\Authenticated Users ReadAndExecute, Synchronize -UnsafeLogonScriptPermission \\eureka.local\sysvol\eureka.local\scripts\run.vbs NT AUTHORITY\Authenticated Users ReadAndExecute, Synchronize -UnsafeLogonScriptPermission \\eureka.local\sysvol\eureka.local\scripts\test.cmd EUREKA\Domain Users Modify, Synchronize +`-Server` and `-Domain` must be used together. `-Credential` applies to LDAP queries; SYSVOL, NETLOGON, and other UNC paths are accessed as the user running PowerShell. +### Example Output +``` +########## Plaintext credentials ########## -########## Unsafe GPO logon script permissions ########## +Misconfiguration Description Details +---------------- ----------- ------- +LSM-Creds Plaintext credentials within a logon script \\contoso.com\sysvol\contoso.com\scripts\logon.bat - net use Z: \\FS01\Tools /user:CONTOSO\svc.deploy [REDACTED] -Type File User Rights ----- ---- ---- ------ -UnsafeGPOLogonScriptPermission \\eureka-dc01\fileshare1\run.bat EUREKA\testuser Write, ReadAndExecute, Synchronize -UnsafeGPOLogonScriptPermission \\eureka-dc01\fileshare1\run.bat Everyone FullControl +########## Unsafe UNC folder permissions ########## +Misconfiguration Description Details +---------------- ----------- ------- +LSM-Access-1 Unsafe UNC folder permissions CONTOSO\Domain Users with Modify on \\FS01\Tools ########## Unsafe UNC file permissions ########## -Type File User Rights ----- ---- ---- ------ -UnsafeUNCFilePermission \\eureka-dc01\fileshare1\IT\securit360pentest.bat Everyone FullControl - +Misconfiguration Description Details +---------------- ----------- ------- +LSM-Access-2 Unsafe UNC file permissions CONTOSO\Domain Users with Modify on \\FS01\Tools\startup.ps1 ########## Unsafe NETLOGON/SYSVOL permissions ########## -Type Folder User Rights ----- ------ ---- ------ -UnsafeNetlogonSysvol \\eureka.local\NETLOGON EUREKA\Domain Users Modify, Synchronize -UnsafeNetlogonSysvol \\eureka.local\SYSVOL NT AUTHORITY\Authenticated Users Modify, Synchronize +Misconfiguration Description Details +---------------- ----------- ------- +LSM-Access-3 Unsafe NETLOGON/SYSVOL permissions CONTOSO\Domain Users with Modify on \\contoso.com\NETLOGON -########## Plaintext credentials ########## +########## Unsafe logon script permissions ########## -Type File Credential ----- ---- ---------- -Credentials \\eureka.local\sysvol\eureka.local\scripts\ADCheck.ps1 $password = ConvertTo-SecureString -String "Password2468!" -AsPlainText -Force -Credentials \\eureka.local\sysvol\eureka.local\scripts\shares.cmd net use f: \\eureka-dc01\fileshare1\it /user:itadmin Password2468! -Credentials \\eureka.local\sysvol\eureka.local\scripts\test.cmd net use g: \\eureka-dc01\fileshare1 /user:user1 Password3355! -Credentials \\eureka.local\sysvol\eureka.local\scripts\test.cmd net use h: \\eureka-dc01\fileshare1\accounting /user:userfoo Password5! -Credentials \\eureka.local\sysvol\eureka.local\scripts\logon.kix Use X: "\\eureka-dc01\fileshare2" /USER:itadmin /P:Password2468! +Misconfiguration Description Details +---------------- ----------- ------- +LSM-Access-4 Unsafe logon script permissions CONTOSO\Domain Users with Modify on \\contoso.com\sysvol\contoso.com\scripts\logon.bat -########## Nonexistent Shares ########## +########## Unsafe GPO logon script permissions ########## -Type Server Share Script DNS Exploitable Admins ----- ------ ----- ------ --- ----------- ------ -NonexistentShare CUHOLDING \\CUHOLDING\QUICKBOOKS \\eureka.local\sysvol\eureka.local\scripts\marketing.bat No Potentially No -NonexistentShare eureka-srvnotexist \\eureka-srvnotexist\NonExistingShare \\eureka.local\sysvol\eureka.local\scripts\test.cmd No Potentially No -NonexistentShare NAS \\NAS\PUBLIC \\eureka.local\sysvol\eureka.local\scripts\main.bat No Potentially No -NonexistentShare NAS \\NAS\SYMITAR \\eureka.local\sysvol\eureka.local\scripts\symregOLD.bat No Potentially No +Misconfiguration Description Details +---------------- ----------- ------- +LSM-Access-5 Unsafe GPO logon script permissions CONTOSO\Domain Users with Modify on \\contoso.com\NETLOGON\logon.bat ########## Admins with logonscripts ########## -Type User LogonScript ----- ---- ----------- -AdminLogonScript LDAP://CN=Administrator,CN=Users,DC=eureka,DC=local run.vbs -AdminLogonScript LDAP://CN=it admin,OU=Admins,OU=Eureka,DC=eureka,DC=local elevate.vbs +Misconfiguration Description Details +---------------- ----------- ------- +LSM-Admins-1 Admins with logonscripts CN=Administrator,CN=Users,DC=contoso,DC=com - logon.bat + +########## Nonexistent Shares ########## + +Misconfiguration Description Details +---------------- ----------- ------- +LSM-Shares Non-existent shares \\OLD-FS01\Legacy mapped in \\contoso.com\sysvol\contoso.com\scripts\logon.bat ########## Admins with logonscripts mapped from nonexistent share ########## -Type Server Share Script DNS Exploitable Admins ----- ------ ----- ------ --- ----------- ------ -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 -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 -``` \ No newline at end of file +Misconfiguration Description Details +---------------- ----------- ------- +LSM-Admins-2 Admins with logonscripts mapped from nonexistent share CN=Administrator,CN=Users,DC=contoso,DC=com - logon.bat mapping \\OLD-FS01\Legacy +```