Quick post: Delete a WSUS update

  • Context

I started working on the KB4577586, the update responsible for the removal of Adobe Flash Player.

I’ve encountered two issues.

  • Issue

First issue, I imported the update for Windows 10 2OH2 x64 using the Id of Server 2019, D’oh!

Second issue, when I first imported my files and their Id, it failed with this message:
Exception calling “ImportUpdateFromCatalogSite” with “2” argument(s): “The underlying connection was closed: An unexpected error occurred on a send.”

Solution

Let’s start with the 2nd issue: the fact that I cannot import an update.
It appears that although I’ve set the correct Protocol to use

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

The ImportUpdateFromCatalogSite method is a .Net method and .NET requires to StrongCrypto value to be set in the registry.

My 2nd issue was solved by

$HT = @{
 Name = 'SchUseStrongCrypto'
 Value = '1'
 Type = 'DWord'
}
$null,'Wow6432Node' | 
Foreach-Object {
 Set-ItemProperty @HT -Path "HKLM:\SOFTWARE$($_)\Microsoft\.NetFramework\v4.0.30319" 
}
Restart-Computer

Let’s get back to my first issue. I got the correct file but not the Id.

# Identify the offending update (no Out-GridView available)
(Get-WsusServer).SearchUpdates('4577586') | Select Title
# Find its Id
(Get-WsusServer).SearchUpdates('4577586') | Select-Object -First 3 | 
Select -Last 1 -ExpandProperty Id
# Detete (no output if ok)
(Get-WsusServer).DeleteUpdate('20bd2d6b-26a9-4ddd-8a3f-04a79b683c1f')

I could reimport the same file using the correct Id, happy days ๐Ÿ˜Ž

MSRC API and MsrcSecurityUpdates module improvements

Great news, the PowerShell MsrcSecurityUpdates has been updated to version 1.9.0 (available on the PSGallery) and doesn’t have any API key requirement anymore ๐Ÿ˜Ž

Here’s the latest blog post from the MSRC about this API key requirement removal and more

While the API key removal is a very nice move, it’s also somehow the tip of the iceberg ๐Ÿ™‚

I’d like to highlight the recent changes made in the code that appear in its GitHub repository.

Change desscription: Related commit:
Adding the info about who issues the CVE (CNA info) 5c8dd29a86fce3146986acdff4f234ca6bfed531
Adding known issues column and info b5876b0aaa22c9f40ea904320ee533a2aed612bd
Forcing any https call to use TLS 1.2 47557e9a2f34b7938d346a932ef37cb7c78895a1
Fixing some CSS issues about a timeout and changing some colors 555acbe8a437ab76dbc91fdb26254aa616091467
Adding more info in error messages (it was discussed here in issue 73) 598755821c8d4473ef54a13c41bd4cc523ebe19a
Restoring Summary (makes sense in Advisories for example) and sort revisions 2114e895900f2d9de0f1f6ea59279cb615e69a0d
Adding a tag in the html commented lines about the version of the MsrcSecurityUpdates module used to generate the html pages 87ba10278b0cbf03921c21e25b881c4de5128acf
Adding a switch to avoid displaying a progress bar 34b528dd954f907c6c7d29217270df72bd041b4d
Yes, we try to fix PSScriptAnalyzer issues reported fdb79f03733dccd4112111516bc92ec0910af26a


If you encounter issues about the module, they can be reported here

Because of the CSS timeout issue, I’ve downloaded all the bulletin data I store in this separate repository.

How to notify logged-on users

You may have already seen or know how to display a nice pop-up toast notification to your Windows 10 users.
If not, I’d like to point out some material related to these notifications:

Now, that you’re familiar with the notifications part, let’s see how to display them.
Being a big fan of what’s robust and sustainable, I’ll show you how to leverage the built-in Windows 10 scheduled tasks to notify your users ๐Ÿ˜€

Let’s say you have a PowerShell script that triggers a notification in the end-user context. It’s named toast.ps1.
Let’s say you’ve a Group Policy (GPO) that runs a scheduled task under the NT AUTHORITY\System account.

How can you escape the SYSTEM account and run the Toast.ps1 in every interactively logged on standard account?

Well, the solution is to identify these interactively logged on users and run a new scheduled task that targets their identity.

# Identify interactive users
try {
 $Users = @(Get-WmiObject -Query 'Select *  from Win32_Process' -ErrorAction Stop) |
 Where-Object { $_.ProcessName -eq 'explorer.exe'} |
 ForEach-Object {
  [PSCustomObject]@{
   User = '{0}\{1}' -f $($_.GetOwner().Domain),$($_.GetOwner().User)
  }
 }
} catch {
 Write-Warning -Message "Failed to get logged on users because $($_.Exception.Message)"
}
# If any and hour is 10AM for example, notify
if ($Users -and ((Get-Date).ToString('HH',[Globalization.CultureInfo]::InvariantCulture) -in '10')) {
 $Users | ForEach-Object {
 Start-UserNotification -UserName $_ -Prefix 'WhatEver' -FilePath 'C:\Toast.ps1'
 }
}

