Changing the PDF Reader

Context

The PDF reader has reached its end-of-life on June 6, 2022 (official announcement)

I was about to change the software by deploying its new major version from the same release channel but I encountered an undesired end-user experience. The deployment has been stopped because of the following behavior.

Issue

The Reader 2017 is being successfully replaced by the Reader 2020. If there’s a standard user logged on the computer, his pdf file association is broken.

Yes, I pulled the carpet under his feet but neither Microsoft, nor Adobe offer a decent solution to handle it and update it smoothly.

The end-user is being prompted.

Easy, it’s documented on this page. This great but it doesn’t fix the broken pdf file association.

I remembered a similar issue that I posted here in 2018.

Unfortunately it doesn’t help anymore. The security mechanism still sees that there’s something wrong under the UserChoice key. It’s reported in the event log and there’s a reset.

The irony here is that it’s Adobe Reader that resets it but to MSEdgePDF (D’Oh!)

Solution

My solution consists in not allowing a reset to be performed.

I’m setting a temporary Deny rule on the .pdf registry key.

# Set a Deny of user on .pdf key
$acl = Get-Acl Path $extRegKeyPath
$rule = New-Object System.Security.AccessControl.RegistryAccessRule (
$user,
([System.Security.AccessControl.RegistryRights]::CreateSubKey+[System.Security.AccessControl.RegistryRights]::ChangePermissions),
[System.Security.AccessControl.AccessControlType]::Deny
)
$null = $acl.AddAccessRule($rule)
Set-Acl Path $acl.Path AclObject $acl
view raw set-regdeny.ps1 hosted with ❤ by GitHub

The key is deleted by a logoff script (using reg.exe import to avoid an access denied) and the computer GPO that handles file association using the official guidance from Microsoft and Adobe restores everything beautifully at next logon.

Happy days, PowerShell saved the day again 😎

From MSRC API to ZDI chart

ZDI, a.k.a ZeroDayInitiative, has a nice chart about updates published by the MSRC

I wondered how I could get the same in a grid view with PowerShell…

#Requires -Module MsrcSecurityUpdates
(Get-MSRCCvrfDocument ID "$((Get-Date).ToString('yyyy-MMM',[System.Globalization.CultureInfo]'en-US'))").Vulnerability |
Foreach-Object {
$v = $_
$Disclosed = $Exploited = $null
$Disclosed = ([regex]'Publicly\sDisclosed:(?<D>(Yes|No));').Match("$(($v.Threats | Where-Object { $_.Type -eq 1}).Description.Value)") |
Select-Object ExpandProperty Groups| Select-Object Last 1 ExpandProperty Value
$Exploited = ([regex]'Exploited:(?<E>(Yes|No));').Match("$(($v.Threats | Where-Object { $_.Type -eq 1}).Description.Value)") |
Select-Object ExpandProperty Groups| Select-Object Last 1 ExpandProperty Value
[PSCustomObject]@{
CVEID = $v.CVE
Tag = $($v.Notes | Where-Object { $_.Type -eq 7}).Value
CNA = $($v.Notes | Where-Object {$_.Type -eq 8}).Value
Title = $v.Title.Value
Date = $($v.RevisionHistory | Select-Object First 1 ExpandProperty Date)
Revision = $($v.RevisionHistory | Select-Object First 1 ExpandProperty Number)
Severity = $( ($v.Threats | Where-Object { $_.Type -eq 3 }).Description | Select-Object ExpandProperty Value ErrorAction SilentlyContinue | Sort-Object Unique)
CVSS = '{0:N1}' -f $($v.CVSSScoreSets.BaseScore | Sort-Object Unique | ForEach-Object { [double]$_} | Sort-Object Descending | Select-Object First 1)
Public = $Disclosed
Exploited = $Exploited
Type = $( ($v.Threats | Where-Object { $_.Type -eq 0 }).Description | Select-Object ExpandProperty Value ErrorAction SilentlyContinue | Sort-Object Unique)
}
} |
Select-Object Property CVEID,Title,Severity,CVSS,Public,Exploited,Type |
Out-GridView

Here’s what the result looks like for February 2022:

About the DellBIOSProvider module and ConstrainedLanguage mode

  • Context

I’ve just started working with the DellBIOSProvider module available on the PowerShell Gallery and had to see how to integrate it smoothly in the environment so that it’s compatible with the Constrained language mode.

  • Issues

