Firmware-level threats have increasingly drawn the attention of security teams due to their ability to bypass traditional operating system-level protections. One such emerging vulnerability is PKFail, which involves compromised or improperly signed Secure Boot Platform Keys (PKs). In light of this, organizations must ensure their systems are not exposed to insecure firmware configurations. For IT professionals and managed service providers (MSPs), automating the detection of these vulnerabilities is critical. In this article, you’ll learn how to check PKFail for vulnerabilities with PowerShell proves invaluable.
Background
The PKFail vulnerability centers around Secure Boot—a critical UEFI security feature that ensures only trusted firmware and OS loaders are executed during the boot process. Systems found to have PKs labeled with markers like “DO NOT TRUST” or “DO NOT SHIP” may be susceptible to attacks stemming from unauthorized or non-production signing keys being left in production firmware.
Vendors like Intel and Supermicro have acknowledged this vulnerability, and tools have emerged to aid detection. However, for IT teams managing fleets of Windows devices, a scriptable, scalable solution is essential. This is especially true for MSPs who rely on platforms like NinjaOne to maintain compliance and security posture across diverse client environments.
The Script
<# .SYNOPSIS Checks for the PKFail vulnerability. .DESCRIPTION Checks the PK(Platform Key) variable for 'DO NOT TRUST' or 'DO NOT SHIP'. Can save a result of 'Trusted' or 'Not Trusted' to a custom field. By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use. Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA). .EXAMPLE (No Parameters) Secure Boot is Trusted .EXAMPLE (No Parameters) Secure Boot is Not Trusted PARAMETER: -CustomField "ReplaceMeWithAnyMultilineCustomField" Name of a custom field to save the PKFail status to with either Trusted or Not Trusted. .EXAMPLE -CustomField "SecureBootPK" Attempting to set Custom Field 'SecureBootPK'. Successfully set Custom Field 'SecureBootPK'! Secure Boot is Trusted .NOTES Minimum OS Supported: Windows 10, Windows Server 2016 .LINK https://github.com/binarly-io/Vulnerability-REsearch/blob/main/PKfail/BRLY-2024-005.md https://www.intel.com/content/www/us/en/security-center/announcement/intel-security-announcement-2024-07-25-001.html https://www.supermicro.com/en/support/security_PKFAIL_Jul_2024 #> [CmdletBinding()] param ( [Parameter()] [String]$CustomField ) begin { function Set-NinjaProperty { [CmdletBinding()] Param( [Parameter(Mandatory = $True)] [String]$Name, [Parameter()] [String]$Type, [Parameter(Mandatory = $True, ValueFromPipeline = $True)] $Value, [Parameter()] [String]$DocumentName ) $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters if ($Characters -ge 200000) { throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") } # If requested to set the field value for a Ninja document, specify it here. $DocumentationParams = @{} if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # This is a list of valid fields that can be set. If no type is specified, assume that the input does not need to be changed. $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # The field below requires additional information to set. $NeedsOptions = "Dropdown" if ($DocumentName) { if ($NeedsOptions -contains $Type) { # Redirect error output to the success stream to handle errors more easily if nothing is found or something else goes wrong. $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 } } else { if ($NeedsOptions -contains $Type) { $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 } } # If an error is received with an exception property, exit the function with that error information. if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # The types below require values not typically given to be set. The code below will convert whatever we're given into a format ninjarmm-cli supports. switch ($Type) { "Checkbox" { # Although it's highly likely we were given a value like "True" or a boolean data type, it's better to be safe than sorry. $NinjaValue = [System.Convert]::ToBoolean($Value) } "Date or Date Time" { # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, match the given value with a GUID. $Date = (Get-Date $Value).ToUniversalTime() $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date $NinjaValue = $TimeSpan.TotalSeconds } "Dropdown" { # Ninjarmm-cli expects the GUID of the option we're trying to select, so match the value we were given with a GUID. $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID if (-not $Selection) { throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") } $NinjaValue = $Selection } default { # All the other types shouldn't require additional work on the input. $NinjaValue = $Value } } # Set the field differently depending on whether it's a field in a Ninja Document or not. if ($DocumentName) { $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 } else { $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 } if ($CustomField.Exception) { throw $CustomField } } if ($env:customFieldName -and $env:customFieldName -notlike "null") { $CustomField = $env:customFieldName } # Check if Secure Boot is supported on the system try { Get-SecureBootUEFI -Name PK -ErrorAction Stop | Out-Null } catch { if ($_.Exception.Message -like "*Cmdlet not supported on this platform*") { Write-Host "[Error] System does not support Secure Boot or is a BIOS (Non-UEFI) system." exit 1 } elseif ($_.Exception.Message -like "*Variable is currently undefined*") { Write-Host "[Error] PK variable is not defined." exit 1 } } function Test-IsElevated { $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object System.Security.Principal.WindowsPrincipal($id) $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } if (-not (Test-IsElevated)) { Write-Host "[Error] Access Denied. Please run with Administrator privileges." exit 1 } } process { # Secure Boot PK cert status try { if ([System.Text.Encoding]::ASCII.GetString((Get-SecureBootUEFI -Name PK).bytes) -match "DO NOT TRUST|DO NOT SHIP") { $SecureBootPK = "Not Trusted" Write-Host "[Alert] Secure Boot is $SecureBootPK" } else { $SecureBootPK = "Trusted" Write-Host "[Info] Secure Boot is $SecureBootPK" } } catch { Write-Host "[Error] Get-SecureBootUEFI return error: $($_.Exception.Message)" exit 1 } if ($CustomField) { try { Write-Host "[Info] Attempting to set Custom Field '$CustomField'." Set-NinjaProperty -Name $CustomField -Value $SecureBootPK Write-Host "[Info] Successfully set Custom Field '$CustomField'!" } catch { Write-Host "[Warn] Failed to set Custom Field '$CustomField'." Write-Host "[Warn] $($_.Exception.Message)" } } } end { }
Detailed Breakdown
This PowerShell script automates the detection of the PKFail vulnerability and optionally records the result in a NinjaOne custom field. Here’s how it works:
- Initialization and Parameters
- The script defines a parameter -CustomField, allowing users to specify the name of a NinjaOne custom field where the result—“Trusted” or “Not Trusted”—will be saved.
- It includes a helper function Set-NinjaProperty for interfacing with NinjaOne’s CLI to set field values securely.
- Environment and Compatibility Checks
- The script first checks if Secure Boot is supported on the system by querying the UEFI PK variable using Get-SecureBootUEFI.
- If the command fails due to platform incompatibility (e.g., BIOS-based systems), the script exits gracefully with a descriptive error.
- Privilege Validation
- Secure Boot checks require elevated privileges. The script verifies administrative access before proceeding.
- PK Variable Evaluation
- It retrieves the PK variable’s byte content and converts it to ASCII.
- If the result contains suspicious strings like “DO NOT TRUST” or “DO NOT SHIP,” the system is flagged as Not Trusted. Otherwise, it’s marked as Trusted.
- Optional NinjaOne Integration
- If a custom field is specified, the script attempts to record the trust status directly into the NinjaOne platform for centralized visibility.
Visual Aid Suggestion:
A diagram showing the script workflow—from privilege validation to PK evaluation and NinjaOne integration—can be a valuable supplement for documentation.
Potential Use Cases
Scenario: A mid-sized MSP managing endpoints for a healthcare provider
Security policies demand that all endpoints use Secure Boot with production-signed firmware. The IT team incorporates this PowerShell script into their routine audit process via NinjaOne. The script runs as a scheduled task, and results populate a custom field named “SecureBootPK”. Devices marked as “Not Trusted” trigger alerts, allowing technicians to intervene before any security compliance issues arise.
Comparisons
Unlike vendor-specific tools or manual BIOS inspection, this PowerShell script offers:
- Automation: Can be executed remotely across multiple devices via RMM platforms.
- Scalability: Ideal for environments with hundreds or thousands of devices.
- Flexibility: Easily integrates into broader compliance or asset management workflows through custom fields.
Other approaches, such as using vendor tools like Intel Converged Security and Management Engine (CSME) analyzers, are often GUI-based and less conducive to scripting in managed environments.
FAQs
Q: What systems support Secure Boot and the PK variable?
A: Only UEFI-enabled systems with Secure Boot support will expose the PK variable. BIOS-based systems are not compatible.
Q: What permissions are required to run this script?
A: The script must be executed with Administrator privileges due to the use of Get-SecureBootUEFI.
Q: Can the script work without NinjaOne?
A: Yes. If no custom field is specified, it simply outputs the status to the console.
Q: What happens if the PK variable is undefined?
A: The script will exit and inform the user that the PK variable is not set, which might indicate misconfigured or disabled Secure Boot.
Implications
Detecting systems with untrusted PKs is a vital step in securing the boot process and preventing firmware-level exploitation. A single compromised device can become an entry point for advanced persistent threats. This script provides early visibility into Secure Boot misconfigurations and helps enforce baseline security policies across an environment.
For MSPs, it supports proactive service delivery by identifying potential vulnerabilities before they escalate into incidents.
Recommendations
- Run Periodically: Include this script in regular audit or compliance checks.
- Review and Remediate: Investigate and re-sign firmware for any systems marked as “Not Trusted.”
- Integrate with RMM: Use NinjaOne to store, monitor, and report PK status across all endpoints.
- Educate Clients: Help end customers understand the value of secure firmware and how vulnerabilities like PKFail impact their risk posture.
Final Thoughts
Firmware vulnerabilities like PKFail highlight the importance of extending security beyond the operating system. With PowerShell and this script, IT professionals gain a reliable method to automate detection and maintain compliance. When used in conjunction with NinjaOne, the process becomes even more powerful—allowing you to track trust status, set alerts, and take informed action at scale.