Here’s the code sample of the Start-UserNotification function used above:

#Requires -RunAsAdministrator
Function Start-UserNotification {
<#
.SYNOPSIS
Start a script in the user context
.DESCRIPTION
Start a script in the user context using scheduled tasks
.PARAMETER UserName
String that represents the targeted user name
.PARAMETER FilePath
String that represents the path to the script to be executed
.PARAMETER Prefix
String that is being used to prefix the task name
.EXAMPLE
Start-UserNotification -UserName LocalUserName -FilePath C:\notif.ps1
#>
[CmdletBinding(SupportsShouldProcess)]
[OutputType('System.Boolean')]
Param(
[Parameter(Mandatory)]
[string]$UserName,
[Parameter(Mandatory)]
[ValidateScript({Test-Path -Path $_ -PathType Leaf})]
[string]$FilePath,
[Parameter()]
[string]$Prefix='__'
)
Begin {
$errHT = @{ ErrorAction = 'Stop' }
$cmd = '& {0}' -f [Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($FilePath)
$aHT = @{
Execute = 'C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe'
Argument = '-NoProfile -WindowStyle Hidden -Exec Bypass -Command "{0}"' -f "$($cmd)"
}
}
Process {
}
End {
# Notify user using a Scheduled task
try {
$taskName = '{0}-Notify' -f [regex]::Escape($Prefix)
Get-ScheduledTask -TaskPath '\' @errHT | Where-Object { $_.TaskName -match "^$($taskName)_" } | ForEach-Object {
if ($pscmdlet.ShouldProcess("$($_.TaskName)", 'Remove previous task')) {
$null = $_ | Stop-ScheduledTask -Verbose @errHT
$null = $_ | Unregister-ScheduledTask -Verbose -Confirm:$false @errHT
Write-Verbose -Message "Successfully remove task $($_.TaskName)"
}
}
} catch {
Write-Warning -Message "Failed to stop and unregister because $($_.Exception.Message)"
}
try {
$HT = @{
TaskName = '{0}_{1}' -f "$($taskName)","$($UserName)"
User = '{0}' -f "$($UserName)"
Force = [switch]::Present
Action = (New-ScheduledTaskAction @aHT @errHT)
Settings = (New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -Compatibility 'Win8' @errHT)
}
if ($pscmdlet.ShouldProcess("$($HT['TaskName'])", 'Create and run task')) {
$null = Register-ScheduledTask @HT @errHT | Start-ScheduledTask @errHT
Write-Verbose -Message "Successfully created and ran task $($HT['TaskName'])"
$true
}
} catch {
Write-Warning -Message "Failed to register and run notification task because $($_.Exception.Message)"
$false
}
}
}

Here’s how to use the function:

End of Life of Flash?

  • Context

Since Adobe will no longer be supporting Flash Player after December 31, 2020 and Adobe will block Flash content from running in Flash Player beginning January 12, 2021, Adobe strongly recommends all users immediately uninstall Flash Player to help protect their systems.

Source: https://www.adobe.com/products/flashplayer/end-of-life.html

This statement is worrying because it suggests that Adobe has some sort of kill switch.

It appears that they have hardcoded the EoL (End-Of-Life) date in the code. If you change the date of your computer before January 12, 2021, you’ll notice a different behavior and won’t get the above blue EoL logo.

Instead you can get for example, just a reminder (IE11 in my case):

  • Issue

You may still have a critical Line of Business (LoB) application that requires Flash to run in a browser.

What can you do after the EoL date?

If you don’t rely on Flash at all, instead of waiting for it to be removed by Microsoft, you can for example implement what I described in this post to avoid Flash from being instantiated at all in Windows and Office. (This post doesn’t include what you’d need to do for Google Chrome or Edge Chromium).

  • Solution

Let’s you have to run Flash for a critical LoB application after January 12, 2021. What can be done?

Well, it depends on the browser.

Let’s say you want it to run in Google Chrome or Edge Chromium.

Both Chromium based browsers (Edge and Google) implement the Chromium based Flash component named PepperFlash.

First, you’ll need to get rid of what’s installed under:

$($env:LOCALAPPDATA)\Google\Chrome\User Data\PepperFlash
$($env:LOCALAPPDATA)\Microsoft\Edge\User Data\PepperFlash

Yes, it’s a per-user non-roaming location ๐Ÿ˜ฆ
Once, you did the above step, you don’t have any Flash at all in these Chromium based browsers.

To be able to restore Flash, you need to get an older vulnerable version of the PPAPI provided by Adobe and install it.
Once installed, you’ll have for example a pepflashplayer64_32_0_0_371.dll file in C:\Windows\system32\Macromed\Flash.
Don’t ask me what I think about having 2 vulnerable dll files planted in System32 ๐Ÿ™„ .

