cyllective's blog

Vulnerabilities in Lenovo Vantage

11. Mar 2026, #windows #cve

This post was originally released as just a write-up of CVE-2025-13154. Since we discovered more vulnerabilities, we updated this blog post instead of releasing a second one.

Intro #

This blog post is a write-up of CVE-2025-13154, CVE-2026-1715, CVE-2026-1716, and CVE-2026-1717, four vulnerabilities we found in Lenovo Vantage add-ins. Lenovo Vantage, also distributed as Lenovo Commercial Vantage, is software that comes pre-installed on Lenovo devices running Windows. Its main goal is to give the end-user quick or easier access to certain system settings. It also displays information about the hardware, warranty, and related details. On the business side, it can be used for fleet management. A company can check which devices need replacing based on the warranty status.

For more technical details about Vantage, see the accompanying deep dive blog post ↗ by our colleague Manuel Kiesel. This research was conducted by him, and his post goes into more detail on how Lenovo Vantage works and how vulnerable add-ins can be exploited.

Here is an overview of the vulnerabilities and affected versions. You can check your installed version by checking the directory of the plugin under C:\ProgramData\Lenovo\Vantage\Addins. In there is a folder whose name is the installed version number.

CVEAdd-InVulnerable VersionPatched VersionAdvisory
CVE-2025-13154SmartPerformanceAddin1.0.0.2661.1.0.1111LEN-208293 ↗
CVE-2026-1715DeviceSettingsSystemAddin1.0.8.31.0.8.15LEN-213044 ↗
CVE-2026-1716DeviceSettingsSystemAddin1.0.8.31.0.8.15LEN-213044 ↗
CVE-2026-1717LenovoProductivitySystemAddin1.0.0.1341.0.0.138LEN-213044 ↗

We want to thank Lenovo’s PSIRT team for the swift and proper handling of the vulnerabilities!

CVE-2025-13154 #

This CVE was a team effort between Manuel Kiesel ↗ of cyllective AG ↗ and John Ostrowski of Compass Security ↗. While we did the initial discovery of the vulnerability, we are grateful for John’s help, as the final exploitation would not have been possible without him. A shout‑out also goes to Alex Lee ↗, who reported the vulnerability to Lenovo before we did. It was Lenovo’s decision to include both reporting parties in the CVE reference.

The SmartPerformanceAddin add-in, which runs elevated, has many contracts and commands. Among them is the Do-CleanFiles command. This command will clean out temporary files from the operating system using the following code:

private static bool CleanFiles()
{
    bool result = false;
    _ = string.Empty;
    try
    {
        DeleteDirectory(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Temp"), deleteRoot: false);
        string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly()?.Location);
        if (directoryName != null)
        {
            string text = Path.Combine(directoryName, "FileCleaner.exe");
            if (File.Exists(text))
            {
                if (FileValidator.GetTrustStatus(text) == TrustStatus.FileTrusted)
                {
                    ProcessLauncher.LaunchUserProcess(text, string.Empty, null, visible: false, bAddTmpEnv: false);
                    result = true;
                }
                else
                {
                    Logger.Info("File cleaner not be trusted!");
                }
            }
            else
            {
                Logger.Info("File cleaner not exist!");
            }
        }
    }
    catch (Exception ex)
    {
        Logger.Info("Got exception:" + ex.Message);
    }
    return result;
}

The first call to DeleteDirectory is important. This tries to delete all files within C:\Windows\Temp, a directory a low-privileged user can write to. Since the add‑in runs elevated, this allows privilege escalation using a known MSI rollback technique.

Fun fact: As you can see, whoever wrote this thought about dropping privileges for delete operations, as FileCleaner.exe is executed with downgraded user privileges and not with elevated ones. For some reason, C:\Windows\Temp is deleted by hand and not by the downgraded FileCleaner.exe.

To achieve an elevation of privilege with a file delete, an MSI feature is abused. This was originally discovered by Abdelhamid Naceri ↗ with general tooling ↗ being made public by the Zero Day Initiative ↗, which also has a very detailed write-up ↗ of the trick. The short version is as follows:

The tools ↗ ZDI provides are excellent for this. However, out of the box, they did not work. We investigated this for a bit and hit a brick wall. This is where our collaboration with Compass Security Compass Security ↗ began. After remembering John Ostrowski’s excellent blog post where he abused a race condition ↗, we got in touch and asked for a bit of help with stage 2. It only took a small change, but in the end, John managed to pull it off! Kudos! Check out Compass’s blog ↗ for more information on what changes were needed.