If I do:

Import-Module -name DellBIOSProvider -Force -Verbose

What could go wrong? 🙄
Well, it depends on what you do. If you go down to the Applocker rules path, it depends on the rules, their type, on what’s missing.
I’ve listed below a few common road blocks you may encounter:

  • psd1 or psm1 has a dedicated rule trusting/allowing it while the other doesn’t:
  • there is/are rule(s) to allow both .psd1 and psm1, but when it loads the dll (listed in the .psd1 manifest file), it fails because there’s a missing rule:

Import-Module : Could not load file or assembly ‘file:///C:\Program
Files\WindowsPowerShell\Modules\DellBIOSProvider\2.6.0\DellBIOSProvider.dll’ or one of its dependencies. Operation is
not supported. (Exception from HRESULT: 0x80131515)

  • there is/are rule(s) to allow both .psd1 and psm1, but the .psm1 uses dot sourcing and tries to load single .ps1 file that don’t have an allow rule:
  • this is what happens when you’ve rule(s) allowing it to load and it’s a 100% success
  • Solution

It appears that there are 2 solutions.

The 1rst one and the longest is about declaring rules that will allow any file contained in the module. Let’s have a look at the content of the module with the following command:

Get-AppLockerFileInformation -Path 'C:\Program Files\WindowsPowerShell\Modules\DellBIOSProvider\2.6.0\*' | ogv -PassThru

We can see above that the dll, ps1, psd1, psm1 and cat files are all signed 🙂
Only the txt and pdf files are unsigned. These 2 files are not loaded so we don’t care.

At this step, we can choose either to add either:
– a single Applocker rule for the Path
or
– a single Applocker rule containing all the files’ hashes
or
– a single Applocker rule trusting the Publisher
O=DELL INC, L=ROUND ROCK, S=TEXAS, C=US
or
– a mix of files’ hashes and publisher based rules

I’ve chosen the latest option because it’s the most precise. I’ve listed the rules in this XML policy file.

The Applocker GUI will allow you to create rules for dll, .psd1 and psm1 files if you copy them with a .ps1 file extension 😎

Let’s see the 2nd solution and the shortest one:

Dell provided a signed catalog file. It contains all the files’ hashes. To trust it, I only have to copy it to its system location. There are various ways of doing this listed on this page.

copy 'C:\Program Files\WindowsPowerShell\Modules\DellBIOSProvider\2.6.0\DellBIOSProvider.cat' "c:\Windows\system32\CatRoot\{F750E6C3-38EE-11D1-85E5-00C04FC295EE}"
Restart-Service CryptSvc -Force -Verbose

That’s all. It’s magic. You don’t need Applocker rules 😀

Group policies update/refresh without gpupdate.exe

  • Context

I was testing group policies, adding, removing them and using gpupdate.exe to apply changes. I messed with the Applocker gpo and set the PC in an unstable state.

I still had my PowerShell console opened but couldn’t use gpupdate.exe anymore. The Start menu wasn’t working anymore… 😦

Here’s what it looks like:

Usually, I’d just restart the computer and the transient state is cleared: either Applocker would work normally or would be disabled.

In this case, I couldn’t restart the computer because of Bitlocker. I was remote and the next time the laptop restarts it’d ask for a PIN. I couldn’t also suspend bitlocker for the next restart or simply disable it. Bad situation actually for Bitlocker, no UI, no cmdlet, no manage-bde.exe… (maybe I could have tried WMI/CIM).

  • Question: how would you refresh group policies when you cannot use gpupdate.exe
  • Solution:

I can still type some PowerShell in the opened console but bitlocker cmdlets don’t work.

It appears that there are 2 super hidden scheduled tasks responsible for refreshing group policies in the background.

Yes, super hidden because you cannot see them in the UI as an administrator even though you’ve enabled the “show hidden tasks” option:

Fortunately, the cmdlets of the ScheduledTasks module can interact with these super hidden tasks 🙂

Answer:

 Get-ScheduledTask -TaskPath '\Microsoft\Windows\GroupPolicy\' |
Where-Object { $_.Actions.Arguments -match 'computer' } |
Start-ScheduledTask
  • Conclusion

The above one-line code allowed me to run gpupdate.exe and saved me from having to restart the computer. Happy days 😎