NB: The following registry keys store the version and location of the Adobe PPAPI component: HKLM\SOFTWARE\Macromedia\FlashPlayerPepper and HKLM\SOFTWARE\WOW6432Node\Macromedia\FlashPlayerPepper

To make Google Chrome or Edge Chromium load it, you’ll have to create a folder in these per-user locations

mkdir "$($env:LOCALAPPDATA)\Google\Chrome\User Data\Default\Pepper Data\Shockwave Flash\System"
mkdir "$($env:LOCALAPPDATA)\Microsoft\Edge\User Data\Default\Pepper Data\Shockwave Flash\System"

You’ll also need to create files there, named mms.cfg, that contain some instructions:

'System32','SysWOW64' |
Foreach-Object {
 $p = $_
 try {
  @'
AutoUpdateDisable=1
SilentAutoUpdateEnable=0
EOLUninstallDisable=1
EnableAllowList=1
AllowListUrlPattern=http://my-LOB-server
'@ | 
  Out-File -FilePath "C:\Windows\$($p)\Macromed\Flash\mms.cfg" -Encoding UTF8 -Force -ErrorAction Stop
 } catch {
  Write-Warning -Message "Failed to write C:\Windows\$($p)\Macromed\Flash\mms.cfg because $($_.Exception.Message)"
 }
}

NB 1: Yes, I’ve created the mms.cfg in C:\Windows\System32\Macromed\Flash.
It just needs to be copied to the “LOCALAPPDATA..\Shockwave Flash\System” folders created previously.

NB 2:
The Adobeยฎ Flashยฎ Player 32.0 Administration Guide (uploaded here as well, if it disappears any time soon) says the following:

EnableAllowList. Disabled by default.
Allows system administrators to allow Flash Player to only load content from a set of allowed URLs.
After Flash Player EOL, EnableAllowList will default to true and the MMS.CFG setting will be ignored.

Since you rolled out an older version, you need this setting to be enabled ๐Ÿ™‚ .

You may also want to have a tighter control on Flash in these Chromium based browsers using Group Policy that block or allow an list of URL if you don’t trust that much text files (mms.cfg, especially in the user profile as it can be modified).
Here are 2 examples:

Let’s say you want Flash to run in Internet Explorer 11.
You’ve already created the 2 mms.cfg files in C:\Windows\System32\Macromed\Flash and C:\Windows\SysWOW64\Macromed\Flash.

There are two security updates that would need to be removed:
4580325, Security Update for Adobe Flash Player: October 13, 2020
4561600, Security Update for Adobe Flash Player: June 9, 2020
๐Ÿ˜ฅ
Yes, that’s worse! Now, all binaries under C:\Windows\System32\Macromed\Flash are vulnerable and can potentially be loaded in a browser ๐Ÿ˜ฅ

Scoring Applocker rules

To be able to audit Applocker rules created or hunt for human error, I wanted to have a simple way to score Applocker rules.
Are they good or evil? Are they tuned and specific enough to allow or block something?
Is there any rule that is more permissive or not well designed due to a human error?

  • Issue

My Google-fu didn’t find anything relevant to help me score Applocker rules ๐Ÿ˜ฆ

  • Solution

How are rules scored?
I’ve tried to score various aspects of the rule, based on the type (Publisher, Path or Hash), the identity targeted, the action (Allow or Deny) and the conditions.
The code aims at finding and identifying some rules that are more permissive than others, so that it’s easier to spot them and pay attention to these rules.
All the functions scoring rules follow a simple principle. The higher score, the more dangerous/permissive the rule is actually.
There’s a limitation, the code doesn’t evaluate at all the Exceptions and worse, having conditions could have a negative impact on the score (lowering it).
The other limitation is that the code assumes that there isn’t an administrator introducing malicious rules.

How does it perform? Show me examples !

Import-Module -Name Applocker -SkipEditionCheck
Import-Module ~/Documents/ApplockerScore.psm1 -Verbose
Get-ApplockerPolicyRuleScore -Type Script -Rule '(Default Rule) All scripts'


You can see that there’s tab completion for the parameter. You just need to begin by the Type to be able to display the rule names on the 2nd parameter using tab completion.
The default rule that allows anything for an admin is score over 1000. It means it’s too permissive. It’s a Bypass rule for anything (a wildcard) for the members of the local Administrators group. It’s not the highest score you can get.

cd\
Import-Module -Name Applocker -SkipEditionCheck
Import-Module ~/Documents/ApplockerScore.psm1
Get-Help Get-ApplockerPolicyRuleScore -Full
$rule = Get-ApplockerPolicyRuleScore -Type Script -Rule '(Default Rule) All scripts' -Verbose
 Write-Verbose -Message "Final score for '$($rule.Name)' is $($rule.Score)" -Verbose
$rule.Object
$rule.Object | gm


It shows how the score is calculated and what was evaluated to score the rule.
There’s an embedded object property that is its XML definition converted back to its original object type.