Here is the full PoC. It injects RPC client shell code into a trusted Lenovo binary that runs as our logged-in, low-privileged user. This allows us to send the RPC command Do-CleanFiles from the Vantage.CleanFiles contract to trigger file deletion. Then, ZDI’s FolderOrFileDeleteToSystem and a slightly modified FolderContentsDeleteToFolderDelete are used to place HID.DLL into C:\Program Files\Common Files\microsoft shared\ink.

CVE-2026-1715 #

The DeviceSettingsSystemAddin add-in, which runs elevated, has many contracts and commands. Among them is the RegisterWidgetPath command. This command accepts a <List>Setting with Setting being defined as:

public class Setting
{
    [JsonProperty("key")]
    [XmlAttribute("key")]
    public string Key { get; set; }

    [JsonProperty("value")]
    [XmlText]
    public string Value { get; set; }
}

This is parsed in a way that allows an attacker to specify arbitrary registry keys:

private static bool ParseRegInfo(string payload, ref string hive, ref string subKey, ref string itemName, ref string itemValue, ref string itemTypeString)
{
    ...
    
    List<Setting> list = Serialization<SettingsRequest>.FromJson(payload)?.SettingList;
    if (list == null)
    
    ...

    for (int i = 0; i < list.Count; i++)
    {
        switch (list[i].Key)
        {
        case "Hive":
            hive = list[i].Value;
            break;
        case "SubKey":
            subKey = list[i].Value;
            break;
        case "ItemName":
            itemName = list[i].Value;
            break;
        case "ItemValue":
            itemValue = list[i].Value;
            break;
        case "ItemType":
            itemTypeString = list[i].Value;
            break;
        }
    }
    
    ...
}

Since this add-in runs elevated, this enables an arbitrary Registry write. As an example payload, the following JSON payload could be used:

{
    "settingList": [
        { 
            "key": "Hive", 
            "value": "HKLM"
        },
        {
            "key": "SubKey",
            "value": "SYSTEM\\CurrentControlSet\\Services\\PlugPlay"
        },
        { 
            "key": "ItemName",
            "value": "ImagePath"
        },
        { 
            "key": "ItemValue",
            "value": "C:\\Windows\\Temp\\runme.exe"
        },
        { 
            "key": "ItemType",
            "value": "ExpandString"
        }
    ]
}

Here is the full PoC. It injects RPC client shell code into a trusted Lenovo binary that runs as our logged-in, low-privileged user. This allows us to send the RPC command RegisterWidgetPath from the SystemManagement.Regedit contract to modify registry keys. For privilege escalation, we change the ImagePath expanded string of a service that runs elevated. Upon rebooting (or restarting the service), our binary is executed with elevated privileges.

CVE-2026-1716 #

The same vector can be abused with the RPC command UnregisterWidgetPath. This leads to arbitrary registry deletion. Lenovo tracked this as a separate, second CVE.

CVE-2026-1717 #

The LenovoProductivitySystemAddin add-in, which runs elevated, has many contracts and commands. Among them is the Set-KillProcess command. This command accepts a PID as a string and then kills set process, leading to an arbitrary process kill. The JSON has to have the following format:

public class GPUModeKillProcess : Serialization<GPUModeKillProcess>
{
    [JsonProperty("result")]
    public bool Result { get; set; }

    [JsonProperty("processlist")]
    public List<ProcessItem> ProcessList { get; set; } = new List<ProcessItem>();
}

...

public sealed class ProcessItem
{
    [JsonProperty("pid")]
    public string Pid = string.Empty;

    [JsonProperty("name")]
    public string Name = string.Empty;

    [JsonProperty("icon")]
    public string Icon = string.Empty;
}

Using the following payload, the process with PID 1337 can be terminated.

{
    "processlist": [
        {
            "pid":"1337"
        }
    ]
}

Here is the full PoC. It injects RPC client shell code into a trusted Lenovo binary that runs as our logged-in, low-privileged user. This allows us to send the RPC command Set-KillProcess from the Bios.Assistant contract to kill any process. This could be used to terminate security‑related software.

Timeline #

Table of Contents