Get CISA vulnerabilities report

There’s a new initiative from the US CyberSecurity & Infrastructure Security Agency.

They publish a list of known exploited vulnerabilities. Nice, isn’t it?

They publish a json version of the catalog. So I wanted a PowerShell function able to get the list of recently added vulnerabilities, the same way it’s presented in this news article from bleepingcomputer.com or this one.

Let me introduce

Get-Help Get-CISAVulnerabilitiesReport
Get-CISAVulnerabilitiesReport | Measure-Object
Get-CISAVulnerabilitiesReport -Last 3
Get-CISAVulnerabilitiesReport -StartDate (Get-Date).AddDays(-15) | ogv

Here’s the full code of the function, enjoy 🙂

Function Get-CISAVulnerabilitiesReport {
<#
.SYNOPSIS
Get known exploited vulnerabilities
.DESCRIPTION
Get the known exploited vulnerabilities catalog from CISA
.PARAMETER StartDate
Datetime object used to filter the catalog
.PARAMETER Last
Last number of entries in the catalog sorted by published date
.EXAMPLE
Get-CISAVulnerabilitiesReport
Get all the known exploited vulnerabilities from the catalog published by CISA
.EXAMPLE
Get-CISAVulnerabilitiesReport | Measure-Object
Get the count of all the known exploited vulnerabilities published in the catalog by CISA
.EXAMPLE
Get-CISAVulnerabilitiesReport -Last 3
Get the 3 most recent known exploited vulnerabilities from the catalog published by CISA
.EXAMPLE
Get-CISAVulnerabilitiesReport -StartDate (Get-Date).AddDays(-15)
Get the known exploited vulnerabilities from the catalog published by CISA over the last 15 days
#>
[CmdletBinding(DefaultParameterSetName='__AllParameterSets')]
Param(
[Parameter(ParameterSetName = 'ByDate')]
[datetime]$StartDate,
[Parameter(ParameterSetName = 'ByLast')]
[int32]$Last
)
Begin {}
Process {
$HT = @{
URI = 'https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json'
ErrorAction = 'Stop'
UseBasicParsing = [switch]::Present
}
try {
$vuln = (Invoke-RestMethod @HT).vulnerabilities |
ForEach-Object Process {
[PSCustomObject]@{
CVEId = $_.cveID
Vendor = $_.vendorProject
ProductName = $_.product
Name = $_.vulnerabilityName
StartDate = ([datetime]$_.dateAdded)
Description = $_.shortDescription
ActionRequired = $_.requiredAction
DueDate = ([datetime]$_.dueDate)
}
}
} catch {
Write-Warning Message "Failed to get data from CISA because $($_.Exception.Message)"
}
if ($vuln) {
Switch ($PSCmdlet.ParameterSetName) {
'ByDate' {
$vuln | Where-Object { $_.StartDate -gt $StartDate }
break
}
'ByLast' {
$vuln | Sort-Object Property StartDate Descending | Select-Object First $Last
break
}
default {
$vuln
}
}
}
}
End {}
}

Default ParameterSetName

  • Context:

Let’s say you use an advanced feature – ParameterSetName – in your PowerShell code.

Let’s say you don’t specify a default ParameterSetName in the CmdletBinding and that some parameters don’t have an explicitly defined ParameterSetName.

What’s the name of the parameterset in this case?

  • Hands-on!

Here’s a sample code I propose to discover it.

Function Test-Param {
[CmdletBinding(DefaultParameterSetName='Set1')]
Param(
[Parameter(ParameterSetName = 'Set1')]
[switch]$Param1,
[Parameter(ParameterSetName = 'Set2')]
[switch]$Param2,
[switch]$Common
)
Begin {}
Process {
    Switch ($PSCmdlet.ParameterSetName) {
        'Set1' {
            $PSCmdlet.ParameterSetName
            break
        }
        'Set2' {
            $PSCmdlet.ParameterSetName
            break
        }
        default {
            $PSCmdlet.ParameterSetName
        }
    }
}
End {}
}

Let’s execute some code and see what we can uncover:

# Execute the function to see that it works
Test-Param

# Check the DefaultParameterSetName specified in the CmdletBinding
(gcm Test-Param).DefaultParameterSet

# Get the properties of the first parameter of the function
(gcm Test-Param).Parameters['Param1']

As you know gcm is the alias for Get-Command.