Besides simple examples, can it be used against exported Applocker policies to XML files?

Yes, it can. You just need to use the internal private functions and load them.
Here’s a practical example on Ultimate AppLocker ByPass List from Oddvar Moe

Let’s say, you’ve downloaded his Github repo as a zip file, extracted it and loaded the internal functions.
You can now do the following:

Get-ChildItem -Path '~\Downloads\UltimateAppLockerByPassList-master\UltimateAppLockerByPassList-master\AppLocker-BlockPolicies\*.xml' |
ForEach-Object {
([xml](Get-Content -Path $_.FullName -ReadCount 0)).AppLockerPolicy.RuleCollection.ChildNodes |
Out-GridView -PassThru |
ForEach-Object {
$TypeScore = Get-ApplockerRuleTypeScore $_.OuterXML -Verbose
$ActionSore = Get-ApplockerRuleActionScore $_.OuterXML -Verbose
$IdScore = Get-ApplockerRuleUserOrGroupSidScore $_.OuterXML -Verbose
$ConditionScore = Get-ApplockerRuleConditionsScore $_.OuterXML -Verbose
[PSCustomObject]@{
Id = $_.Id
Name = $_.Name
Score = ($TypeScore+$IdScore+$ConditionScore)*$ActionSore
XML = $_.OuterXML
}
}
}

Here’s an example on the rule named ‘Signed by Skype’:

Here’s a 2nd example on the rule about MSHTA:

Where’s the code?

It’s stored in a psm1 file uploaded as gist on GitHub.com:

