This post uses code from CyberDrain
Click the link below to check out the original post on CyberDrain.com and support Kelvin's fantastic work for the MSP community.
Click the link below to check out the original post on CyberDrain.com and support Kelvin's fantastic work for the MSP community.
Sometimes I get a script idea put in my head that's so irritatingly pervasive that the only fix is to write the damned script. David Szpunar from the NinjaOne Users Discord made a somewhat passing comment about time drift causing issues with a remote support tool and that let to me thinking... You could probably monitor for that with a PowerShell one-liner right?
Wrong! Turns out that it's more than one line!
This script requires user input, whether in the form of variables, parameters or edits to the script itself before you can run it. Areas where you need to provide input will be indicated with:
### Inline Comments
and / or
'<MARKED STRINGS>'
Parameters will be indicated before the script block.
This script was updated after being published, if you're using it please compare the version you have with the version available here.
This script was last updated on 2023/03/17.
# This script will monitor the time drift between the local machine and a reference server.
# The script accepts the following parameters:
## ReferenceServer: The NTP or local domain controller to use as a reference for time drift.
## NumberOfSamples: The number of samples to take.
## AllowedTimeDrift: The allowed time drift in seconds.
# The script will return the following:
## If the time drift is within the allowed time drift, the script will return a message if the -Verbose switch is used.
## If the time drift is greater than the allowed time drift, the script will throw an error.
## If the -Debug switch is used, the script will return various raw data.
# Thanks to David Szpunar from the NinjaOne Users Discord for inspiring this one.
# Thanks to Kevin Holman for the many useful bits of code in his script here: https://kevinholman.com/2017/08/26/monitoring-for-time-drift-in-your-enterprise/
# Thanks to Scott - CO from the One Mand Band MSP Discord for the idea to add a resync option.
# Thanks to Chris Taylor (https://christaylor.codes/) for the suggestion to add `| Where-Object { $_ }` to exclude empty lines from the output.
[CmdletBinding()]
param (
# The NTP or local domain controller to use as a reference for time drift.
[string]$ReferenceServer = 'time.windows.com',
# The number of samples to take.
[int]$NumberOfSamples = 1,
# The allowed time drift in seconds.
[int]$AllowedTimeDrift = 10,
# Force a resync of the time if the time drift is greater than the allowed time drift.
[switch]$ForceResync
)
$Win32TimeExe = Join-Path -Path $ENV:SystemRoot -ChildPath 'System32\w32tm.exe'
$Win32TimeArgs = '/stripchart /computer:{0} /samples:{1} /dataonly' -f $ReferenceServer, $NumberOfSamples
$ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo
$ProcessInfo.FileName = $Win32TimeExe
$ProcessInfo.Arguments = $Win32TimeArgs
$ProcessInfo.RedirectStandardError = $true
$ProcessInfo.RedirectStandardOutput = $true
$ProcessInfo.UseShellExecute = $false
$ProcessInfo.CreateNoWindow = $true
$Process = New-Object System.Diagnostics.Process
$Process.StartInfo = $ProcessInfo
$Process.Start() | Out-Null
$ProcessResult = [PSCustomObject]@{
ExitCode = $Process.ExitCode
StdOut = $Process.StandardOutput.ReadToEnd()
StdErr = $Process.StandardError.ReadToEnd()
}
$Process.WaitForExit()
if ($ProcessResult.StdErr) {
throw "w32tm.exe returned the following error: $($ProcessResult.StdErr)"
} elseif ($ProcessResult.StdOut -contains 'Error') {
throw "w32tm.exe returned the following error: $($ProcessResult.StdOut)"
} else {
Write-Debug ('Raw StdOut: {0}' -f $ProcessResult.StdOut)
$ProcessOutput = $ProcessResult.StdOut.Split("`n") | Where-Object { $_ }
$Skew = $ProcessOutput[-1..($NumberOfSamples * -1)] | ConvertFrom-Csv -Header @('Time', 'Skew') | Select-Object -ExpandProperty Skew
Write-Debug ('Raw Skew: {0}' -f $Skew)
$AverageSkew = $Skew | ForEach-Object { $_ -replace 's', '' } | Measure-Object -Average | Select-Object -ExpandProperty Average
Write-Debug ('Average Skew: {0}' -f $AverageSkew)
if ($AverageSkew -lt 0) { $AverageSkew = $AverageSkew * -1 }
$TimeDriftSeconds = [Math]::Round($AverageSkew, 2)
if ($TimeDriftSeconds -gt $AllowedTimeDrift) {
if ($ForceResync) {
Start-Process -FilePath $Win32TimeExe -ArgumentList '/resync' -Wait
Write-Warning "Time drift was greater than the allowed time drift of $AllowedTimeDrift seconds. Time drift was $TimeDriftSeconds seconds A resync was forced."
} else {
throw "Time drift is greater than the allowed time drift of $AllowedTimeDrift seconds. Time drift is $TimeDriftSeconds seconds."
}
} else {
Write-Verbose "Time drift is within accepted limits. Time drift is $TimeDriftSeconds seconds."
}
}
We tested this as a "Script Result Condition" in NinjaOne set to trigger the monitor if a machine's time drifts by more than 10 seconds from uk.pool.ntp.org
(the UK's NTP pool) and it worked like a charm. The script is pretty self-explanatory but here's a quick rundown of what it does:
-ReferenceServer
)w32tm
executable to conduct a number of skew checks against that reference server (Parameter -NumberOfSamples
)-AllowedTimeDrift
)-ForceResync
)If the average time drift is greater than the threshold, the script returns a non-zero exit code and the monitor triggers. If the w32tm
command errors (non existent server, network down etc) the script returns a non-zero exit code and the monitor triggers.
This script borrows ideas and the approach and a little code from the excellent blog of Kevin Holman.
The formidable Chris Taylor helped with a cool suggestion to suppress empty lines in the output and his site is well worth a visit.
This post will hold detection scripts for any serious CVE vulnerability that we write detection scripts for in the future. It will be updated and added to as new vulnerability detection scripts are written.
This script has been compiled using information from the following Microsoft sources:
This article relates to CVE-2022-41099 which is a vulnerability in the Windows Recovery Environment (WinRE) which could allow a successful attacker to bypass the BitLocker Device Encryption feature on the system storage device. An attacker with physical access to the target could exploit this vulnerability to gain access to encrypted data.
Thanks to DTGBilly from the NinjaOne Users Discord for pointing out that in altogether far too many places I had typo'd the CVE as CVE-4022-41099
instead of CVE-2022-41099
🤦♂️ this included field names and labels so please check yours are correct as now shown in the post.
Since version 1.2.0 (2023-03-21) this script now requires one of two mandatory parameters.
If you are checking for the presence of the small "Safe OS Dynamic Update (SODU)" which is the minimum required change to mitigate the vulnerability use the -CheckPackage
parameter and if required alter the -MountDirectory
and -LogDirectory
parameters (defaults to C:\RMM\WinRE
).
If you are checking for the presence of the larger "Servicing Stack Update (SSU)" or "Dynamic Cumulative Update" which updates more than is required to mitigate the vulnerability, but may offer other benefits including new WinRE functionality or more reliable reset/restore behaviours use the -CheckImage
parameter which checks the image build version.
If you were passing these in NinjaOne your parameter preset might look like this:
-CheckPackage -MountDirectory C:\RMM\WinRE -LogDirectory C:\RMM\WinRE
or this:
-CheckImage
Before version 1.3.0 the script did not check if WinRE was enabled which could lead to confusing error output in the event WinRE was disabled. Now if you get the WinRE not enabled warning you are clear on why the script isn't executing.
A simple reagentc /enable
should enable WinRE or at least provide some useful troubleshooting output.
To create a custom field at the device level in NinjaOne go to Administration > Devices and select either Role Custom Fields or Global Custom Fields then select Add.
Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles.
To create a custom field at the organisation level in NinjaOne go to Administration > Apps > Documentation, enable the Documentation feature if you haven't already. If you have then select Add.
When you create your custom field you need to make sure that you set the "Scripts" permission to ensure that you can read or write to the field from your scripts - as appropriate for the script you're using.
We're adding one role custom field for devices with the Windows Desktop or Laptop role, note that we've customised slightly the autogenerated machine name here, if you use the default adjust the field name in the script appropriately.
Field Label | Field Name | Field Type | Description |
---|---|---|---|
CVE-2022-41099 | CVE202241099 | Checkbox | Whether the device has a WinRE image vulnerable to CVE-2022-41099 |
This script was updated after being published, if you're using it please compare the version you have with the version available here.
This script was last updated on 2023/03/24.
# This script tests whether the Windows Recovery Environment is vulnerable to CVE-2022-41099.
# Thanks to Wisecompany on the One Man Band MSP Discord and NinjaOne Users Discord for prompting me to update this to check for the Safe OS Dynamic Update package.
# Version 1.3.0 - Checks whether WinRE is even enabled before attempting to check for the update/vulnerability. - 2023/03/24
# Version 1.2.0 - Major refactor, now supports checking for the SafeOS Dynamic Update package presence. - 2023/03/21
# Version 1.1.0 - Better logic, more versions supported - 2023-01-17
# Version 1.0.0 - Initial Release - 2023-01-13
[CmdletBinding()]
param (
[Parameter(ParameterSetName = 'Package', Mandatory = $true)]
[Switch]$CheckPackage,
[Parameter(ParameterSetName = 'Image', Mandatory = $true)]
[Switch]$CheckImage,
[Parameter(ParameterSetName = 'Package')]
[System.IO.DirectoryInfo]$MountDirectory = 'C:\RMM\WinRE\Mount',
[Parameter(ParameterSetName = 'Package')]
[System.IO.DirectoryInfo]$LogDirectory = 'C:\RMM\WinRE\Logs'
)
$WinREEnabled = (reagentc /info | findstr 'Enabled').Replace('Windows RE status: ', '').Trim()
if (-not ($WinREEnabled)) {
Write-Warning 'Windows RE is disabled - exiting...'
return $false
}
$WinREImagePath = (reagentc /info | findstr '\\?\GLOBALROOT\device').Replace('Windows RE location: ', '').Trim() + '\winre.wim'
$WinREBuild = (Get-WindowsImage -ImagePath $WinREImagePath -Index 1).SPBuild
# $WinREModified = (Get-WindowsImage -ImagePath $WinREImagePath -Index 1).ModifiedTime
$WinOSBuild = [System.Environment]::OSVersion.Version.Build
$BuildtoKBMap = @{
22623 = 5023527
22621 = 5023527
22000 = 5021040
19045 = 5021043
19044 = 5021043
19043 = 5021043
19042 = 5021043
}
function Mount-WinRE {
if (-not(Test-Path $MountDirectory)) {
New-Item $MountDirectory -ItemType Directory
} else {
$MountDirectoryContents = Get-ChildItem $MountDirectory
if ($MountDirectoryContents) {
Write-Warning "Mount directory isn't empty - exiting..."
return $false
}
}
if ((Get-WindowsImage -Mounted).count -ge 1) {
Write-Warning 'There is at least one other image mounted already = exiting...'
return $false
}
$Mount = ReAgentC.exe /mountre /path $MountDirectory
if ($Mount) {
if ($Mount[0] -notmatch '.*\d+.*' -and (Get-WindowsImage -Mounted).count -ge 1 -and $LASTEXITCODE -eq 0) {
return $true
}
} else {
Write-Warning 'Could not mount WinRE image.'
Write-Warning "$Mount"
return $false
}
}
function Dismount-WinRE {
$DismountImageLogFile = Join-Path -Path $LogDirectory -ChildPath ('Dismount-WindowsImage_{0}.log' -f $DateTime)
$DismountWinRECommonParameters = @{
Path = $MountDirectory
LogLevel = 'WarningsInfo'
}
$UnmountDiscard = ReAgentC.exe /unmountre /path $($MountDirectory) /discard
if (($UnmountDiscard[0] -match '.*\d+.*') -or $LASTEXITCODE -ne 0) {
Write-Warning 'Attempting to unmount and discard failed - trying alternative method'
Dismount-WindowsImage @DismountWinRECommonParameters -LogPath $DismountImageLogFile -Discard
if ($(Get-WindowsImage -Mounted).count -ge 1) {
Write-Warning 'Unmounting failed, including alternative methods.'
return $false
} else {
return $true
}
} else {
return $true
}
}
if ($CheckPackage) {
if (-not (Mount-WinRE)) {
Write-Warning 'Could not mount WinRE image - exiting...'
exit 1
}
$KB = ('KB{0}' -f $BuildtoKBMap[$WinOSBuild])
$PackageApplied = (Get-WindowsPackage -Path $MountDirectory | Where-Object { $_.PackageName -like "*$KB*" }).PackageState -eq 'Installed'
if (-not (Dismount-WinRE)) {
Write-Warning 'Could not dismount WinRE image - exiting...'
exit 1
}
if (-not ($PackageApplied)) {
Write-Warning 'SafeOS Dynamic Update Package not present in WinRE image.'
$Vulnerable = $true
} else {
Write-Output 'SafeOS Dynamic Update Package present in WinRE image.'
$Vulnerable = $false
}
}
if ($CheckImage) {
if (($WinOSBuild -in @(22623, 22621)) -and ($WinREBuild -lt 1105)) {
$Vulnerable = $true
} elseif (($WinOSBuild -eq 22000) -and ($WinREBuild -lt 1455)) {
$Vulnerable = $true
} elseif (($WinOSBuild -in @(19045, 19044, 19042)) -and ($WinREBuild -lt 2486)) {
$Vulnerable = $true
} elseif (($WinOSBuild -eq 19043) -and ($WinREBuild -lt 2364)) {
$Vulnerable = $true
} else {
$Vulnerable = $false
}
}
if ($Vulnerable) {
Write-Warning 'Vulnerable to CVE-2022-41099'
Ninja-Property-Set CVE202241099 1
} else {
Write-Output 'Not vulnerable to CVE-2022-41099'
Ninja-Property-Set CVE202241099 0
}
We run this script daily and have a corresponding monitor setup to check CVE fields with a value of "Yes" and alert us if any are found. You'll find information on remediating this vulnerability in this followup post.
This script has been compiled using information from the following Microsoft sources:
Write-Warning
!-lt
instead of -ne
to ensure future compatibility / accuracy.This has only been tested against M365 Apps and Office 2021 VL versions "en masse" and only 64-bit office - if it doesn't work for you let me know on the NinjaOne Users Discord and I'll see what I can do to fix it!
This article relates to CVE-2023-23397 which is a vulnerability in Microsoft Outlook whereby an attacker could access a user's Net-NTLMv2 hash which could be used as a basis of an NTLM Relay attack against another service to authenticate as the user.
To create a custom field at the device level in NinjaOne go to Administration > Devices and select either Role Custom Fields or Global Custom Fields then select Add.
Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles.
To create a custom field at the organisation level in NinjaOne go to Administration > Apps > Documentation, enable the Documentation feature if you haven't already. If you have then select Add.
When you create your custom field you need to make sure that you set the "Scripts" permission to ensure that you can read or write to the field from your scripts - as appropriate for the script you're using.
We're adding one role custom field for devices with the Windows Desktop or Laptop role, note that we've customised slightly the autogenerated machine name here, if you use the default adjust the field name in the script appropriately.
Field Label | Field Name | Field Type | Description |
---|---|---|---|
CVE-2023-23397 | CVE202323397 | Checkbox | Whether the device has an Office or Microsoft 365 Apps version vulnerable to CVE-2023-23397. |
This script was updated after being published, if you're using it please compare the version you have with the version available here.
This script was last updated on 2023/03/17.
# This script tests whether the Microsoft Office (C2R) installation is vulnerable to CVE-2020-23397.
# Thanks to Concentus on the NinjaOne Users Discord for help running down Office versions to check the logic!
# Thanks to Wisecompany on the One Man Band MSP Discord for reminding me to add an exit code and not overuse `Write-Warning`!
# Thanks to KennyW on the MSPGeek Discord for helping find an error where certain versions were incorrectly detected as not vulnerable!
# Thanks to Alkerayn on the NinjaOne Users Discord for helping find an error where certain channels were incorrectly detected as not vulnerable and identifying that we needed to first check the GPO-configured update channel!
# Thanks to Tanner - MO from MSPs R Us Discord for pointing out that comparisons should all be `-lt` for future proofing.
# Thanks to DarrenWhite99 on the MSPGeek Discord for pointing out that the check for the GPO UpdateChannel was completely nonsensical and incompletely written.
# Thanks to Nullzilla on all the Discords for pointing out that we need to silently continue on missing registry props.
# Thanks to JSanz on the NinjaOne Users Discord for pointing out the GUID matching issue/bug.
# Thanks to Jhn - TS on the NinjaOne Users Discord for discovering the issue with empty registry props causing the script to error.
# Version 1.7.0 - Fix URL handling to avoid errors then the key exists with a null value. - 2023/03/17
# Version 1.6.0 - Improved output, fixed a bug where the update channel GUID was failing to match despite actually matching! - 2023/03/17
# Version 1.5.0 - Silently continue if missing registry properties. - 2023/03/17
# Version 1.4.0 - Handle more Office update channel configuration locations. Fix incorrect channel detection logic when using the GPO UpdateChannel. - 2023/03/17
# Version 1.3.0 - Check versions using a "less than" comparison for vulnerability to allow future proof usage. - 2023/03/16
# Version 1.2.0 - Check GPO channel config, adjust target version for Semi-Annual Enterprise (Preview) channel, fix Write-Warning/Write-Output mixup, more output info. - 2023/03/16
# Version 1.1.0 - Fix O365 app misdetection, better error handling, don't omit warning on success. - 2023/03/16
# Version 1.0.0 - Initial Release - 2023/03/15
$IsC2R = Test-Path 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun'
if ($IsC2R) {
# Get the installed Office Version
$OfficeVersion = [version]( Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' | Select-Object -ExpandProperty VersionToReport )
# Get the installed Office Product IDs
$OfficeProductIds = ( Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' | Select-Object -ExpandProperty ProductReleaseIds )
} else {
Write-Error 'No Click-to-Run Office installation detected. This script only works with Click-to-Run Office installations.'
Exit 1
}
$IsO365 = $OfficeProductIds -like '*O365*'
if ($IsO365) {
# Check the Office GPO settings for the update channel.
$OfficeUpdateChannelGPO = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Office\16.0\Common\OfficeUpdate' -ErrorAction 'SilentlyContinue' | Select-Object -ExpandProperty UpdateBranch -ErrorAction 'SilentlyContinue')
if ($OfficeUpdateChannelGPO) {
Write-Output 'Office is configured to use a GPO update channel.'
# Define the Office Update Channels
$Channels = @(
@{
ID = 'Current'
Name = 'Current'
PatchedVersion = [version]'16.0.16130.20306'
},
@{
ID = 'FirstReleaseCurrent'
Name = 'Current (Preview)'
PatchedVersion = [version]'16.0.16227.20094'
},
@{
ID = 'MonthlyEnterprise'
Name = 'Monthly Enterprise'
PatchedVersion = [version]'16.0.16026.20238'
},
@{
ID = 'Deferred'
Name = 'Semi-Annual Enterprise'
PatchedVersion = [version]'16.0.15601.20578'
},
@{
# This does not match Microsoft's documented version but is the latest available update on tested SAE-Preview channel installations.
ID = 'FirstReleaseDeferred'
Name = 'Semi-Annual Enterprise (Preview)'
PatchedVersion = [version]'16.0.16026.20238'
},
@{
ID = 'InsiderFast'
Name = 'Beta'
PatchedVersion = [version]'16.0.16310.20000'
}
)
foreach ($Channel in $Channels) {
if ($OfficeUpdateChannelGPO -eq $Channel.ID) {
$OfficeChannel = $Channel
}
}
} else {
$C2RConfigurationPath = 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration'
Write-Output 'Office is not configured to use a GPO update channel.'
# Get the UpdateUrl if set
$OfficeUpdateURL = [System.Uri](Get-ItemProperty -Path $C2RConfigurationPath -ErrorAction 'SilentlyContinue' | Select-Object -ExpandProperty UpdateURL -ErrorAction 'SilentlyContinue')
# Get the UnmanagedUpdateUrl if set
$OfficeUnmanagedUpdateURL = [System.Uri](Get-ItemProperty -Path $C2RConfigurationPath -ErrorAction 'SilentlyContinue' | Select-Object -ExpandProperty UnmanagedUpdateURL -ErrorAction 'SilentlyContinue')
# Get the Office Update CDN URL
$OfficeUpdateChannelCDNURL = [System.Uri](Get-ItemProperty -Path $C2RConfigurationPath -ErrorAction 'SilentlyContinue' | Select-Object -ExpandProperty CDNBaseUrl -ErrorAction 'SilentlyContinue')
# Get just the channel GUID
if ($OfficeUpdateURL.IsAbsoluteUri) {
$OfficeUpdateGUID = $OfficeUpdateURL.Segments[2]
} elseif ($OfficeUnmanagedUpdateURL.IsAbsoluteUri) {
$OfficeUpdateGUID = $OfficeUnmanagedUpdateURL.Segments[2]
} elseif ($OfficeUpdateChannelCDNURL.IsAbsoluteUri) {
$OfficeUpdateGUID = $OfficeUpdateChannelCDNURL.Segments[2]
} else {
Write-Error 'Unable to determine Office update channel URL.'
Exit 1
}
# Define the Office Update Channels
$Channels = @(
@{
ID = '492350f6-3a01-4f97-b9c0-c7c6ddf67d60'
Name = 'Current'
PatchedVersion = [version]'16.0.16130.20306'
},
@{
ID = '64256afe-f5d9-4f86-8936-8840a6a4f5be'
Name = 'Current (Preview)'
PatchedVersion = [version]'16.0.16227.20094'
},
@{
ID = '55336b82-a18d-4dd6-b5f6-9e5095c314a6'
Name = 'Monthly Enterprise'
PatchedVersion = [version]'16.0.16026.20238'
},
@{
ID = '7ffbc6bf-bc32-4f92-8982-f9dd17fd3114'
Name = 'Semi-Annual Enterprise'
PatchedVersion = [version]'16.0.15601.20578'
},
@{
# This does not match Microsoft's documented version but is the latest available update on tested SAE-Preview channel installations.
ID = 'b8f9b850-328d-4355-9145-c59439a0c4cf'
Name = 'Semi-Annual Enterprise (Preview)'
PatchedVersion = [version]'16.0.16026.20238'
},
@{
ID = '5440fd1f-7ecb-4221-8110-145efaa6372f'
Name = 'Beta'
PatchedVersion = [version]'16.0.16310.20000'
}
)
foreach ($Channel in $Channels) {
if ($OfficeUpdateGUID -eq $Channel.ID) {
$OfficeChannel = $Channel
}
}
}
if (-not $OfficeChannel) {
Write-Error 'Unable to determine Office update channel.'
Exit 1
} else {
Write-Output ("{0} found using the {1} update channel. `r`nChannel ID: {2}. `r`nTarget Version: {3}. `r`nDetected Version: {4}" -f 'Microsoft 365 Apps', $OfficeChannel.Name, $OfficeChannel.ID, $OfficeChannel.PatchedVersion, $OfficeVersion)
}
}
if ( $OfficeVersion.Major -eq '16' ) {
if ( ( $OfficeVersion.Build -ge 7571 ) -and ( $OfficeVersion.Build -le 16130 ) -and $IsO365 ) {
# Handle Microsoft 365 Apps
if ($OfficeVersion -lt $OfficeChannel.PatchedVersion) {
$Vulnerable = $true
}
} elseif ( ( $OfficeVersion.Build -ge 10356) -and ( $OfficeVersion.Build -le 10396 ) -and ( $OfficeProductIds -like '*2019Volume*' ) -and ( $OfficeProductIds -like '*2019Volume*' ) ) {
# Handle VL Office 2019
if ( ( $OfficeVersion.Build -lt 10396 ) -and ( $OfficeVersion.Revision -lt 20023 ) ) {
Write-Output ("{0} found. `r`nTarget Version: {1}. `r`nDetected Version: {2}" -f 'Office 2019 VL', [Version]'16.0.10396.20023', $OfficeVersion)
$Vulnerable = $true
}
} elseif ( ( $OfficeVersion.Build -ge 12527 ) -and ( $OfficeVersion.Build -le 16130 ) -and ( $OfficeProductIds -like '*Retail*' ) ) {
# Handle Office 2021 Retail, Office 2019 Retail and Office 2016 Retail
if ( ( $OfficeVersion.Build -lt 16130 ) -and ( $OfficeVersion.Revision -lt 20306 ) ) {
Write-Output ("{0} found. `r`nTarget Version: {1}. `r`nDetected Version: {2}" -f 'Office 2021, 2019 or 2016 Retail', [Version]'16.0.16130.20306', $OfficeVersion)
$Vulnerable = $true
}
} elseif ( ( $OfficeVersion.Build -eq 14332 ) -and ( $OfficeProductIds -like '*2021Volume*' ) ) {
# Handle VL Office LTSC 2021
if ( ( $OfficeVersion.Build -ne 14332 ) -and ( $OfficeVersion.Revision -lt 20481 ) ) {
Write-Output ("{0} found. `r`nTarget Version: {1}. `r`nDetected Version: {2}" -f 'Office LTSC 2021', [Version]'16.0.14332.20481', $OfficeVersion)
$Vulnerable = $true
}
}
} elseif ( $OfficeVersion.Major -eq '15' ) {
if ( [version]'15.0.5537.1000' -gt $OfficeVersion ) {
Write-Output ("{0} found. `r`nTarget Version: {1}. `r`nDetected Version: {2}" -f 'Office 2013', [Version]'15.0.5537.1000', $OfficeVersion)
$Vulnerable = $true
}
}
if ($Vulnerable) {
Write-Warning 'This version of Office is vulnerable to CVE-2023-23397.'
Ninja-Property-Set CVE202323397 1
} else {
Write-Output 'This version of Office is not vulnerable to CVE-2023-23397.'
Ninja-Property-Set CVE202323397 0
}
We run this script daily and have a corresponding monitor setup to check CVE fields with a value of "Yes" and alert us if any are found. To remediate this vulnerability update Microsoft Office by running something like this:
This script was updated after being published, if you're using it please compare the version you have with the version available here.
This script was last updated on 2023/03/16.
# Version 1.1.0 - Exit if no C2R.
# Version 1.0.0 - Initial Release - 2023/03/15
[CmdletBinding()]
param ()
if ([System.Environment]::Is64BitOperatingSystem) {
$C2RPaths = @(
(Join-Path -Path $ENV:SystemDrive -ChildPath 'Program Files (x86)\Common Files\Microsoft Shared\ClickToRun\OfficeC2RClient.exe'),
(Join-Path -Path $ENV:SystemDrive -ChildPath 'Program Files\Common Files\Microsoft Shared\ClickToRun\OfficeC2RClient.exe')
)
} else {
$C2RPaths = (Join-Path -Path $ENV:SystemDrive -ChildPath 'Program Files\Common Files\Microsoft Shared\ClickToRun\OfficeC2RClient.exe')
}
$C2RPaths | ForEach-Object {
if (Test-Path -Path $_) {
$C2RPath = $_
}
}
if ($C2RPath) {
Write-Verbose "C2RPath: $C2RPath"
Start-Process -FilePath $C2RPath -ArgumentList '/update user displaylevel=false forceappshutdown=true' -Wait
} else {
Write-Error 'No Click-to-Run Office installation detected. This script only works with Click-to-Run Office installations.'
Exit 1
}
This update script will force restart Office apps - it should restore open files automatically but if you want a softer approach replace the Start-Process
line with:
Start-Process -FilePath $C2RPath -ArgumentList '/update user forceappshutdown=true updatepromptuser=true' -Wait
Prejay on the MSPGeek Discord has helpfully suggested the following to update C2R Office builds without a user logged in or as system:
Start-Process -FilePath $C2RPath -ArgumentList '/frequentupdate SCHEDULEDTASK displaylevel=false' -Wait
Mark Hodges (also on the MSPGeek Discord) has also helpfully suggested this more comprehensive update script which will update Office 2016 and 2019 as well as C2R Office.
This post will show you how to use registry keys to test, set and remove target versions for Windows Feature Updates. This allows you to prevent Windows 10 or 11 from updating past your configured limit.
<#
.DESCRIPTION:
Script to disable the Windows 11 upgrade
.SYNOPSIS:
This script can check the Windows 11 upgrade settings or be used to set/unset those settings using. `-Test` for test-only mode, `-Unset` to remove the block on Windows 11 upgrades and no parameters to set the block.
.AUTHOR:
Mikey O'Toole
www.homotechsual.dev
.REVISION HISTORY:
2023-02-16: Parameterise the script to allow more control over the target versions
2022-01-25: Initial version
#>
[CmdletBinding()]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'RMM script - not useful to implement ShouldProcess')]
param (
[Switch]$Test,
[Switch]$Unset,
[String]$TargetProductVersion = '22H2',
[String]$TargetProduct = 'Windows 11'
)
function Test-UpdateSettings {
$UpdateSettings = Get-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\'
$Message = [System.Collections.Generic.List[String]]::New()
if ($UpdateSettings.TargetReleaseVersion -and $UpdateSettings.TargetReleaseVersion -ne 0) {
$Message.Add('Windows Update is currently set to target a specific release version.')
if ($UpdateSettings.TargetReleaseVersionInfo) {
$Message.Add("Target release version: $($UpdateSettings.TargetReleaseVersionInfo.ToString())")
} else {
$Message.Add('Target release version is not set.')
}
if ($UpdateSettings.ProductVersion) {
$Message.Add("Product version: $($UpdateSettings.ProductVersion.ToString())")
} else {
$Message.Add('Product version is not set.')
}
} else {
$Message.Add('Windows Update is currently set to target all versions.')
}
if ($String -is [array] -or $String.Count -gt 0) {
return $Message.Join(' ')
} else {
return $Message
}
}
function Set-UpdateSettings ([switch]$Unset) {
if ($Unset) {
try {
Set-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\' -Name 'TargetReleaseVersion' -Value 0 -Type DWord
if (Test-Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\TargetReleaseVersionInfo') {
Remove-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\' -Name 'TargetReleaseVersionInfo'
}
if (Test-Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\ProductVersion') {
Remove-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\' -Name 'ProductVersion'
}
} catch {
Throw $_
}
$Message = 'Windows Update is now set to target all versions.'
} else {
try {
Set-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\' -Name 'TargetReleaseVersion' -Value 1 -Type DWord
Set-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\' -Name 'TargetReleaseVersionInfo' -Value $TargetProductVersion
Set-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\' -Name 'ProductVersion' -Value $TargetProduct
$Message = 'Windows Update is now set to target Windows 10, 21H2.'
} catch {
Throw $_
}
}
return $Message
}
if ($Test) {
$Message = Test-UpdateSettings
Write-Output $Message
} elseif ($Unset) {
$Message = Set-UpdateSettings -Unset
Write-Output $Message
} else {
$Message = Set-UpdateSettings
Write-Output $Message
}
When you run this script you might want to pass some parameters - here's what they do:
-Test
- This will test the current target version settings and show you the results.-Unset
- This will remove the target version settings.-TargetProductVersion
- Specify the target version to aim for, examples would be 21H2
or 22H2
.-TargetProduct
- Specify the target product to aim for, examples would be Windows 10
or Windows 11
.This post will show you how to deploy the Printix client using NinjaOne Documentation fields and a PowerShell script.
To create a custom field at the device level in NinjaOne go to Administration > Devices and select either Role Custom Fields or Global Custom Fields then select Add.
Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles.
To create a custom field at the organisation level in NinjaOne go to Administration > Apps > Documentation, enable the Documentation feature if you haven't already. If you have then select Add.
When you create your custom field you need to make sure that you set the "Scripts" permission to ensure that you can read or write to the field from your scripts - as appropriate for the script you're using.
We're adding two documentation fields to facilitate this script. You'll need to note your document template id, in the screenshots / our internal use we have a template called "Integration Identifiers" which we use to store any integration identifiers we need to reference in our scripts.
Field Label | Field Name | Field Type | Description |
---|---|---|---|
Printix Tenant Id | printixTenantId | Text | Holds the customer's Printix tenant id. |
Printix Tenant Domain | printixTenantDomain | Text | Holds the customer's Printix domain. |
[Cmdletbinding()]
param (
[Parameter(Mandatory = $true)]
[String]$DocumentTemplate
)
try {
$PrintixTenantId = Ninja-Property-Docs-Get-Single $DocumentTemplate printixTenantId
$PrintixTenantDomain = Ninja-Property-Docs-Get-Single $DocumentTemplate printixTenantDomain
Write-Verbose ('Found Printix Tenant: {0} ({1})' -f $PrintixTenantId, $PrintixTenantDomain)
if (-not ([String]::IsNullOrEmpty($PrintixTenantId) -and ([String]::IsNullOrEmpty($PrintixTenantDomain)))) {
$PrintixInstallerURL = ('https://api.printix.net/v1/software/tenants/{0}/appl/CLIENT/os/WIN/type/MSI' -f $PrintixTenantId)
Write-Verbose ('Built Printix Installer URL: {0}' -f $PrintixInstallerURL)
$PrintixFileName = "CLIENT_{$PrintixTenantDomain}_{$PrintixTenantId}.msi"
$PrintixSavePath = 'C:\RMM\Installers'
if (-not (Test-Path $PrintixSavePath)) {
New-Item -Path $PrintixSavePath -ItemType Directory | Out-Null
}
$PrintixInstallerPath = ('{0}\{1}' -f $PrintixSavePath, $PrintixFileName)
Invoke-WebRequest -Uri $PrintixInstallerURL -OutFile $PrintixInstallerPath -Headers @{ 'Accept' = 'application/octet-stream' }
if (Test-Path $PrintixInstallerPath) {
Start-Process -FilePath 'msiexec.exe' -ArgumentList @(
'/i',
('"{0}"' -f $PrintixInstallerPath),
'/quiet',
('WRAPPED_ARGUMENTS=/id:{0}' -f $PrintixTenantId)
) -Wait
} else {
Write-Error ('Printix installer not found in {0}' -f $PrintixInstallerPath)
}
}
} catch {
Write-Error ('Failed to install Printix Client: `r`n {0}' -f $_)
}
When you run this script you need to pass your document template id. For example, sticking with our example above, you'd run the script with the parameter: -DocumentTemplate Integration Identifiers
We run this script on a group of devices which don't have the Printix client installed.
This post and the WinRE patching script on Martin's blog at https://manima.de are the result of a collaboration between Martin and I to help mutually improve our various efforts towards patching CVE-2022-41099.
This article relates to CVE-2022-41099 which is a vulnerability in the Windows Recovery Environment (WinRE) which could allow a successful attacker to bypass the BitLocker Device Encryption feature on the system storage device. An attacker with physical access to the target could exploit this vulnerability to gain access to encrypted data.
If you're running Windows 10 or 11 you might have come across CVE-2022-41099 which is a vulnerability in the Windows Recovery Environment (WinRE) which could allow a successful attacker to bypass BitLocker if they can boot the device to WinRE. This is a pretty serious vulnerability and Microsoft have released a patch for it. However, the patch is not applied automatically and you need to take action to apply it.
Martin Himken has written a script to patch the WinRE drivers and I've written a script to download and stage the patch and servicing stack update files. The link to Martin's blog is at the top of this post and will be repeated at the end.
This post takes a snippet from the SMSAgent Blog and adds some additional magic along with two new custom functions.
If you're a Windows 10 or 11 user you'll be familiar with the toast notifications that appear in the bottom right of your screen. These are a great way to get a quick message to the user without interrupting what they're doing. In this article we'll look at how to send a toast notification from PowerShell.
We could use the excellent BurntToast PowerShell module to send a toast notification, but in the interests of reducing the number of third-party modules installed on client machines we'll be using the underlying .NET APIs directly as our needs are fairly simple.
Sending toast notifications is fairly simple once you get to grips with the underlying XML schema but we want our Toasts to be next-level so we're going to make them beautiful by registering a custom Notification App with Windows so we can customise the App Name and Icon which appear.
To create a custom field at the device level in NinjaOne go to Administration > Devices and select either Role Custom Fields or Global Custom Fields then select Add.
Make sure you add the fields to the roles you want to use them in at Administration > Devices > Roles.
To create a custom field at the organisation level in NinjaOne go to Administration > Apps > Documentation, enable the Documentation feature if you haven't already. If you have then select Add.
When you create your custom field you need to make sure that you set the "Scripts" permission to ensure that you can read or write to the field from your scripts - as appropriate for the script you're using.
We're adding three role custom fields for devices with the Windows Laptop role:
Field Name | Field Type | Description |
---|---|---|
Driver Update: Reboot Required | Checkbox | Whether the latest driver update run requires a reboot to finalise. |
Driver Update: Last Run | Date/Time | The date and time the driver update script last ran successfully. |
Driver Update: Number Installed on Last Run | Integer | The number of driver updates installed on last script run. |
[CmdletBinding()]
param ()
try {
# Create a new update service manager COM object.
$UpdateService = New-Object -ComObject Microsoft.Update.ServiceManager
# If the Microsoft Update service is not enabled, enable it.
$MicrosoftUpdateService = $UpdateService.Services | Where-Object { $_.ServiceId -eq '7971f918-a847-4430-9279-4a52d1efe18d' }
if (!$MicrosoftUpdateService) {
$UpdateService.AddService2('7971f918-a847-4430-9279-4a52d1efe18d', 7, '')
}
# Create a new update session COM object.
$UpdateSession = New-Object -ComObject Microsoft.Update.Session
# Create a new update searcher in the update session.
$UpdateSearcher = $UpdateSession.CreateUpdateSearcher()
# Configure the update searcher to search for driver updates from Microsoft Update.
## Set the update searcher
$UpdateSearcher.ServiceID = '7971f918-a847-4430-9279-4a52d1efe18d'
## Set the update searcher to search for per-machine updates only.
$UpdateSearcher.SearchScope = 1
## Set the update searcher to search non-Microsoft sources only (no WSUS, no Windows Update) so Microsoft Update and Manufacturers only.
$UpdateSearcher.ServerSelection = 3
# Set our search criteria to only search for driver updates.
$SearchCriteria = "IsInstalled=0 and Type='Driver'"
# Search for driver updates.
Write-Verbose 'Searching for driver updates...'
$UpdateSearchResult = $UpdateSearcher.Search($SearchCriteria)
$UpdatesAvailable = $UpdateSearchResult.Updates
# If no updates are available, output a message and exit.
if (($UpdatesAvailable.Count -eq 0) -or ([string]::IsNullOrEmpty($UpdatesAvailable))) {
Write-Warning 'No driver updates are available.'
Ninja-Property-Set driverUpdateRebootRequired 0 # Adjust for RMM
Ninja-Property-Set driverUpdateLastRun (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss') # Adjust for RMM
Ninja-Property-Set driverUpdateNumberInstalledOnLastRun 0 # Adjust for RMM
exit 0
} else {
Write-Verbose "Found $($UpdatesAvailable.Count) driver updates."
# Output available updates.
$UpdatesAvailable | Select-Object -Property Title, DriverModel, DriverVerDate, DriverClass, DriverManufacturer | Format-Table
# Create a new update collection to hold the updates we want to download.
$UpdatesToDownload = New-Object -ComObject Microsoft.Update.UpdateColl
$UpdatesAvailable | ForEach-Object {
# Add the update to the update collection.
$UpdatesToDownload.Add($_) | Out-Null
}
# If there are updates to download, download them.
if (($UpdatesToDownload.count -gt 0) -or (![string]::IsNullOrEmpty($UpdatesToDownload))) {
# Create a fresh session to download and install updates.
$UpdaterSession = New-Object -ComObject Microsoft.Update.Session
$UpdateDownloader = $UpdaterSession.CreateUpdateDownloader()
# Add the updates to the downloader.
$UpdateDownloader.Updates = $UpdatesToDownload
# Download the updates.
Write-Verbose 'Downloading driver updates...'
$UpdateDownloader.Download()
}
# Create a new update collection to hold the updates we want to install.
$UpdatesToInstall = New-Object -ComObject Microsoft.Update.UpdateColl
# Add downloaded updates to the update collection.
$UpdatesToDownload | ForEach-Object {
if ($_.IsDownloaded) {
# Add the update to the update collection if it has been downloaded.
$UpdatesToInstall.Add($_) | Out-Null
}
}
# If there are updates to install, install them.
if (($UpdatesToInstall.count -gt 0) -or (![string]::IsNullOrEmpty($UpdatesToInstall))) {
# Create an update installer.
$UpdateInstaller = $UpdaterSession.CreateUpdateInstaller()
# Add the updates to the installer.
$UpdateInstaller.Updates = $UpdatesToInstall
# Install the updates.
Write-Verbose 'Installing driver updates...'
$InstallationResult = $UpdateInstaller.Install()
# If we need to reboot flag that information.
if ($InstallationResult.RebootRequired) {
Write-Warning 'Reboot required to complete driver updates.'
Ninja-Property-Set driverUpdateRebootRequired 1 # Adjust for RMM
}
# Output the results of the installation.
## Result codes: 0 = Not Started, 1 = In Progress, 2 = Succeeded, 3 = Succeeded with Errors, 4 = Failed, 5 = Aborted
## We consider 1, 2, and 3 to be successful here.
if (($InstallationResult.ResultCode -eq 1) -or ($InstallationResult.ResultCode -eq 2) -or ($InstallationResult.ResultCode -eq 3)) {
Write-Verbose 'Driver updates installed successfully.'
Ninja-Property-Set driverUpdateRebootRequired 0 # Adjust for RMM
Ninja-Property-Set driverUpdateLastRun (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss') # Adjust for RMM
Ninja-Property-Set driverUpdateNumberInstalledOnLastRun $UpdatesToInstall.Count # Adjust for RMM
} else {
Write-Warning "Driver updates failed to install. Result code: $($InstallationResult.ResultCode.ToString())"
exit 1
}
}
}
} catch {
Write-Error $_.Exception.Message
exit 1
}
You can set this up to run on a schedule - we run this script immediately on machine onboarding and then every 7 days on a Tuesday. This doesn't always have anything to do as our Windows Update run usually handles these updates, but it's a good way to ensure that we're always up to date with the latest drivers from Microsoft Update.
This post draws on multiple posts, click the link below to check out CyberDrain.com and support Kelvin's fantastic work for the MSP community.
You will find more excellent uses for NinjaOne custom fields on the Dojo, on Stephen Murphy's blog and on Luke Whitelock's blog.
Custom fields are a great way to store arbitrary data from your devices in NinjaOne. In this post I will explore a few examples, some using code from CyberDrain, which store data in NinjaOne custom fields.
This post was updated on 2022/12/22 to add a new script to run a speedtest on a device and store the results in NinjaOne.
Ninja doesn't currently support native AV monitoring via Windows Security Center, integrated AV packages are monitored but what if you need more?