Everything looks good and is expected so far.
Now, let’s have a look at the 3rd parameter that doesn’t have any ParameterSetName defined

(gcm Test-Param).Parameters['Common']

Got it. It seems that when there’s no ParameterSetName defined, its name is: __AllParameterSets

Let’s say, I change the above function and omit the DefaultParameterSetName in the CmdletBinding:

At runtime, there’s an error thrown saying that the parameterSet is ambiguous.
Get-Command is still able to see the syntax although the function will fail at runtime.

Let’s use the default parameter name __AllParameterSets instead of Set1 and compare the syntax of the functions

Set1 syntax:

Set1

__AllParameterSets syntax:

__AllParameterSets
  • Conclusion

Using the default parameter name __AllParameterSets gives us a 3rd way to execute with the Common parameter alone. That parameter is valid and used as well by the 2 other ParameterSetNames I specified in the Param block.

Nice and subtle. PowerShell rock 😎

How to fix cve-2021-43890

Microsoft recently published the following vulnerability cve-2021-43890 that is currently exploited by malware like Emotet/Trickbot/Bazaloader.

If your computer doesn’t have access to the store, it may not be that straightforward to install the fixed universal app to all users of a Windows 10 computer.

If the computer is not vulnerable, it’ll tell you the above message.

If it installed the required patched universal app, it’ll say “Successfully provisionned Microsoft.DesktopAppInstaller”.

You can run the code in a scheduled tasked running under the System account. Any user that has an interactive session opened will get the new Appx in his account.

If there’s a local user profile but the user is not logged on, it’ll automatically get the updated appx after an interactive logon.

#Requires -RunAsAdministrator
[CmdletBinding()]
Param()
Begin {}
Process {
if ([version]'1.16.13405.0' -gt [version](Get-AppxPackage Name 'Microsoft.DesktopAppInstaller' ErrorAction SilentlyContinue).Version) {
$zip = (Join-Path Path $env:TEMP ChildPath 'Microsoft.DesktopAppInstaller_1.16.13405.0_8wekyb3d8bbwe.zip')
$zipFolder = "$($zip -replace '\.zip','')"
if (-not(Test-Path Path $zip)) {
$HT = @{
Uri = 'https://download.microsoft.com/download/6/6/8/6680c5b1-3fbe-4b70-8189-90ea08609563/Microsoft.DesktopAppInstaller_1.16.13405.0_8wekyb3d8bbwe.zip'
UseBasicParsing = $true
ErrorAction = 'Stop'
OutFile = $zip
}
try {
Invoke-WebRequest @HT
} catch {
Write-Warning Message "Failed to download zip because $($_.Exception.Message)"
}
}
if (Test-Path Path $zip) {
if ((Get-FileHash Path $zip).Hash -eq 'e79cea914ba04b953cdeab38489b3190fcc88e566a43696aaefc0eddba1af6ab' ) {
try {
Expand-Archive Path $zip DestinationPath (Split-Path $zipFolder Parent) Force ErrorAction Stop
} catch {
Write-Warning Message "Failed to unzip because $($_.Exception.Message)"
}
if ('Valid' -in (Get-ChildItem Path "$($zipFolder)\*" Include * Recurse Exclude '*.xml' | Get-AuthenticodeSignature |
Select-Object ExpandProperty Status | Sort-Object Unique)
) {
$HT = @{
Online = $true
PackagePath = Join-Path Path $zipFolder ChildPath 'Microsoft.DesktopAppInstaller_1.16.13405.0_8wekyb3d8bbwe.msixbundle'
SkipLicense = $true
ErrorAction = 'Stop'
}
try {
$r = Add-AppxProvisionedPackage @HT
if ($r.Online) {
Write-Verbose 'Successfully provisionned Microsoft.DesktopAppInstaller' Verbose
}
} catch {
Write-Warning Message "Failed to install Appx because $($_.Exception.Message)"
}
}
} else {
Write-Warning Message "Downloaded zip file thumbprint (SHA256) doesn't match"
}
} else {
Write-Warning Message "Zip file $($zip) not found"
}
} else {
Write-Verbose Message 'Current Microsoft.DesktopAppInstaller appx version is not vulnerable' Verbose
}
}
End {}

Update of Windows Defender Attack Surface Reduction (ASR) Rules module