#Requires -Version 3.0
#region Score the identity
Function Get-ApplockerRuleUserOrGroupSidScore {
[CmdletBinding()]
[OutputType([int32])]
Param(
[Parameter(Mandatory)]
[Alias('UserOrGroupSid','SID')]
$InputObject
)
Begin {}
Process {
$InputObject |
ForEach-Object {
Switch -Regex ($_) {
'S-1-1-0' { # EveryOne
Write-Verbose -Message 'Applocker score: 50 ; Everyone'
50 ; break
}
'S-1-5-32-544' { # Administrators
Write-Verbose -Message 'Applocker score: 10 ; Administrators'
10 ; break
}
'S-1-5-32-545' { # Users
Write-Verbose -Message 'Applocker score: 10 ; Users'
10 ; break
}
# S-1-5-11 # Authenticated Users
# S-1-5-32-546 # Guests
# S-1-5-32-547 # Power Users
default {
Write-Verbose -Message 'Applocker score: Id is specific'
10
}
}
}
}
End {}
}
#endregion
#region Score the rule type
Function Get-ApplockerRuleTypeScore {
[CmdletBinding()]
[OutputType([int32])]
Param(
[Parameter(Mandatory)]
[string[]]$InputObject
)
Begin {}
Process {
$InputObject |
ForEach-Object {
Switch -Regex ($_) {
'^<FilePath(Rule|Condition)' { Write-Verbose -Message 'Applocker score: 100 ; Path type' ; 100 }
'^<FilePublisher(Rule|Condition)' { Write-Verbose -Message 'Applocker score: 50 ; Path publisher' ; 50}
'^<FileHash(Rule|Condition)' {Write-Verbose -Message 'Applocker score: 50 ; Path Hash' ; 50}
default {}
}
}
}
End {}
}
#endregion
#region Score the rule action
Function Get-ApplockerRuleActionScore {
[CmdletBinding()]
[OutputType([int32])]
Param(
[Parameter(Mandatory)]
[string[]]$InputObject
)
Begin {}
Process {
$InputObject |
ForEach-Object {
Switch -Regex ($_) {
'\sAction="Allow"' {
Write-Verbose -Message 'Applocker score: *1 ; Action is Allow'
1
}
'\sAction="Deny"' {
Write-Verbose -Message 'Applocker score: /10 ; Action is Deny'
0.1
}
default {}
}
}
}
End {}
}
#endregion
#region Score the rule conditions
Function Get-ApplockerRuleConditionsScore {
[CmdletBinding()]
[OutputType([int32])]
Param(
[Parameter(Mandatory)]
[string[]]$InputObject
)
Begin {}
Process {
$InputObject |
ForEach-Object {
($(
Switch -Regex ($_) {
'^<FilePath(Rule|Condition)' {
Write-Verbose -Message 'Applocker score: Path type'
# Depth: count the occurrence of '\'
$depth = 110 - (($_ -split '\\').Count)*10
Write-Verbose -Message "Depth score is $($depth)"
$depth
# Evaluate if there's a wildcard in the path
if ($_ -match '\\\*\\') {
50
}
Switch -Regex ($_) {
# Just a specific extension
# .exe, .com *.msi, msp mst ps1 cmd bat vbs js appx msix dll ocx
'\sPath="\*\.([eE][xX][eE]|[cC][oO][mM]|[mM][sS][iI]|[mM][sS][pP]|[mM][sS][tT]|[pP][sS]1|[cC][mM][dD]|[bB][aA][tT]|[vV][bB][sS]|[jJ][sS])"\s' {
Write-Verbose -Message "Path score: 500 (only a *.ext)"
500
}
# Path letter
'\sPath="(c|C):\\' {
Write-Verbose -Message "Path score: 100 (Path on C: drive)"
100
}
'\sPath="[abABd-zD-Z]:\\' {
Write-Verbose -Message "Path score: 100 (Path not on C: drive)"
50
}
# Extension: just a wildcard
'\sPath="\*\.\*"\s' { # *.*
Write-Verbose -Message "Path score: 1000 (only a *.*)"
1000 ; break
}
'\sPath="\*"\s' { # *
Write-Verbose -Message "Path score: 1000 (only a *)"
1000 ; break
}
default {}
}
}
'^<FilePublisher(Rule|Condition)' {
Write-Verbose -Message 'Applocker score: Path publisher'
Switch -Regex ($_) {
'PublisherName="\*"\sProductName="\*"\sBinaryName="\*"\>\<BinaryVersionRange\sLowSection="0\.0\.0\.0"\sHighSection="\*"' {
Write-Verbose -Message "Publisher score: 1000 (default Appx PublisherName=*)"
1000 ; break
}
'ProductName="\*"\sBinaryName="\*"\>\<BinaryVersionRange\sLowSection="\*"\sHighSection="\*"' {
Write-Verbose -Message "Publisher score: 500 (only a PublisherName)"
500 ; break
}
'\sBinaryName="\*"\>\<BinaryVersionRange\sLowSection="\*" HighSection="\*"' {
Write-Verbose -Message "Publisher score: 500 (PublisherName and ProductName)"
200 ; break
}
'BinaryVersionRange\sLowSection="\*"\sHighSection="\*"' {
Write-Verbose -Message "Publisher score: 100 (PublisherName and ProductName and FileName)"
100 ; break
}
default {
Write-Verbose -Message "Publisher score: 50 (default)"
50
}
}
}
'^<FileHash(Rule|Condition)' {
Write-Verbose -Message "Hash score: 50"
50
}
default {}
}
) | Measure-Object -Sum).Sum
}
}
End {}
}
#endregion
#region Convert to Object from Xml
Function Get-ApplockerRuleObjectFromXml {
[CmdletBinding()]
[OutputType([int32])]
Param(
[Parameter(Mandatory)]
[string[]]$InputObject
)
Begin {}
Process {
$InputObject |
ForEach-Object {
$o = $_
Switch -Regex ($_) {
'^<FilePathRule' {
try {
[Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.FilePathRule]::FromXml($_)
} catch {
Write-Warning -Message "Failed to translate $($o) to Applocker FilePathRule because $($_.Exception.Message)"
}
break
}
'^<FilePublisherRule' {
try {
[Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.FilePublisherRule]::FromXml($_)
} catch {
Write-Warning -Message "Failed to translate $($o) to Applocker FilePublisherRule because $($_.Exception.Message)"
}
break
}
'^<FileHashRule' {
try {
[Microsoft.Security.ApplicationId.PolicyManagement.PolicyModel.FileHashRule]::FromXml($_)
} catch {
Write-Warning -Message "Failed to translate $($o) to Applocker FilePublisherRule because $($_.Exception.Message)"
}
break
}
default {
Write-Warning -Message 'Failed to transtale input into an Applocker rule'
}
}
}
}
End {}
}
#endregion
Function Get-ApplockerPolicyRuleScore {
<#
.SYNOPSIS
Score an Applocker policy rule
.DESCRIPTION
Score an Applocker policy rule
.PARAMETER Type
Parameter that indicates the RuleCollectionType to find a rule
.PARAMETER Rule
Dynamic parameter that indicates the Name of the rule to score
.EXAMPLE
Get-ApplockerPolicyRuleScore -Type Script -Rule '(Default Rule) All scripts'
Id Name Score XML
-- ---- ----- ---
ed97d0cb-15ff-430f-b82c-8d7832957725 (Default Rule) All scripts 1210 <FilePathRule Id
.EXAMPLE
$rule = Get-ApplockerPolicyRuleScore -Type Script -Rule '(Default Rule) All scripts' -Verbose
Write-Verbose -Message "Final score for '$($rule.Name)' is $($rule.Score)" -Verbose
$rule.Object
VERBOSE: Dealing with Rule Collection type: Script
VERBOSE: Dealing with Rule Name: (Default Rule) All scripts
VERBOSE: Applocker score: 100 ; Path type
VERBOSE: Applocker score: *1 ; Action is Allow
VERBOSE: Applocker score: 10 ; Administrators
VERBOSE: Applocker score: Path type
VERBOSE: Depth score is 100
VERBOSE: Path score: 1000 (only a *)
VERBOSE: Final score for '(Default Rule) All scripts' is 1210
PathConditions : {*}
PathExceptions : {}
PublisherExceptions : {}
HashExceptions : {}
Id : ed97d0cb-15ff-430f-b82c-8d7832957725
Name : (Default Rule) All scripts
Description : Allows members of the local Administrators group to run all scripts.
UserOrGroupSid : S-1-5-32-544
Action : Allow
#>
[CmdletBinding()]
Param(
[ValidateSet('Exe','Script','Msi','Appx','Dll')]
[Parameter(Mandatory)]
[String]$Type
)
DynamicParam {
$Dictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary
#region helper function
Function New-ParameterAttributCollection {
[CmdletBinding(SupportsShouldProcess)]
Param(
[Switch]$Mandatory,
[Switch]$ValueFromPipeline,
[Switch]$ValueFromPipelineByPropertyName,
[String]$ParameterSetName,
[Parameter()]
[ValidateSet(
'Arguments','Count','Drive','EnumeratedArguments','Length','NotNull',
'NotNullOrEmpty','Pattern','Range','Script','Set','UserDrive'
)][string]$ValidateType,
[Parameter()]
$ValidationContent
)
Begin {}
Process {
if ($PSCmdlet.ShouldProcess('Create new Attribute')) {
$c = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$a = New-Object System.Management.Automation.ParameterAttribute
if ($Mandatory) {
$a.Mandatory = $true
}
if ($ValueFromPipeline) {
$a.ValueFromPipeline = $true
}
if ($ValueFromPipelineByPropertyName) {
$a.ValueFromPipelineByPropertyName=$true
}
if ($ParameterSetName) {
$a.ParameterSetName = $ParameterSetName
}
$c.Add($a)
if ($ValidateType -and $ValidationContent) {
try {
$c.Add((New-Object "System.Management.Automation.Validate$($ValidateType)Attribute"(
$ValidationContent
)))
} catch {
Throw $_
}
}
$c
}
}
End {}
}
#endregion
try {
$ApplockerPolicyXml = [xml](Get-AppLockerPolicy -Effective -Xml -ErrorAction Stop)
} catch {
Throw 'Failed to read the effective Applocker policy into XML'
}
#region param Rule
$Dictionary.Add(
'Rule',
(New-Object System.Management.Automation.RuntimeDefinedParameter(
'Rule',
[string],
(New-ParameterAttributCollection -Mandatory -ValidateType Set -ValidationContent (
$ApplockerPolicyXml.SelectNodes("/AppLockerPolicy/RuleCollection[@Type='$($PSBoundParameters['Type'])']").ChildNodes| ForEach-Object { $_.Name }
))
))
)
$Dictionary
}
Begin {}
Process {
Write-Verbose -Message "Dealing with Rule Collection type: $($PSBoundParameters['Type'])"
Write-Verbose -Message "Dealing with Rule Name: $($PSBoundParameters['Rule'])"
# Select node
$n = $ApplockerPolicyXml.SelectNodes("/AppLockerPolicy/RuleCollection[@Type='$($PSBoundParameters['Type'])']").ChildNodes |
Where-Object { $_.Name -eq "$($PSBoundParameters['Rule'])" }
try {
$TypeScore = Get-ApplockerRuleTypeScore -InputObject $n.OuterXml
$ActionSore = Get-ApplockerRuleActionScore -InputObject $n.OuterXml
$IdScore = Get-ApplockerRuleUserOrGroupSidScore -InputObject $n.OuterXml
$ConditionScore = Get-ApplockerRuleConditionsScore -InputObject $n.OuterXml
$r = [PSCustomObject]@{
Id = $n.Id
Name = $n.Name
Score = ($TypeScore+$IdScore+$ConditionScore)*$ActionSore
XML = $n.OuterXml
Object = Get-ApplockerRuleObjectFromXml -InputObject $n.OuterXml
}
Update-TypeData -TypeName 'Applocker.Rule.Score' -DefaultDisplayPropertySet 'Id','Name','Score','XML' -Force
$r.PSTypeNames.Insert(0,'Applocker.Rule.Score')
$r
} catch {
Throw "Something went wrong while scoring applocker rule: $($_.Exception.Message)"
}
}
End {}
} # endof Get-ApplockerPolicyRuleScore
Export-ModuleMember -Function 'Get-ApplockerPolicyRuleScore'
view raw ApplockerScore.psm1 hosted with ❤ by GitHub

