Add a check for enumerating whitelisted MSI files with interesting custom actions (issue #76)

This commit is contained in:
itm4n
2025-12-25 17:23:24 +01:00
parent 51bdb58ba3
commit e25e453f4c
4 changed files with 187 additions and 21 deletions
+1
View File
@@ -66,6 +66,7 @@
"CONFIG_SMB", "Invoke-SmbConfigurationCheck", "TA0008 - Lateral Movement", "Configuration - SMB", "Low", "List", "Audit", "True", "False", "Check whether SMBv1 is enabled (server), and whether signing is required (server and client)."
"CONFIG_PRINTNIGHTMARE", "Invoke-PointAndPrintConfigurationCheck", "TA0004 - Privilege Escalation", "Configuration - Point and Print", "High", "List", "Base", "True", "False", "Check whether the Print Spooler service is enabled and if the Point and Print configuration allows non-administrator users to install printer drivers."
"CONFIG_MSI_REPAIR_UAC_PROMPT", "Invoke-MsiAutomaticRepairUacPromptCheck", "TA0004 - Privilege Escalation", "Configuration - Application Repair UAC Prompt", "Medium", "List", "Extended", "True", "False", "Check whether the UAC prompt displayed when attempting an application repair through the Windows Installer was disabled."
"CONFIG_MSI_REPAIR_WHITELIST", "Invoke-MsiAutomaticRepairWhitelistCheck", "TA0004 - Privilege Escalation", "Configuration - Application Repair Whitelist", "Low", "List", "Base", "True", "False", "Check whether a whitelist of MSI packages is set in the registry to disable UAC prompts, and whether they have custom actions that may be leveraged for local privilege escalation."
"CONFIG_COINSTALLERS", "Invoke-DriverCoInstallerCheck", "TA0004 - Privilege Escalation", "Configuration - Driver Co-Installers", "Low", "List", "Audit", "True", "False", "Check whether Driver Co-installers are disabled. A local user might be able to gain SYSTEM privileges by plugging in a device such as a mouse or keyboard with a vulnerable Driver Co-installer."
"CONFIG_SCCM_CACHE_FOLDERS", "Invoke-SccmCacheFolderCheck", "TA0043 - Reconnaissance", "Configuration - SCCM Cache Folders", "None", "List", "Extended", "True", "False", "Get information about SCCM cache folders (incl. number and list of binary, script, and text files)."
"CONFIG_COM_REGISTRY_PERMISSIONS", "Invoke-ComServerRegistryPermissionCheck", "TA0004 - Privilege Escalation", "Configuration - COM registry permissions", "Medium", "List", "Extended", "False", "False", "Check whether the current user has any modification rights on a COM server in the registry. This may not necessarily result in a privilege escalation. Further analysis is required."
1 Id Command Category DisplayName Severity Format Type RunIfAdmin Risky Description
66 CONFIG_SMB Invoke-SmbConfigurationCheck TA0008 - Lateral Movement Configuration - SMB Low List Audit True False Check whether SMBv1 is enabled (server), and whether signing is required (server and client).
67 CONFIG_PRINTNIGHTMARE Invoke-PointAndPrintConfigurationCheck TA0004 - Privilege Escalation Configuration - Point and Print High List Base True False Check whether the Print Spooler service is enabled and if the Point and Print configuration allows non-administrator users to install printer drivers.
68 CONFIG_MSI_REPAIR_UAC_PROMPT Invoke-MsiAutomaticRepairUacPromptCheck TA0004 - Privilege Escalation Configuration - Application Repair UAC Prompt Medium List Extended True False Check whether the UAC prompt displayed when attempting an application repair through the Windows Installer was disabled.
69 CONFIG_MSI_REPAIR_WHITELIST Invoke-MsiAutomaticRepairWhitelistCheck TA0004 - Privilege Escalation Configuration - Application Repair Whitelist Low List Base True False Check whether a whitelist of MSI packages is set in the registry to disable UAC prompts, and whether they have custom actions that may be leveraged for local privilege escalation.
70 CONFIG_COINSTALLERS Invoke-DriverCoInstallerCheck TA0004 - Privilege Escalation Configuration - Driver Co-Installers Low List Audit True False Check whether Driver Co-installers are disabled. A local user might be able to gain SYSTEM privileges by plugging in a device such as a mouse or keyboard with a vulnerable Driver Co-installer.
71 CONFIG_SCCM_CACHE_FOLDERS Invoke-SccmCacheFolderCheck TA0043 - Reconnaissance Configuration - SCCM Cache Folders None List Extended True False Get information about SCCM cache folders (incl. number and list of binary, script, and text files).
72 CONFIG_COM_REGISTRY_PERMISSIONS Invoke-ComServerRegistryPermissionCheck TA0004 - Privilege Escalation Configuration - COM registry permissions Medium List Extended False False Check whether the current user has any modification rights on a COM server in the registry. This may not necessarily result in a privilege escalation. Further analysis is required.
+1
View File
@@ -5,6 +5,7 @@
### Added
- Attempt to determine private key file path when enumerating machine and user personal certificates (issue #77).
- Check for whitelisted MSI packages with potentially interesting custom actions (issue #76).
## 2025-12-24
+127 -5
View File
@@ -1194,7 +1194,8 @@ function Invoke-MsiAutomaticRepairUacPromptCheck {
begin {
$RegKey = "HKLM\SOFTWARE\Policies\Microsoft\Windows\Installer"
$RegValue = "DisableLUAInRepair"
$RegValueDisableLUAInRepair = "DisableLUAInRepair"
$RegValueSecureRepairPolicy = "SecureRepairPolicy"
$DisableLUAInRepairDescriptions = @(
"The User Account Control (UAC) prompts for credentials before initiating an application repair.",
@@ -1203,13 +1204,30 @@ function Invoke-MsiAutomaticRepairUacPromptCheck {
}
process {
$RegData = (Get-ItemProperty -Path "Registry::$($RegKey)" -Name $RegValue -ErrorAction SilentlyContinue).$RegValue
$Vulnerable = $RegData -ge 1
$Vulnerable = $False
$DisableLUAInRepair = (Get-ItemProperty -Path "registry::$($RegKey)" -Name $RegValueDisableLUAInRepair -ErrorAction SilentlyContinue).$RegValueDisableLUAInRepair
$SecureRepairPolicy = (Get-ItemProperty -Path "registry::$($RegKey)" -Name $RegValueSecureRepairPolicy -ErrorAction SilentlyContinue).$RegValueSecureRepairPolicy
$SecureRepairWhitelist = Get-Item -Path "registry::$($RegKey)\SecureRepairWhitelist" -ErrorAction SilentlyContinue
$SecureRepairWhitelistCount = $(if ($SecureRepairWhitelist) { $SecureRepairWhitelist.ValueCount } else { 0 })
if ($DisableLUAInRepair -ge 1) {
$Vulnerable = $True
$Description = $DisableLUAInRepairDescriptions[1]
}
else {
$Description = $DisableLUAInRepairDescriptions[0]
if (($SecureRepairWhitelistCount -gt 0) -and ($SecureRepairPolicy -eq 2)) {
$Vulnerable = $True
$BaseSeverity = $script:SeverityLevel::Low
$Description = "$($Description) A whitelist of MSI packages is set."
}
}
$Result = New-Object -TypeName PSObject
$Result | Add-Member -MemberType "NoteProperty" -Name "Key" -Value $RegKey
$Result | Add-Member -MemberType "NoteProperty" -Name "Value" -Value $RegValue
$Result | Add-Member -MemberType "NoteProperty" -Name "Data" -Value $(if ($null -eq $RegData) { "(null)" } else { $RegData })
$Result | Add-Member -MemberType "NoteProperty" -Name "DisableLUAInRepair" -Value $(if ($null -eq $DisableLUAInRepair) { "(null)" } else { $DisableLUAInRepair })
$Result | Add-Member -MemberType "NoteProperty" -Name "SecureRepairPolicy" -Value $(if ($null -eq $SecureRepairPolicy) { "(null)" } else { $SecureRepairPolicy })
$Result | Add-Member -MemberType "NoteProperty" -Name "SecureRepairWhitelistCount" -Value $SecureRepairWhitelistCount
$Result | Add-Member -MemberType "NoteProperty" -Name "Description" -Value $DisableLUAInRepairDescriptions[[UInt32]$Vulnerable]
$CheckResult = New-Object -TypeName PSObject
@@ -1219,6 +1237,110 @@ function Invoke-MsiAutomaticRepairUacPromptCheck {
}
}
function Invoke-MsiAutomaticRepairWhitelistCheck {
<#
.SYNOPSIS
Enumerate whitelisted MSI packages with custom actions that may be leveraged for local privilege escalation.
.DESCRIPTION
This cmdlet enumerates MSI packages in the 'SecureRepairWhitelist' (Windows Installer registry key) and attempts to determine if they have custom actions set that may be exploited for elevating privileges locally.
.EXAMPLE
PS C:\> Invoke-MsiAutomaticRepairWhitelistCheck
...
Path : C:\WINDOWS\Installer\1b4a8b.msi
ProductCode : C53ECC06-72B1-FEBF-D241-038A4BC83E57
Name : Application Verifier x64 External Package (OnecoreUAP)
Vendor : Microsoft
Version : 10.1.22621.5040
AllUsers : 1
CAName : AVRFInstall_UnInstallwow64External
CASource : WixCA
CATarget : CAQuietExec
CATargetExpanded : CAQuietExec
CAExeType : Dll
CASecurityContextFlags : NoImpersonate
CARunAsSystem : True
CABinaryExtractCommand : Invoke-MsiExtractBinaryData -Path "C:\WINDOWS\Installer\1b4a8b.msi" -Name "WixCA" -OutputPath "WixCA.dll"
...
.NOTES
This check is inspired by previous work from @garatc (https://github.com/garatc), see MSIAudit (https://github.com/garatc/MSIAudit).
.LINK
https://support.microsoft.com/en-us/topic/unexpected-uac-prompts-when-running-msi-repair-operations-after-installing-the-august-2025-windows-security-update-5806f583-e073-4675-9464-fe01974df273
#>
[CmdletBinding()]
param (
[UInt32] $BaseSeverity
)
begin {
# https://support.microsoft.com/en-us/topic/unexpected-uac-prompts-when-running-msi-repair-operations-after-installing-the-august-2025-windows-security-update-5806f583-e073-4675-9464-fe01974df273
$InstallerKeyPath = "HKLM\SOFTWARE\Policies\Microsoft\Windows\Installer"
}
process {
$Results = @()
$SecureRepairPolicyValue = Get-ItemProperty -Path "registry::$($InstallerKeyPath)" -Name "SecureRepairPolicy" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SecureRepairPolicy
if ($SecureRepairPolicyValue -eq 2) {
$Exceptions = [String[]] (Get-Item -Path "registry::$($InstallerKeyPath)\SecureRepairWhitelist" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Property)
$WhitelistedMsiFiles = Find-MsiFile -ProductCode $Exceptions | Where-Object { -not [String]::IsNullOrEmpty($_) }
$ProgressCount = 0
Write-Progress -Activity "Checking whitelisted MSI files (0/$($WhitelistedMsiFiles.Count))..." -Status "0% Complete:" -PercentComplete 0
foreach ($WhitelistedMsiFile in $WhitelistedMsiFiles) {
$ProgressPercent = [UInt32] ($ProgressCount * 100 / $WhitelistedMsiFiles.Count)
Write-Progress -Activity "Checking whitelisted MSI files ($($ProgressCount)/$($WhitelistedMsiFiles.Count)): $($WhitelistedMsiFile)" -Status "$($ProgressPercent)% Complete:" -PercentComplete $ProgressPercent
$Candidates = Get-MsiFileItem -FilePath $WhitelistedMsiFile
foreach ($Candidate in $Candidates) {
$CandidateCustomActions = [Object[]] ($Candidate.CustomActions | Where-Object { $_.Candidate -eq $true })
if ($Candidate.AllUsers -ne 1) { continue }
if ($null -eq $Candidate.CustomActions) { continue }
if ($CandidateCustomActions.Count -eq 0) { continue }
$MsiObject = $Candidate | Select-Object -Property Path,ProductCode,Name,Vendor,Version,AllUsers
$AnalyzeCommand = "Get-MsiFileItem -FilePath `"$($MsiObject.Path)`" | Select-Object -ExpandProperty CustomActions | Where-Object { `$_.Candidate }"
$RepairCommand = "Start-Process -FilePath `"msiexec.exe`" -ArgumentList `"/fa $($MsiObject.Path)`""
$MsiObject | Add-Member -MemberType "NoteProperty" -Name "AnalyzeCommand" -Value $AnalyzeCommand
$MsiObject | Add-Member -MemberType "NoteProperty" -Name "RepairCommand" -Value $RepairCommand
foreach ($CandidateCustomAction in $CandidateCustomActions) {
$MsiObjectCopy = $MsiObject.PSObject.Copy()
$MsiObjectCopy | Add-Member -MemberType "NoteProperty" -Name "CAName" -Value $CandidateCustomAction.Action
$MsiObjectCopy | Add-Member -MemberType "NoteProperty" -Name "CASource" -Value $CandidateCustomAction.Source
$MsiObjectCopy | Add-Member -MemberType "NoteProperty" -Name "CATarget" -Value $CandidateCustomAction.Target
$MsiObjectCopy | Add-Member -MemberType "NoteProperty" -Name "CATargetExpanded" -Value $CandidateCustomAction.TargetExpanded
$MsiObjectCopy | Add-Member -MemberType "NoteProperty" -Name "CAExeType" -Value $CandidateCustomAction.ExeType
$MsiObjectCopy | Add-Member -MemberType "NoteProperty" -Name "CASecurityContextFlags" -Value $CandidateCustomAction.SecurityContextFlags
$MsiObjectCopy | Add-Member -MemberType "NoteProperty" -Name "CARunAsSystem" -Value $CandidateCustomAction.RunAsSystem
$MsiObjectCopy | Add-Member -MemberType "NoteProperty" -Name "CABinaryExtractCommand" -Value $CandidateCustomAction.BinaryExtractCommand
$Results += $MsiObjectCopy
}
}
$ProgressCount += 1
}
}
$CheckResult = New-Object -TypeName PSObject
$CheckResult | Add-Member -MemberType "NoteProperty" -Name "Result" -Value $Results
$CheckResult | Add-Member -MemberType "NoteProperty" -Name "Severity" -Value $(if ($Results) { $BaseSeverity } else { $script:SeverityLevel::None })
$CheckResult
}
}
function Invoke-CredentialDelegationCheck {
<#
.SYNOPSIS
+58 -16
View File
@@ -152,7 +152,7 @@ function Get-CustomAction {
begin {
$SystemFolders = Get-MsiSystemFolderProperty -Arch $Arch -AllUsers $AllUsers
$QuietExecFunctions = @("CAQuietExec", "CAQuietExec64", "WixQuietExec", "WixQuietExec64")
# $QuietExecFunctions = @("CAQuietExec", "CAQuietExec64", "WixQuietExec", "WixQuietExec64")
}
process {
@@ -179,7 +179,6 @@ function Get-CustomAction {
$SecurityContextFlags = ([string[]] (Get-CustomActionSecurityContextFlag -Type $Type)) -join ","
$TargetExpanded = Get-MsiExpandedString -String $Target -Database $Database -SystemFolders $SystemFolders
if ($TargetExpanded -eq $Target) { $TargetExpanded = $null }
# 0x0800 -> no impersonation, run in system context
$RunAsSystem = $([bool] ($Type -band 0x0800))
@@ -198,7 +197,7 @@ function Get-CustomAction {
$BinaryExtractCommand = "Invoke-MsiExtractBinaryData -Path `"$($FilePath)`" -Name `"$($Source)`" -OutputPath `"$($OutputFilename)`""
}
else {
$BinaryExtractCommand = "(null)"
$BinaryExtractCommand = "N/A (non-binary source)"
}
$Candidate = $false
@@ -206,15 +205,16 @@ function Get-CustomAction {
# CA must not be configured to run only on patch uninstall
(-not $RunOnPatchUninstallOnly) -and
# CA must run as SYSTEM
($RunAsSystem) -and
($RunAsSystem)
# ($RunAsSystem) -and
# If CA is a DLL, it must not be a "quiet exec" function
(
($ExeType -ne "Dll") -or
(
($ExeType -eq "Dll") -and
(-not ($QuietExecFunctions -contains $Target))
)
)
# (
# ($ExeType -ne "Dll") -or
# (
# ($ExeType -eq "Dll") -and
# (-not ($QuietExecFunctions -contains $Target))
# )
# )
) {
$Candidate = $true
}
@@ -224,7 +224,7 @@ function Get-CustomAction {
$CustomAction | Add-Member -MemberType "NoteProperty" -Name "Type" -Value $Type
$CustomAction | Add-Member -MemberType "NoteProperty" -Name "Source" -Value $Source
$CustomAction | Add-Member -MemberType "NoteProperty" -Name "Target" -Value $Target
if ($TargetExpanded) { $CustomAction | Add-Member -MemberType "NoteProperty" -Name "TargetExpanded" -Value $TargetExpanded }
$CustomAction | Add-Member -MemberType "NoteProperty" -Name "TargetExpanded" -Value $TargetExpanded
$CustomAction | Add-Member -MemberType "NoteProperty" -Name "ExeType" -Value $ExeType
$CustomAction | Add-Member -MemberType "NoteProperty" -Name "SourceType" -Value $SourceType
$CustomAction | Add-Member -MemberType "NoteProperty" -Name "ReturnProcessing" -Value $ReturnProcessing
@@ -715,15 +715,15 @@ function Get-MsiItem {
$Installer = New-Object -ComObject WindowsInstaller.Installer
$Database = Invoke-MsiOpenDatabase -Installer $Installer -Path $Path -Mode 0
$IdentifyingNumber = [string] (Get-MsiProperty -Database $Database -Property "ProductCode")
$ProductCode = [string] (Get-MsiProperty -Database $Database -Property "ProductCode")
$Name = [string] (Get-MsiProperty -Database $Database -Property "ProductName")
$Vendor = [string] (Get-MsiProperty -Database $Database -Property "Manufacturer")
$Version = [string] (Get-MsiProperty -Database $Database -Property "ProductVersion")
$AllUsers = Get-MsiProperty -Database $Database -Property "ALLUSERS"
# Extract the GUID value, without the curly braces.
if ($IdentifyingNumber -match "(\d|[A-F]){8}-((\d|[A-F]){4}-){3}((\d|[A-F]){12})") {
$IdentifyingNumber = $Matches[0]
if ($ProductCode -match "(\d|[A-F]){8}-((\d|[A-F]){4}-){3}((\d|[A-F]){12})") {
$ProductCode = $Matches[0]
}
# If ALLUSERS is not defined, the default is "per-user", which corresponds to a value of 0.
@@ -732,7 +732,7 @@ function Get-MsiItem {
$MsiItem = New-Object -TypeName PSObject
$MsiItem | Add-Member -MemberType "NoteProperty" -Name "Path" -Value $Path
$MsiItem | Add-Member -MemberType "NoteProperty" -Name "IdentifyingNumber" -Value $(if ($IdentifyingNumber) { $IdentifyingNumber.Trim() })
$MsiItem | Add-Member -MemberType "NoteProperty" -Name "ProductCode" -Value $(if ($ProductCode) { $ProductCode.Trim() })
$MsiItem | Add-Member -MemberType "NoteProperty" -Name "Name" -Value $(if ($Name) { $Name.Trim() })
$MsiItem | Add-Member -MemberType "NoteProperty" -Name "Vendor" -Value $(if ($Vendor) { $Vendor.Trim() })
$MsiItem | Add-Member -MemberType "NoteProperty" -Name "Version" -Value $(if ($Version) { $Version.Trim() })
@@ -776,4 +776,46 @@ function Get-MsiFileItem {
Get-MsiItem -Path $FilePath
}
}
}
function Find-MsiFile {
[OutputType([String[]])]
[CmdletBinding()]
param (
[String[]] $ProductCode
)
begin {
$InstallerPath = Join-Path -Path $env:windir -ChildPath "Installer"
$MsiFilePaths = [String[]] (Get-ChildItem -Path "$($InstallerPath)\*.msi" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName)
}
process {
for ($i = 0; $i -lt $ProductCode.Count; $i++) {
$ProductCode[$i] = ($ProductCode[$i] -replace '{','') -replace '}',''
}
$ProgressCount = 0
Write-Progress -Activity "Enumerating MSI files (0/$($MsiFilePaths.Count))..." -Status "0% Complete:" -PercentComplete 0
foreach ($MsiFilePath in $MsiFilePaths) {
$ProgressPercent = [UInt32] ($ProgressCount * 100 / $MsiFilePaths.Count)
Write-Progress -Activity "Enumerating MSI files ($($ProgressCount)/$($MsiFilePaths.Count)): $($MsiFilePath)" -Status "$($ProgressPercent)% Complete:" -PercentComplete $ProgressPercent
$Installer = New-Object -ComObject WindowsInstaller.Installer
$Database = Invoke-MsiOpenDatabase -Installer $Installer -Path $MsiFilePath -Mode 0
$MsiProductCode = ([String] (Get-MsiProperty -Database $Database -Property "ProductCode")).Trim()
$null = [System.Runtime.InteropServices.Marshal]::ReleaseComObject($Installer)
if ($MsiProductCode -match "(\d|[A-F]){8}-((\d|[A-F]){4}-){3}((\d|[A-F]){12})") {
$MsiProductCode = $Matches[0]
}
if ($ProductCode -contains $MsiProductCode) {
$MsiFilePath
}
$ProgressCount += 1
}
}
}