Following the announcement of a new rule in this blog post about blocking vulnerable drivers, I’ve added it to the module 🙂

Get-ASRRuleConfig | Select Name,Action | ft -AutoSize
Get-ASRRuleData | ogv -PassThru | fl *
Get-ASRRuleData -Name 'Block abuse of exploited vulnerable signed drivers' | Get-ASRRuleConfig
Get-ASRRuleData -Name 'Block abuse of exploited vulnerable signed drivers' | Set-ASRRuleConfig -Mode AuditMode -WhatIf 
Get-ASRRuleData -Name 'Block abuse of exploited vulnerable signed drivers' | Set-ASRRuleConfig -Mode AuditMode -Verbose
Get-ASRRuleData -Name 'Block abuse of exploited vulnerable signed drivers' | Get-ASRRuleConfig     

That’all folks, have fun 😎

Install Windows 11 in a Hyper-V VM

Context

I wanted to test the new Windows 11 released on Hyper-V

The readiness script reported that both storage and TPM are missing 😥

Here’s what I did to get a PASS on all the hardware prerequisites on the VM:

~\Downloads\HardwareReadiness.ps1|Out-String | ConvertFrom-Json

Solution

$VMName = 'MyVMName'

# 1. Add Secure boot
Get-VM -Name $VMName | 
Set-VMFirmware -EnableSecureBoot:On -SecureBootTemplate 'MicrosoftWindows'

# 2. Add a TPM
Set-VMKeyProtector -VMName $VMName -NewLocalKeyProtector
Enable-VMTPM -VMName $VMName

# 3. Resize VHDX
Get-VMHardDiskDrive -VMName $VMName |
Select -First 1 -ExpandProperty Path | Get-VHD | 
Resize-VHD -SizeBytes 65GB

Inside the VM, I had to extend the C: drive

# Remove the offending 5th partition
$null = Remove-Partition -DiskNumber 0 -PartitionNumber 5 -Confirm:$false

# Resize C: to the maximum
Resize-Partition -DiskNumber 0 -PartitionNumber 4 -Size (Get-PartitionSupportedSize -DiskNumber 0 -PartitionNumber 4).SizeMax

A return code of 0 means the VM is Windows 11 capable 😎

About CVE-2021-40444

Microsoft has recently published a security bulletin about the Microsoft MSHTML Remote Code Execution Vulnerability https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-40444
Others urge anybody to apply the only workaround against this 0day because it has been publicly disclosed.

How would you do that by Group Policy on a domain joined device.

#Requires -RunasAdministrator
#Requires -Modules ActiveDirectory,GroupPolicy
[CmdletBinding()]
Param()
Begin{}
Process {
# Make sure we can reach the PDC
$PDC = (Get-ADDomainController Service 1 Discover ErrorAction SilentlyContinue).Hostname
if ($PDC) {
# Get the domain name
$DomainName = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name
# Create the GPO
try {
$GPO = New-GPO Name 'Workaround for CVE-2021-40444' Domain "$($DomainName)" ErrorAction Stop
} catch {
Write-Warning Message "Failed to create GPO because $($_.Exception.Message)"
}
if ($GPO) {
# Don't need user settings
$GPO.GpoStatus = [Microsoft.GroupPolicy.GpoStatus]::UserSettingsDisabled
$HT = @{ GUID = ($GPO).Id ; ErrorAction = 'Stop' }
# Zones
0..3 |
ForEach-Object {
$ZoneId = $_
1, # URLACTION_DOWNLOAD_SIGNED_ACTIVEX (0x1001)
4 | # URLACTION_DOWNLOAD_UNSIGNED_ACTIVEX (0x1004)
ForEach-Object {
$Value = $_
$reg = @{
Key = 'HKLM\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\{0}' -f $ZoneId
ValueName = '100{0}' -f $Value
Type = 'DWORD'
Value = 3
}
try {
Set-GPRegistryValue @HT @reg
} catch {
Write-Warning Message "Faile to set GPO setting because $($_.Exception.Message)"
}
}
}
}
}
}
End{}

If you look in the GPMC snap-in it looks like this:

Next steps are:
– you may need to add a filter if you want
– you may want to change the permissions, delegation…if required in your environement
– you need to link it on a OU or at another level so that the GPO actually applies to vulnerable computers
– wait for Microsoft. They will release a patch as soon as it’s ready