How to whitelist your AutoRuns artifacts

Have you ever wondered how can I create a whitelist of my known harmless AutoRuns artifacts?

There are various ways to filter and ignore known artifacts. It can be done either for hunting purposes and to spot more quickly what’s unknown that requires a review and (probably) further actions.

You can filter Autoruns artifacts by using the authenticode certificate, the SHA256 hash,…

All these methods will work but there are some known artifacts that are still difficult to filter out.

I’ll show here a way to deal with these artifacts and create a baseline or whitelist of known artifacts.

I’ve introduced a new function in the module to create a whitelist of artifacts stored in a .ps1 file.

You can directly send the output of the main Get-PSAutorun function to the new New-AutoRunsBaseLine so that a .ps1 file is created. This powershell script stores an array of artifcats stored as PSCustomobject.

You may wonder why a .ps1 file and not a json file. The reason is that you can use a code signing certificate to sign the created ps1 file using the built-in Set-AuthenticodeSignature cmdlet. It brings integrity. It’s a signed whitelist. Signing may also be required if you use the PowerShell constrained language mode.

You can for example store all the artifacts with the maximum properties available.

Get-PSAutorun -VerifyDigitalSignature -ShowFileHash|
New-AutoRunsBaseLine -Verbose

Later on, to see if there are any changes, you can use the second function I’ve introduced: Compare-AutoRunsBaseLine

