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 ImplementsIAllowedTypesSupport
: 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 theLoadProperties
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.
- Before the patch:

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!
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.
- Look for implementations of
- 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.
- Evaluate how persisted UI states are stored and accessed:
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.'
- If the patched version of Telerik UI is installed, it introduces strict type validation and logs errors when an unauthorized object is deserialized:
- 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.
- Applications using IsolatedStorage to retrieve persisted UI states may log retrieval events:
- 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
triggersRequestNavigateEventArgs
, which can serve as an exploitation signature.
- If an exploit successfully triggers execution, system logs may show unexpected child processes spawned by the application:
- 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.
- Beyond headers in
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.