Skip to main content

CVE Detection / Monitoring with NinjaOne Custom Fields

· 6 min read

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.

CVE-2022-41099

This script has been compiled using information from the following Microsoft sources:

security

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.

Fixed a Bug

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.

Parameters!

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

Windows Recovery Environment (WinRE) Not Enabled

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.

Creating Fields

Creating custom fields in NinjaOne

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.

  • Role Custom Fields are custom fields that are specific to a device role.
  • Global Custom Fields are custom fields that are applicable to all devices.

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 LabelField NameField TypeDescription
CVE-2022-41099CVE202241099CheckboxWhether the device has a WinRE image vulnerable to CVE-2022-41099

The Script

This Script Was Updated

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
}

The Results

CVE-2022-41099 Related Custom Fields

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.

CVE-2023-23397

This script has been compiled using information from the following Microsoft sources:

Thanks to:
  • Concentus on the NinjaOne Users Discord for helping me run down and test different versions of Office to ensure this script was as accurate as possible.
  • 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 on the MSPs R Us Discord for pointing out that version comparisons should all use -lt instead of -ne to ensure future compatibility / accuracy.
  • 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 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.

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!

security

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.

Creating Fields

Creating custom fields in NinjaOne

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.

  • Role Custom Fields are custom fields that are specific to a device role.
  • Global Custom Fields are custom fields that are applicable to all devices.

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 LabelField NameField TypeDescription
CVE-2023-23397CVE202323397CheckboxWhether the device has an Office or Microsoft 365 Apps version vulnerable to CVE-2023-23397.

The Script

This Script Was Updated

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
}

The Results

CVE-2023-23397 Related Custom Fields

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

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.