Here’s an example where I duplicated an existing file and modified one of its properties: the ending digit of the version.

For hunting purposes, you may want to store a minimalist whitelist and use it as a filter to view what’s relevant.

Get-PSAutorun -VerifyDigitalSignature|
Where { -not($_.isOSbinary)}|
New-AutoRunsBaseLine

If you want to view what has been exported in the whitelist, you just need to run the script created.

~\Documents\PSAutoRunsBaseLine-20201102214715.ps1 |
Out-GridView -PassThru

Here’s an example where I selected only one difficult artifact to filter out from the grid output:

Here’s another example I use on my personal computer:

Get-PSAutorun -VerifyDigitalSignature -ShowFileHash |
Where {-not($_.Version)} | Where { -not($_.isOSbinary)} |
New-AutoRunsBaseLine -Verbose

If you want to see what that file contains, I’ve uploaded a version here.

If you think that these new functions are useful, please vote for them ๐Ÿ˜€

The above new functions are currently only part of the experimental branch of Autoruns repository on Github. It’s not signed digitally and the Autoruns.cat file has not been modified to reflect the changes of the main Autoruns files (.psd1 and psm1). It’s also not been published yet to the PowerShell Gallery. It all depends on your votes ๐Ÿ˜€ . If you think it’s useful, please let me know. If you think, it’s not, please let also me know. If you find issues with the code, please introduce it directly on Github using this link.

Enjoy and happy huntingย ๐Ÿ˜Ž

Reinstall preinstalled (appx) Apps

  • Context

I’ve discovered recently this link to reinstall-preinstalledApps.zip that was provided on this page

  • Solution

The code is straightforward and probably does the job in most situations.
But, there are some assumptions about the version, admin rights,.. and the code has no error handling. You can see the file here.
I’ve decided to improve it by adding a help, a #require statement, error handling and the ability to use the -WhatIf parameter.

If you use my “slightly improved” version, you can do:

# Show what would be done using the -WhatIf parameter
Start-ReInstallAppx -Filter "*Microsoft.WindowsStore*" -WhatIf

# Do it:
Start-ReInstallAppx -Filter "*Microsoft.WindowsStore*" -Verbose

#Requires -RunAsAdministrator
Function Start-ReInstallAppx {
<#
.SYNOPSIS
Get back all the apps that come default with Windows 10
.DESCRIPTION
Get back all the apps that come default with Windows 10
.EXAMPLE
Start-ReInstallAppx -Filter "*Microsoft.WindowsStore*"
.NOTES
http://go.microsoft.com/fwlink/?LinkId=619547
Adapted from
http://download.microsoft.com/download/5/F/0/5F04003A-035E-4A0F-9662-43E32C546F6C/reinstall-preinstalledApps.zip
#>
[CmdletBinding(SupportsShouldProcess)]
Param(
[Parameter()]
[ValidateNotNullOrEmpty()]
[string]$Filter
)
Begin {
$HT = @{ ErrorAction = 'Stop'}
}
Process {
try {
# Get all the provisioned packages
$Packages = Get-Item -Path 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Appx\AppxAllUserStore\Applications' @HT |
Get-ChildItem @HT
} catch {
Write-Warning -Message "Failed to get packages list from the registry or filesystem because $($_.Exception.Message)"
}
if ($Packages) {
if ($Filter) {
$Packages = $Packages | Where-Object {$_.Name -like "$($Filter)" }
}
$Packages |
ForEach-Object -Process {
$p = $_
$PackageName = $PackagePath = $null
# get package name & path
try {
$d = $p | Get-ItemProperty @HT
if ($d) {
$PackageName = $d | Select-Object -ExpandProperty PSChildName
$PackagePath = [System.Environment]::ExpandEnvironmentVariables(($d| Select-Object -ExpandProperty Path))
}
} catch {
Write-Warning -Message "Failed to get package name and path for package $($p) because $($_.Exception.Message)"
}
# register the package
if ($PackageName -and $PackagePath) {
Write-Verbose -Message "Attempting to register package: $($PackageName)"
if ($PSCmdlet.ShouldProcess("$($PackageName))",'Register Appx package')) {
try {
Add-AppxPackage -Register $PackagePath -DisableDevelopmentMode @HT
Write-Verbose -Message "Successfully registered package from path $($PackagePath)"
} catch {
Write-Warning -Message "Failed to regiter from $($PackagePath) because $($_.Exception.Message)"
}
}
}
}
}
}
End {}
}

