From ff06672de291d9158f09881969866e908f31e356 Mon Sep 17 00:00:00 2001 From: Akash Sharma <77853107+SharmaAkash1@users.noreply.github.com> Date: Thu, 18 Mar 2021 21:26:26 +0530 Subject: [PATCH 01/10] Adding changes to reduce false positives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The changes made in the script are as follows: 1. For Cve26858 we only show the error if the path in the error message “Download failed and temporary file needs to be removed” is invalid (does not start with C:\Program Files\Microsoft\Exchange Server\V15\ClientAccess\OAB) 2. For set-virtual directory we throw an error only if the url is invalid 3. For the get-suspicious files we only show those if one of the 4 vulnerabilities (Cve26855 Or Cve26857 or Cve26858 or Cve27065 ) is found. This was done because even customers who did not have any vulnerabilities were alerted due to the presence of some zip files that they had created in their system. These false positives lead to a lot of confusion and the uneasiness for the customers. 4. We also show an additional error message regarding web shells if we find logs of successful reset-virtualdirectory hits (having a bad anchor mailbox object) in the httpsproxy folder. --- Security/src/Test-ProxyLogon.ps1 | 141 ++++++++++++++++++++++++------- 1 file changed, 110 insertions(+), 31 deletions(-) diff --git a/Security/src/Test-ProxyLogon.ps1 b/Security/src/Test-ProxyLogon.ps1 index 4a7aa28953..4a0bde0013 100644 --- a/Security/src/Test-ProxyLogon.ps1 +++ b/Security/src/Test-ProxyLogon.ps1 @@ -1,4 +1,4 @@ -# Checks for signs of exploit from CVE-2021-26855, 26858, 26857, and 27065. +# Checks for signs of exploit from CVE-2021-26855, 26858, 26857, and 27065. # # Examples # @@ -109,7 +109,7 @@ begin { "HttpStatus" ) - $files = (Get-ChildItem -Recurse -Path $HttpProxyPath -Filter '*.log').FullName + $files = [System.Array](Get-ChildItem -Recurse -Path $HttpProxyPath -Filter '*.log').FullName $allResults = @{ Hits = [System.Collections.ArrayList]@() @@ -169,8 +169,30 @@ begin { Write-Host " Exchange 2013 or later not found. Skipping CVE-2021-26858 test." return } + + $allResults = @{ + downloadPaths = [System.Collections.ArrayList]@() + filePaths = [System.Collections.ArrayList]@() + } - Get-ChildItem -Recurse -Path "$exchangePath\Logging\OABGeneratorLog" | Select-String "Download failed and temporary file" -List | Select-Object -ExpandProperty Path + $files = [System.Array](Get-ChildItem -Recurse -Path "$exchangePath\Logging\OABGeneratorLog" | Select-String "Download failed and temporary file" -List | Select-Object -ExpandProperty Path) + + for( $i=0; $i -lt $files.Count; $i++) + { + $maliciousPathFound = $false + $loginstance = Select-String -Path $files[$i] -Pattern "Download failed and temporary file" + foreach ($logLine in $loginstance) { + $path = ([String]$logLine | Select-String -Pattern 'Download failed and temporary file (.*?) needs to be removed').Matches.Groups[1].value + if($path -ne $null -and (-not ($path.StartsWith("'$exchangePath" + "ClientAccess\OAB","CurrentCultureIgnoreCase")))){ + [Void]$allResults.downloadPaths.Add( [String]$path ) + $maliciousPathFound = $true + } + } + if($maliciousPathFound){ + $allResults.FilePaths.Add([String]$files[$i]) + } + } + return $allResults } function Get-Cve27065 { @@ -178,12 +200,37 @@ begin { param () $exchangePath = Get-ExchangeInstallPath - if ($null -eq $exchangePath) { - Write-Host " Exchange 2013 or later not found. Skipping CVE-2021-27065 test." - return + + $outProps = @( + "DateTime", "RequestId", "ClientIPAddress", "UrlHost", + "UrlStem", "RoutingHint", "UserAgent", "AnchorMailbox", + "HttpStatus" + ) + + $files =[System.Array](Get-ChildItem -Recurse -Path "$exchangePath\Logging\HttpProxy\Ecp" -Filter '*.log').FullName + $allResults = @{ + resetVDirHits = [System.Collections.ArrayList]@() + resetVDirFiles = [System.Collections.ArrayList]@() + setVDirMaliciousUrlLogs = [System.Collections.ArrayList]@() } + For ( $i = 0; $i -lt $files.Count; ++$i ) { - Get-ChildItem -Recurse -Path "$exchangePath\Logging\ECP\Server\*.log" -ErrorAction SilentlyContinue | Select-String "Set-.+VirtualDirectory" -List | Select-Object -ExpandProperty Path + if ((Get-ChildItem $files[$i] -ErrorAction SilentlyContinue | Select-String -Pattern "ServerInfo~").Count -gt 0) { + + $FoundMaliciousResetVDir = $false + Import-Csv -Path $files[$i] -ErrorAction SilentlyContinue | Where-Object { $_.AnchorMailbox -Like 'ServerInfo~*/*Reset*VirtualDirectory#' -and $_.HttpStatus -eq 200} | + Select-Object -Property $outProps | + ForEach-Object { + [Void]$allResults.resetVDirHits.Add( $_ ) + $FoundMaliciousResetVDir = $true + } + if($FoundMaliciousResetVDir){ + [Void]$allResults.resetVDirFiles.Add( $files[$i] ) + } + } + } + $allResults.setVDirMaliciousUrlLogs = Get-ChildItem -Recurse -Path "$exchangePath\Logging\ECP\Server\*.log" -ErrorAction SilentlyContinue | Select-String "Set-.+VirtualDirectory.+?(?=Url).+<\w+.*>(.*?)<\/\w+>.+?(?=VirtualDirectory)" -List | Select-Object -ExpandProperty Path + return $allResults } function Get-SuspiciousFile { @@ -260,14 +307,15 @@ begin { ComputerName = $env:COMPUTERNAME Cve26855 = Get-Cve26855 Cve26857 = @(Get-Cve26857) - Cve26858 = @(Get-Cve26858) - Cve27065 = @(Get-Cve27065) - Suspicious = @(Get-SuspiciousFile) + Cve26858 = Get-Cve26858 + Cve27065 = Get-Cve27065 LogAgeDays = Get-LogAge IssuesFound = $false + Suspicious = $null } - if ($results.Cve26855.Hits.Count -or $results.Cve26857.Count -or $results.Cve26858.Count -or $results.Cve27065.Count -or $results.Suspicious.Count) { + if($results.Cve26855.Hits.Count -or $results.Cve26857.Count -or $results.Cve26858.downloadPaths.Count -or $results.Cve27065.resetVDirHits.Count -or $results.Cve27065.setVDirMaliciousUrlLogs.Count){ + $results.Suspicious = @(Get-SuspiciousFile) $results.IssuesFound = $true } @@ -416,36 +464,40 @@ begin { } Write-Host "" } - if ($report.Cve26858.Count -gt 0) { + if ($report.Cve26858.downloadPaths.Count -gt 0) { Write-Host " [CVE-2021-26858] Suspicious activity found in OAB generator logs!" -ForegroundColor Red - Write-Host " Please review the following files for 'Download failed and temporary file' entries:" - foreach ($entry in $report.Cve26858) { - Write-Host " $entry" - if ($CollectFiles -and $isLocalMachine) { - Write-Host " Copying Files:" - if (-not (Test-Path -Path "$($LogFileOutPath)\CVE26858")) { - Write-Host " Creating CVE26858 Collection Directory`n`r" - New-Item "$($LogFileOutPath)\CVE26858" -ItemType Directory -Force | Out-Null - } + Write-Host " Webshells possibly downloaded in file system. Explore below locations:" -ForegroundColor Red + if (-not $DisplayOnly) { + $newFileDownloadPaths = Join-Path -Path $OutPath -ChildPath "$($report.ComputerName)-Cve-2021-26858-DownloadPaths.log" + $newFileForFilePaths = Join-Path -Path $OutPath -ChildPath "$($report.ComputerName)-Cve-2021-26858.log" + $report.Cve26858.downloadPaths | Set-Content -Path $newFileDownloadPaths + $report.Cve26858.filePaths | Set-Content -Path $newFileForFilePaths + Write-Host " Report exported to: $newFileForFilePaths" + Write-Host " Report exported to: $newFileDownloadPaths" + } else { + $report.Cve26858.downloadPaths | Out-Host + } + if ($CollectFiles -and $isLocalMachine) { + Write-Host " Copying Files:" + if (-not (Test-Path -Path "$($LogFileOutPath)\CVE26858")) { + Write-Host " Creating CVE26858 Collection Directory" + New-Item "$($LogFileOutPath)\CVE26858" -ItemType Directory -Force | Out-Null + } + foreach ($entry in $report.Cve26858.filePaths) { if (Test-Path -Path $entry) { Write-Host " Copying $($entry) to $($LogFileOutPath)\CVE26858" -ForegroundColor Green Copy-Item -Path $entry -Destination "$($LogFileOutPath)\CVE26858" } else { - Write-Host " Warning: Unable to copy file $($entry.Path). File does not exist.`n`r " -ForegroundColor Red + Write-Host " Warning: Unable to copy file $($entry). File does not exist." -ForegroundColor Red } } } - if (-not $DisplayOnly) { - $newFile = Join-Path -Path $OutPath -ChildPath "$($report.ComputerName)-Cve-2021-26858.log" - $report.Cve26858 | Set-Content -Path $newFile - Write-Host " Report exported to: $newFile" - } Write-Host "" } - if ($report.Cve27065.Count -gt 0) { + if ($report.Cve27065.setVDirMaliciousUrlLogs.Count -gt 0) { Write-Host " [CVE-2021-27065] Suspicious activity found in ECP logs!" -ForegroundColor Red - Write-Host " Please review the following files for 'Set-*VirtualDirectory' entries:" - foreach ($entry in $report.Cve27065) { + Write-Host " Please review the following files for 'Set-*VirtualDirectory' entries (potentially malicious urls used):" + foreach ($entry in $report.Cve27065.setVDirMaliciousUrlLogs) { Write-Host " $entry" if ($CollectFiles -and $isLocalMachine) { Write-Host " Copying Files:" @@ -463,11 +515,38 @@ begin { } if (-not $DisplayOnly) { $newFile = Join-Path -Path $OutPath -ChildPath "$($report.ComputerName)-Cve-2021-27065.log" - $report.Cve27065 | Set-Content -Path $newFile + $report.Cve27065.setVDirMaliciousUrlLogs | Set-Content -Path $newFile Write-Host " Report exported to: $newFile" } Write-Host "" } + if ($report.Cve27065.resetVDirHits.Count -gt 0) { + Write-Host " [CVE-2021-27065] Webshell possibly downloaded in file system" -ForegroundColor Red + Write-Host " Please scan your file system for any malicious webshells. Reset-VirtualDirectory entries:" + if (-not $DisplayOnly) { + $newFile = Join-Path -Path $OutPath -ChildPath "$($report.ComputerName)-Cve-2021-27065-ResetVDir.csv" + $report.Cve27065.resetVDirHits | Export-Csv -Path $newFile + Write-Host " Report exported to: $newFile" + } else { + $report.Cve27065.resetVDirHits | Format-Table DateTime, AnchorMailbox -AutoSize | Out-Host + } + if ($CollectFiles -and $isLocalMachine) { + Write-Host " Copying Files:" + if (-not (Test-Path -Path "$($LogFileOutPath)\Cve27065")) { + Write-Host " Creating Cve27065 Collection Directory" + New-Item "$($LogFileOutPath)\Cve27065" -ItemType Directory -Force | Out-Null + } + foreach ($entry in $report.Cve27065.resetVDirFiles) { + if (Test-Path -Path $entry) { + Write-Host " Copying $($entry) to $($LogFileOutPath)\Cve27065" -ForegroundColor Green + Copy-Item -Path $entry -Destination "$($LogFileOutPath)\Cve27065" + } else { + Write-Host " Warning: Unable to copy file $($entry). File does not exist." -ForegroundColor Red + } + } + } + Write-Host "" + } if ($report.Suspicious.Count -gt 0) { Write-Host " Other suspicious files found: $(@($report.Suspicious).Count)" if (-not $DisplayOnly) { From fb3840b5a938c93bed76a66d0403c71def2648e7 Mon Sep 17 00:00:00 2001 From: Bill Long Date: Thu, 18 Mar 2021 11:59:38 -0500 Subject: [PATCH 02/10] Fix sorting in release description --- .build/Build.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.build/Build.ps1 b/.build/Build.ps1 index 590e8a78eb..87f2a86d9f 100644 --- a/.build/Build.ps1 +++ b/.build/Build.ps1 @@ -29,8 +29,8 @@ $scriptFiles = Get-ChildItem -Path $repoRoot -Directory | Where-Object { $_.Name -ne ".build" } | ForEach-Object { Get-ChildItem -Path $_.FullName *.ps1 -Recurse } | Where-Object { -not $_.Name.Contains(".Tests.ps1") -and - -not $_.Name.Contains(".NotPublished.ps1") } -Sort-Object Name | + -not $_.Name.Contains(".NotPublished.ps1") } | + Sort-Object Name | ForEach-Object { $_.FullName } $nonUnique = @($scriptFiles | ForEach-Object { [IO.Path]::GetFileName($_) } | Group-Object | Where-Object { $_.Count -gt 1 }) From c85f083682bf9de4776f8cd15901ca7a527f4fed Mon Sep 17 00:00:00 2001 From: SharmaAkash1 Date: Thu, 18 Mar 2021 22:57:02 +0530 Subject: [PATCH 03/10] Formatting fixes to tets-proxyLogon --- Security/src/Test-ProxyLogon.ps1 | 43 ++++++++++++++++---------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/Security/src/Test-ProxyLogon.ps1 b/Security/src/Test-ProxyLogon.ps1 index 4a0bde0013..5be61dde8e 100644 --- a/Security/src/Test-ProxyLogon.ps1 +++ b/Security/src/Test-ProxyLogon.ps1 @@ -1,4 +1,4 @@ -# Checks for signs of exploit from CVE-2021-26855, 26858, 26857, and 27065. +# Checks for signs of exploit from CVE-2021-26855, 26858, 26857, and 27065. # # Examples # @@ -169,29 +169,28 @@ begin { Write-Host " Exchange 2013 or later not found. Skipping CVE-2021-26858 test." return } - + $allResults = @{ downloadPaths = [System.Collections.ArrayList]@() - filePaths = [System.Collections.ArrayList]@() + filePaths = [System.Collections.ArrayList]@() } $files = [System.Array](Get-ChildItem -Recurse -Path "$exchangePath\Logging\OABGeneratorLog" | Select-String "Download failed and temporary file" -List | Select-Object -ExpandProperty Path) - for( $i=0; $i -lt $files.Count; $i++) - { + for ( $i = 0; $i -lt $files.Count; $i++) { $maliciousPathFound = $false $loginstance = Select-String -Path $files[$i] -Pattern "Download failed and temporary file" foreach ($logLine in $loginstance) { $path = ([String]$logLine | Select-String -Pattern 'Download failed and temporary file (.*?) needs to be removed').Matches.Groups[1].value - if($path -ne $null -and (-not ($path.StartsWith("'$exchangePath" + "ClientAccess\OAB","CurrentCultureIgnoreCase")))){ + if ($null -ne $path -and (-not ($path.StartsWith("'$exchangePath" + "ClientAccess\OAB", "CurrentCultureIgnoreCase")))) { [Void]$allResults.downloadPaths.Add( [String]$path ) $maliciousPathFound = $true } } - if($maliciousPathFound){ + if ($maliciousPathFound) { $allResults.FilePaths.Add([String]$files[$i]) } - } + } return $allResults } @@ -207,29 +206,29 @@ begin { "HttpStatus" ) - $files =[System.Array](Get-ChildItem -Recurse -Path "$exchangePath\Logging\HttpProxy\Ecp" -Filter '*.log').FullName + $files = [System.Array](Get-ChildItem -Recurse -Path "$exchangePath\Logging\HttpProxy\Ecp" -Filter '*.log').FullName $allResults = @{ - resetVDirHits = [System.Collections.ArrayList]@() - resetVDirFiles = [System.Collections.ArrayList]@() + resetVDirHits = [System.Collections.ArrayList]@() + resetVDirFiles = [System.Collections.ArrayList]@() setVDirMaliciousUrlLogs = [System.Collections.ArrayList]@() } For ( $i = 0; $i -lt $files.Count; ++$i ) { if ((Get-ChildItem $files[$i] -ErrorAction SilentlyContinue | Select-String -Pattern "ServerInfo~").Count -gt 0) { - + $FoundMaliciousResetVDir = $false - Import-Csv -Path $files[$i] -ErrorAction SilentlyContinue | Where-Object { $_.AnchorMailbox -Like 'ServerInfo~*/*Reset*VirtualDirectory#' -and $_.HttpStatus -eq 200} | - Select-Object -Property $outProps | - ForEach-Object { - [Void]$allResults.resetVDirHits.Add( $_ ) - $FoundMaliciousResetVDir = $true + Import-Csv -Path $files[$i] -ErrorAction SilentlyContinue | Where-Object { $_.AnchorMailbox -Like 'ServerInfo~*/*Reset*VirtualDirectory#' -and $_.HttpStatus -eq 200 } | + Select-Object -Property $outProps | + ForEach-Object { + [Void]$allResults.resetVDirHits.Add( $_ ) + $FoundMaliciousResetVDir = $true + } + if ($FoundMaliciousResetVDir) { + [Void]$allResults.resetVDirFiles.Add( $files[$i] ) } - if($FoundMaliciousResetVDir){ - [Void]$allResults.resetVDirFiles.Add( $files[$i] ) - } } } - $allResults.setVDirMaliciousUrlLogs = Get-ChildItem -Recurse -Path "$exchangePath\Logging\ECP\Server\*.log" -ErrorAction SilentlyContinue | Select-String "Set-.+VirtualDirectory.+?(?=Url).+<\w+.*>(.*?)<\/\w+>.+?(?=VirtualDirectory)" -List | Select-Object -ExpandProperty Path + $allResults.setVDirMaliciousUrlLogs = Get-ChildItem -Recurse -Path "$exchangePath\Logging\ECP\Server\*.log" -ErrorAction SilentlyContinue | Select-String "Set-.+VirtualDirectory.+?(?=Url).+<\w+.*>(.*?)<\/\w+>.+?(?=VirtualDirectory)" -List | Select-Object -ExpandProperty Path return $allResults } @@ -314,7 +313,7 @@ begin { Suspicious = $null } - if($results.Cve26855.Hits.Count -or $results.Cve26857.Count -or $results.Cve26858.downloadPaths.Count -or $results.Cve27065.resetVDirHits.Count -or $results.Cve27065.setVDirMaliciousUrlLogs.Count){ + if ($results.Cve26855.Hits.Count -or $results.Cve26857.Count -or $results.Cve26858.downloadPaths.Count -or $results.Cve27065.resetVDirHits.Count -or $results.Cve27065.setVDirMaliciousUrlLogs.Count) { $results.Suspicious = @(Get-SuspiciousFile) $results.IssuesFound = $true } From 3694aa4952a0b1098f529d4608183594ee2adb33 Mon Sep 17 00:00:00 2001 From: SharmaAkash1 Date: Thu, 18 Mar 2021 23:04:28 +0530 Subject: [PATCH 04/10] Used workaround to fix formatting error being thrown --- Security/src/Test-ProxyLogon.ps1 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Security/src/Test-ProxyLogon.ps1 b/Security/src/Test-ProxyLogon.ps1 index 5be61dde8e..6027e3b166 100644 --- a/Security/src/Test-ProxyLogon.ps1 +++ b/Security/src/Test-ProxyLogon.ps1 @@ -216,14 +216,12 @@ begin { if ((Get-ChildItem $files[$i] -ErrorAction SilentlyContinue | Select-String -Pattern "ServerInfo~").Count -gt 0) { - $FoundMaliciousResetVDir = $false Import-Csv -Path $files[$i] -ErrorAction SilentlyContinue | Where-Object { $_.AnchorMailbox -Like 'ServerInfo~*/*Reset*VirtualDirectory#' -and $_.HttpStatus -eq 200 } | Select-Object -Property $outProps | ForEach-Object { [Void]$allResults.resetVDirHits.Add( $_ ) - $FoundMaliciousResetVDir = $true } - if ($FoundMaliciousResetVDir) { + if ($allResults.resetVDirHits.Count -gt 0) { [Void]$allResults.resetVDirFiles.Add( $files[$i] ) } } From c77a06fc13dfd033589aa80ebfcf1d39d7892684 Mon Sep 17 00:00:00 2001 From: Bill Long Date: Thu, 18 Mar 2021 12:44:11 -0500 Subject: [PATCH 05/10] Make script names download links for that release --- azure-pipelines.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1b90fa694c..0d3dbaf06c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -27,6 +27,8 @@ steps: - pwsh: | $tag = "v$((Get-Date).ToString(`"yy.MM.dd.HHmm`"))" Write-Host "##vso[task.setvariable variable=ReleaseTagValue]$tag" + (Get-Content .\dist\ScriptVersions.txt) -replace '^(\S+.ps1)', ('[$1](https://github.com/microsoft/CSS-Exchange/releases/download/' + $tag + '/$1)') | Out-File dist\ScriptVersions.txt + Get-Content dist\ScriptVersions.txt - task: EsrpCodeSigning@1 condition: and(succeeded(), ne (variables['Build.Reason'], 'PullRequest'), eq(variables['Build.SourceBranch'], 'refs/heads/release')) From 765cf5b9ef4d34028aca186c1f072bc377c5ea1e Mon Sep 17 00:00:00 2001 From: David Paulson Date: Wed, 17 Mar 2021 22:45:06 -0500 Subject: [PATCH 06/10] Created FixInstallerCache.ps1 Script --- Setup/FixInstallerCache.ps1 | 293 ++++++++++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 Setup/FixInstallerCache.ps1 diff --git a/Setup/FixInstallerCache.ps1 b/Setup/FixInstallerCache.ps1 new file mode 100644 index 0000000000..cdc1bd0a3a --- /dev/null +++ b/Setup/FixInstallerCache.ps1 @@ -0,0 +1,293 @@ +[CmdletBinding(DefaultParameterSetName = "CopyFromCu")] +param( + [Parameter(Mandatory = $true, ParameterSetName = "CopyFromCu")] + [ValidateNotNullOrEmpty()] + [string]$CurrentCuRootDirectory, + [Parameter(Mandatory = $true, ParameterSetName = "CopyFromServer")] + [ValidateNotNullOrEmpty()] + [string[]]$MachineName +) + +Function Receive-Output { + param( + [string]$ForegroundColor = "Gray" + ) + process { + Write-Host $_ -ForegroundColor $ForegroundColor + $_ | Out-File -FilePath $scriptLogging -Append + } +} + +#By doing it this way and looking at the registry, we get msp files as well. (Security Updates) +#Vs doing Get-CimInstance -ClassName Win32_Product +Function Get-InstallerPackages { + param( + [string[]]$FilterDisplayName + ) + $localPackageChildItems = Get-ChildItem -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer" -Recurse | + Where-Object { $_.Property -eq "LocalPackage" } + + $installerList = New-Object 'System.Collections.Generic.List[PSObject]' + + foreach ($regKey in $localPackageChildItems) { + + $filePackagePath = [IO.FileInfo] $regKey.GetValue("LocalPackage") + $item = $null + + if ($filePackagePath.Extension -eq ".msp") { + $revisionGuid = Get-GuidProductCodeFromString -GuidString $regKey.PSChildName + } else { + $productRegKey = "Registry::HKEY_CLASSES_ROOT\Installer\Products\$($regKey.PSParentPath.Split("\")[-1])" + + if (Test-Path $productRegKey) { + $item = Get-Item $productRegKey + try { + $revisionGuid = Get-GuidProductCodeFromString -GuidString ($item.GetValue("PackageCode")) + } catch { + + } + } else { + "Failed to find $productRegKey in order to get the revisionGuid value" | Receive-Output + } + } + + $displayName = $regKey.GetValue("DisplayName") + if ($null -ne $FilterDisplayName -and + -not ([string]::IsNullOrEmpty($displayName))) { + $inFilter = $false + + foreach ($filter in $FilterDisplayName) { + if ($displayName -like "*$filter*") { + $inFilter = $true + break + } + } + + if (!$inFilter) { + continue + } + } + + #Go one more step to see if the package is set with what we want. + $filePackageInfo = $null + $foundFile = Test-Path $filePackagePath + $correctRevisionValue = $false + if ($foundFile) { + $filePackageInfo = Get-FileInformation -File $filePackagePath + } + + if ($foundFile -and + $null -ne $filePackageInfo) { + $correctRevisionValue = $filePackageInfo.RevisionNumber.Contains($revisionGuid.ToString().ToUpper()) + } + + $installerList.Add([PSCustomObject]@{ + DisplayName = $displayName + DisplayVersion = $regKey.GetValue("DisplayVersion") + CacheLocation = $filePackagePath + FoundFileInCache = $foundFile + ValidMsi = $correctRevisionValue + UninstallString = $regKey.GetValue("UninstallString") + RevisionGuid = $revisionGuid + RevisionNumber = "{$($revisionGuid.ToString().ToUpper())}" + PackageInfo = $filePackageInfo + ProductItem = $item + InstallerItem = $regKey + }) + } + + return $installerList +} + +Function Get-GuidProductCodeFromString { + param( + [string]$GuidString + ) + $index = 0 + $newGuidString = [string]::Empty + + while ($index -lt $GuidString.Length) { + $l = 2 + if ($index -lt 8) { + $l = 8 + } elseif ($index -lt 16) { + $l = 4 + } + + $substringArray = $GuidString.Substring($index, $l).ToCharArray() + [Array]::Reverse($substringArray) + $newGuidString += $substringArray -join '' + $index += $l + } + + return [guid]$newGuidString +} + +Function Get-FileInformation { + param( + [IO.FileInfo]$File, + [bool]$AllowFileSubjectOnly = $false + ) + $installerCOM = $null + try { + + $installerCOM = New-Object -ComObject "WindowsInstaller.Installer" + + if (-not($installerCOM) -and + -not($AllowFileSubjectOnly)) { + "Failed to create 'WindowsInstaller.Installer' COM object. This can lead to issues with validation of the script." | Receive-Output -ForegroundColor Red + #If we fail doing this, we shouldn't continue. + exit + } + + if ($installerCOM) { + #This would be nice to have i think. Not fully sure how to call it however. + #https://docs.microsoft.com/en-us/windows/win32/msi/installer-fileversion + + #https://docs.microsoft.com/en-us/windows/win32/msi/installer-summaryinformation + $summaryInformation = $installerCOM.GetType().InvokeMember("SummaryInformation", [System.Reflection.BindingFlags]::GetProperty, $null, $installerCOM, @($File.FullName, 0)) + #https://docs.microsoft.com/en-us/windows/win32/msi/summaryinfo-summaryinfo + $subject = $summaryInformation.GetType().InvokeMember("Property", [System.Reflection.BindingFlags]::GetProperty, $null, $summaryInformation, @(3)) + $revNumber = $summaryInformation.GetType().InvokeMember("Property", [System.Reflection.BindingFlags]::GetProperty, $null, $summaryInformation, @(9)) + + return [PSCustomObject]@{ + FilePath = $File.FullName + Subject = $subject + RevisionNumber = $revNumber.ToUpper() + } + } + + + $shellApplication = New-Object -ComObject "Shell.Application" + + if (-not($shellApplication)) { + "Failed to create 'Shell.Application' COM Object. This can lead to issues with validation of the script." | Receive-Output -ForegroundColor Red + exit + } + + $fileItem = Get-Item $File + $shellFolder = $shellApplication.NameSpace($fileItem.Directory.FullName) + $subject = $shellFolder.GetDetailsOf($shellFolder.ParseName($fileItem.Name), 22) + + } catch { + $Error[0].Exception | Receive-Output + $Error[0].ScriptStackTrace | Receive-Output + Write-Error "Failed to properly process file $($File.FullName) to get required MSI information" + exit + } +} + +Function MainIsoCopy { + $installedVersion = (Get-ItemProperty -Path Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ExchangeServer\v15\AdminTools -ErrorAction SilentlyContinue).PostSetupVersion + $filterDisplayNames = @("Microsoft Lync Server", "Exchange", "Microsoft Server Speech", "Microsoft Unified Communications") + + [IO.FileInfo]$cuExchangeMsi = "$CurrentCuRootDirectory\EXCHANGESERVER.msi" + + if (!(Test-Path $cuExchangeMsi)) { + #We want the root of the install directory, let the script handle the rest + Write-Error "Failed to find the root of the Exchange Setup directory. Trying to find $cuExchangeMsi" + exit + } + + $cuExchangeFileInfo = Get-FileInformation -File $cuExchangeMsi + + if (!($cuExchangeFileInfo.Subject.Contains($installedVersion))) { + "Failed to find the correct version of the ISO" | Receive-Output -ForegroundColor Red + "Looking for version $installedVersion" | Receive-Output -ForegroundColor Red + "Found Version $($cuExchangeFileInfo.Subject.Substring($cuExchangeFileInfo.Subject.LastIndexOf("v")+1))" | Receive-Output -ForegroundColor Red + Start-Sleep 1 + Write-Error "Failed to find correct ISO version" + exit + } + + $msiInstallerPackages = Get-InstallerPackages -FilterDisplayName $filterDisplayNames + $missingPackages = $msiInstallerPackages | Where-Object { $_.ValidMsi -eq $false } + $currentMissingPackages = $missingPackages.Count + $missingPackages | ForEach-Object { $_ | Select-Object DisplayName, DisplayVersion, RevisionNumber, ValidMsi, FoundFileInCache } | Receive-Output + $packagesInIso = Get-ChildItem -Recurse $CurrentCuRootDirectory | + Where-Object { $_.Name.ToLower().EndsWith(".msi") } | + ForEach-Object { return Get-FileInformation -File $_.FullName } + $fixedFiles = 0 + + foreach ($missingMsi in $missingPackages) { + $fileFound = $packagesInIso | Where-Object { $_.RevisionNumber -eq $missingMsi.RevisionNumber } + + if ($null -eq $fileFound) { + "Failed to find MSI - $($missingMsi.DisplayName) - $($missingMsi.RevisionNumber) - $($missingMsi.DisplayVersion)" | Receive-Output + } elseif ($fileFound.Count -gt 1) { + Write-Host "Found more than 1 MSI file that matched our revision number." | Receive-Output + } else { + "Copying file $($fileFound.FilePath) to $($missingMsi.CacheLocation)" | Receive-Output + Copy-Item $fileFound.FilePath $missingMsi.CacheLocation + $fixedFiles++ + } + } + + "Fixed $fixedFiles out of $currentMissingPackages" | Receive-Output +} + +Function MainMachineCopy { + + $msiInstallerPackages = Get-InstallerPackages -FilterDisplayName $filterDisplayNames + [System.Collections.Generic.List[PSObject]]$missingPackages = $msiInstallerPackages | Where-Object { $_.ValidMsi -eq $false } + $currentMissingPackages = $missingPackages.Count + + "Current Missing Files" | Receive-Output + #Fix later, figure out how to log this better. + $missingPackages | ForEach-Object { $_ | Select-Object DisplayName, DisplayVersion, RevisionNumber, ValidMsi, FoundFileInCache } | Receive-Output + + $runAgain = $false + + foreach ($machine in $MachineName) { + + $remoteInstallerCache = "\\$machine\c$\Windows\Installer" + + try { + $remoteFiles = Get-ChildItem $remoteInstallerCache -ErrorAction Stop | + Where-Object { $_.Name.ToLower().EndsWith(".msi") } | + ForEach-Object { + return Get-FileInformation -File $_.FullName + } + } catch { + Write-Error "Failed to get files from the following path: $remoteInstallerCache" + continue + } + + if ($runAgain) { + $msiInstallerPackages = Get-InstallerPackages -FilterDisplayName $filterDisplayNames + [System.Collections.Generic.List[PSObject]]$missingPackages = $msiInstallerPackages | Where-Object { $_.ValidMsi -eq $false } + } + + foreach ($missingMsi in $missingPackages) { + + $fileFound = $remoteFiles | Where-Object { $_.RevisionNumber -eq $missingMsi.RevisionNumber } + + if ($null -eq $fileFound) { + "Failed to find MSI - $($missingMsi.DisplayName) - $($missingMsi.RevisionNumber)" | Receive-Output + } elseif ($fileFound.Count -gt 1) { + Write-Host "Found more than 1 MSI file that matched our revision number." | Receive-Output + } else { + "Copying file $($fileFound.FilePath) to $($missingMsi.CacheLocation)" | Receive-Output + Copy-Item $fileFound.FilePath $missingMsi.CacheLocation + $fixedFiles++ + } + } + $runAgain = $true + } + + "Fixed $fixedFiles out of $currentMissingPackages" | Receive-Output +} + +Function Main { + $Script:scriptLogging = ".\InstallerCacheLogger.log" + + if ($PsCmdlet.ParameterSetName -eq "CopyFromCu") { + MainIsoCopy + return + } else { + MainMachineCopy + return + } +} + +Main \ No newline at end of file From da1fef7d743f7f4f3f235654cf188cd0aa7031a7 Mon Sep 17 00:00:00 2001 From: Bill Long Date: Thu, 18 Mar 2021 13:35:51 -0500 Subject: [PATCH 07/10] Fix 27065 --- Security/src/Test-ProxyLogon.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Security/src/Test-ProxyLogon.ps1 b/Security/src/Test-ProxyLogon.ps1 index 6027e3b166..43ad8dfdaa 100644 --- a/Security/src/Test-ProxyLogon.ps1 +++ b/Security/src/Test-ProxyLogon.ps1 @@ -216,12 +216,12 @@ begin { if ((Get-ChildItem $files[$i] -ErrorAction SilentlyContinue | Select-String -Pattern "ServerInfo~").Count -gt 0) { - Import-Csv -Path $files[$i] -ErrorAction SilentlyContinue | Where-Object { $_.AnchorMailbox -Like 'ServerInfo~*/*Reset*VirtualDirectory#' -and $_.HttpStatus -eq 200 } | - Select-Object -Property $outProps | - ForEach-Object { + $hits = @(Import-Csv -Path $files[$i] -ErrorAction SilentlyContinue | Where-Object { $_.AnchorMailbox -Like 'ServerInfo~*/*Reset*VirtualDirectory#' -and $_.HttpStatus -eq 200 } | + Select-Object -Property $outProps) + if ($hits.Count -gt 0) { + $hits | ForEach-Object { [Void]$allResults.resetVDirHits.Add( $_ ) } - if ($allResults.resetVDirHits.Count -gt 0) { [Void]$allResults.resetVDirFiles.Add( $files[$i] ) } } From 68d2d867ae91ad424374578ea989bef9c8109ddb Mon Sep 17 00:00:00 2001 From: David Paulson Date: Thu, 18 Mar 2021 13:34:57 -0500 Subject: [PATCH 08/10] Store MSI Copy Script --- Setup/Utils/CopyMsiOnly.NotPublished.ps1 | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Setup/Utils/CopyMsiOnly.NotPublished.ps1 diff --git a/Setup/Utils/CopyMsiOnly.NotPublished.ps1 b/Setup/Utils/CopyMsiOnly.NotPublished.ps1 new file mode 100644 index 0000000000..8e06d51f93 --- /dev/null +++ b/Setup/Utils/CopyMsiOnly.NotPublished.ps1 @@ -0,0 +1,21 @@ +[CmdletBinding()] +param( + [string]$CuRoot, + [string]$CopyToRoot +) + + +$msiFromCU = Get-ChildItem $CuRoot -Recurse | + Where-Object {$_.Name.ToLower().EndsWith(".msi")} + + +foreach($msi in $msiFromCU) { + + $copyTo = $msi.Directory.FullName.Replace($CuRoot, $CopyToRoot) + + if (!(Test-Path $copyTo)) { + New-Item $copyTo -ItemType Directory | Out-Null + } + + Copy-Item $msi.FullName $copyTo +} \ No newline at end of file From 3314e3e65e9954e2ec32571ac3fb5af092dbb0cf Mon Sep 17 00:00:00 2001 From: David Paulson Date: Thu, 18 Mar 2021 13:39:09 -0500 Subject: [PATCH 09/10] Added new script to readme --- Setup/README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Setup/README.md b/Setup/README.md index 30bec44376..cef70e21f9 100644 --- a/Setup/README.md +++ b/Setup/README.md @@ -43,4 +43,16 @@ This script is used to copy over missing dlls that might have occurred during a Parameter | Description ----------|------------ -[string]IsoRoot | The Root location of the ISO. Example: `D:` \ No newline at end of file +[string]IsoRoot | The Root location of the ISO. Example: `D:` + + +# [FixInstallerCache.ps1](https://github.com/microsoft/CSS-Exchange/releases/latest/download/FixInstallerCache.ps1) + +Download the latest release here: [https://github.com/microsoft/CSS-Exchange/releases/latest/download/FixInstallerCache.ps1](https://github.com/microsoft/CSS-Exchange/releases/latest/download/FixInstallerCache.ps1) + +This script is used to copy over the missing MSI files from the installer cache. + +Parameter | Description +----------|------------ +[string]CurrentCuRootDirectory | The root location of the current CU that you are on. +[string[]]MachineName | The name of the machine that we are waiting to try to copy the MSI files over from, if any are there. \ No newline at end of file From 93bfd2a865c38306189fa99c4f204ae682db3669 Mon Sep 17 00:00:00 2001 From: David Paulson Date: Thu, 18 Mar 2021 13:49:43 -0500 Subject: [PATCH 10/10] Fixed Code Formatting --- Setup/FixInstallerCache.ps1 | 4 ++-- Setup/Utils/CopyMsiOnly.NotPublished.ps1 | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Setup/FixInstallerCache.ps1 b/Setup/FixInstallerCache.ps1 index cdc1bd0a3a..975d2b16a7 100644 --- a/Setup/FixInstallerCache.ps1 +++ b/Setup/FixInstallerCache.ps1 @@ -1,3 +1,4 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Parameters are being used')] [CmdletBinding(DefaultParameterSetName = "CopyFromCu")] param( [Parameter(Mandatory = $true, ParameterSetName = "CopyFromCu")] @@ -44,7 +45,7 @@ Function Get-InstallerPackages { try { $revisionGuid = Get-GuidProductCodeFromString -GuidString ($item.GetValue("PackageCode")) } catch { - + "Failed to get the Revision Guid $($item.FullName)" | Receive-Output } } else { "Failed to find $productRegKey in order to get the revisionGuid value" | Receive-Output @@ -168,7 +169,6 @@ Function Get-FileInformation { $fileItem = Get-Item $File $shellFolder = $shellApplication.NameSpace($fileItem.Directory.FullName) $subject = $shellFolder.GetDetailsOf($shellFolder.ParseName($fileItem.Name), 22) - } catch { $Error[0].Exception | Receive-Output $Error[0].ScriptStackTrace | Receive-Output diff --git a/Setup/Utils/CopyMsiOnly.NotPublished.ps1 b/Setup/Utils/CopyMsiOnly.NotPublished.ps1 index 8e06d51f93..138a99e124 100644 --- a/Setup/Utils/CopyMsiOnly.NotPublished.ps1 +++ b/Setup/Utils/CopyMsiOnly.NotPublished.ps1 @@ -6,10 +6,10 @@ param( $msiFromCU = Get-ChildItem $CuRoot -Recurse | - Where-Object {$_.Name.ToLower().EndsWith(".msi")} + Where-Object { $_.Name.ToLower().EndsWith(".msi") } -foreach($msi in $msiFromCU) { +foreach ($msi in $msiFromCU) { $copyTo = $msi.Directory.FullName.Replace($CuRoot, $CopyToRoot)