#PowerShell at #Microsoft #MSIgnite

The Microsoft Ignite conference took place this year on https://myignite.microsoft.com/home

Here’s some content related to #PowerShell, what else ! (NB: it’s not the only content in this year’s event):

  • Taking your automation to the next level with #PowerShell 7

Please allow me to add a quick note about the presentation and this slide:

WinRM and Remoting have been released in PowerShell 2.0 with the RTM release of Windows 7 and 2008 R2.
It means Remoting is available since August 2009.
PowerShell 3.0 was released in 2012 and shipped in Windows 8 and Server 2012.

What Joey Aiello meant on the slide is that Remoting is enabled by default since Windows Server 2012, released in September 2012.

Source: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_remote_requirements

Windows Server 2012 and newer releases of Windows Server are enabled for PowerShell remoting by default.

Remoting is the killer feature that made me adopt PowerShell in 2009, it’s so cool because it enables remote management of anything in a secure manner ๐Ÿ˜Ž
Remoting is on and rocks since 11 years.

Please let me also add another thought about this presentation.
Windows PowerShell is part of the WMF, a.k.a. the Windows Management Framework that adds these components:

So, my question is: what are the investments made into the above components of the WMF since Microsoft open-sourced PowerShell?

  • PowerShell Unplugged – Challenge Edition

What a mind-blowing kickboxing match of new features ๐Ÿ˜‰ !

History of previous feature updates

  • Context

I’ve discovered that the history of previous versions you had before running a feature update is stored in the registry under HKLM\System\Setup

  • Solution

I wrote a function to read the information under this key:

Function Get-PreviousUpgrade {
<#
.SYNOPSIS
Get info about previous feature updates
.DESCRIPTION
Get info about previous feature updates being used to upgrade Windows 10 to a new branch.
.EXAMPLE
Get-PreviousUpgrade
#>
[CmdletBinding()]
Param()
Begin {}
Process {}
End {
try {
$names = (Get-Item -Path 'HKLM:\SYSTEM\Setup' -ErrorAction Stop).GetSubKeyNames() |
Where-Object { $_ -match '^Source\sOS\s\(Updated\son\s' }
} catch {
Write-Warning -Message "Failed to read the registry key because $($_.Exception.Message)"
}
if ($names) {
$names |
ForEach-Object {
$date = ([regex]'^Source\sOS\s\(Updated\son\s(?<Date>.+)\)').Matches($_) |
Select-Object -ExpandProperty Groups |
Select-Object -Last 1 -ExpandProperty Value
$FromBranch = (Get-ItemProperty -Path (Join-Path -Path 'HKLM:\SYSTEM\Setup' -ChildPath $_) -Name 'ReleaseId').'ReleaseId'
[PSCustomObject]@{
Date = [datetime]$date
Branch = $FromBranch
}
}
} else {
Write-Warning -Message 'No previous upgrades found'
}
}
}

Here’s how to use it ๐Ÿ™‚

Get-PreviousUpgrade | 
Where  { $_.Branch -eq '1809' } | 
Select -Property Date

Friday fun: Windows Feature Update progress

  • Context

I started recently to launch some feature updates of Windows 10 in a scheduled task running as ‘NT AUTHORIY\SYSTEM’ account. It also means that the UI is hidden for whoever is logged on the computer.

I was also looking for some additional info about:
– how to detect that a feature is running
– how to detect that a feature has just finished running
– how to detect that a reboot is pending because of the feature update that just ran

  • Solution

I started investigating using procmon.exe and tracked what’s written under HKLM\System\setup.

I noticed that I can track the progress of a feature update by using a one-liner:

While (ps setup* ) { Write-Progress -Activity Upgrade -PercentComplete "$((gp HKLM:\SYSTEM\Setup\MoSetup\Volatile -Name SetupProgress).SetupProgress)" -Status "at $((gp HKLM:\SYSTEM\Setup\MoSetup\Volatile -Name SetupProgress).SetupProgress)%" }

It appears that a SetupProgress value is written under the registry key HKLM:\SYSTEM\Setup\MoSetup\Volatile
Here’s a more readable splatted version of the one-liner:

While (Get-Process -Name setup*) {
 $k = 'HKLM:\SYSTEM\Setup\MoSetup\Volatile'
 $n = 'SetupProgress'
 $p = Get-ItemProperty -Path $k -Name $n
 $HT = @{
  Activity =  'Upgrade'
  PercentComplete = "$($($p).$n)"
  Status = "at $($($p).$n)%"
 }
 Write-Progress @HT
}