Deserialisation

When the data chooses the code

Insecure deserialisation turns "read this data" into "build whatever this data describes". When the data comes from a client, the client is choosing what your server runs.

Between November 2022 and early January 2023, CISA and the FBI investigated a compromise at a United States federal civilian agency. The way in was a public-facing Microsoft IIS web server running an application built with Telerik UI for ASP.NET AJAX, a widely used commercial component library. The version installed was from 2013. Several groups, including an advanced persistent threat actor and a known criminal group, had used the same flaw to upload and run their own code on the server.

The flaw was CVE-2019-18935, a .NET deserialisation vulnerability rated CVSS 9.8. A fixed version had been available since January 2020. The detail that makes it worth a post is in the advisory: the agency's vulnerability scanner had the correct check for this CVE and still did not report it, because the Telerik library was installed in a file path the scanner did not normally look at. The vulnerability was known, patched, detectable in principle, and present for years.

CVE
CVE-2019-18935
Product
Progress Telerik UI for ASP.NET AJAX through 2019.3.1023. From R1 2020 (2020.1.114) a default setting prevents exploitation.
Class
Deserialisation of untrusted data (CWE-502)
Severity
CVSS 3.1 base score 9.8 (Critical)
Disclosed
December 2019; added to CISA's Known Exploited Vulnerabilities catalogue in November 2021. Joint CISA, FBI and MS-ISAC advisory AA23-074A describes exploitation at a federal agency from November 2022.
Impact
The file-upload component deserialised attacker-supplied data into .NET objects. Where the component's encryption keys were known — through older Telerik flaws or other means — this led to remote code execution as the web server process.

The root cause, in one sentence

The server let data it received decide which kinds of object to create, and some of those objects do things when they are created.

Serialisation is how applications turn objects into bytes and back: for caching, session state, message queues, view state, API payloads and file uploads. Deserialisation is safe when the receiving code decides what type it expects and the data only fills in values. It becomes dangerous when the data itself names the type to construct and the runtime obliges. Most languages have classes that run code as a side effect of being constructed or populated, and chaining those together — a “gadget chain” — turns a data-parsing call into code execution. In the Telerik case, the upload component accepted a serialised description of the upload and allowed it to specify a .NET type. Encryption was meant to stop anybody tampering with it, which made the security of the whole application depend on a key staying secret.

No payloads appear here. As in the other posts in this series, the mechanism is described to the depth a defender needs and no further. If you have any application using Telerik UI for ASP.NET AJAX, work from Progress Telerik's own guidance and CISA's advisory, and search for the library by filename across every server rather than trusting a scanner to find it.

The five shapes it takes

Few development teams would describe themselves as deserialising untrusted data. Most of the deserialisation we find was added by a framework, a library or a convenience feature, and nobody chose it deliberately.

1. Framework state carried by the client

ASP.NET view state, Java Server Faces view state, serialised session cookies and “remember me” tokens. The framework stores an object on the client and expects it back unchanged, protected by a signature or encryption. That protection is only as strong as its key, and keys leak: they are committed to repositories, copied from tutorials, left at framework defaults, and readable through a file read. A known machine key turns a signed state field into a deserialisation entry point.

2. Third-party components with their own formats

This is the Telerik shape. UI component suites, reporting tools, document converters and upload widgets frequently define their own serialised formats for communicating between browser and server. They sit inside your application, so they are reachable on your domain, but they are not your code, and they are versioned and patched separately — which is how a 2013 library survives on a production server in 2023. The same is true of the libraries those libraries depend on.

3. Type information inside JSON and XML

JSON and XML are not safe by being text. Many serialisers support polymorphism: a field in the payload tells the parser which class to construct, so that a list of shapes can contain circles and squares. If that feature is enabled for data that crosses a trust boundary, the client chooses the class. We look for type hints in requests and responses, for serialiser settings that enable polymorphic typing, and for endpoints that accept XML into object-mapping libraries.

4. Internal channels that are not as internal as assumed

Message queues, caches, job runners and remote-procedure endpoints often exchange native serialised objects on the assumption that only trusted services can write to them. Then a cache is shared with another tenant, a queue becomes reachable from a less trusted service, or an SSRF lets an external request reach an internal endpoint. Any channel where the format allows arbitrary types is only as safe as the weakest thing able to write to it.

5. Language-native formats used for convenience

Java object streams, .NET binary formatters, Python pickle, PHP's native unserialize and Ruby's marshalling, used to save time when storing an object in a cookie, a hidden field, a database column or an uploaded file. These formats are designed to reconstruct arbitrary object graphs, which is exactly the capability an attacker needs. Several runtimes now warn against them for untrusted input or have removed them. If the data could ever have come from outside, a native format is the wrong choice.

Why scanning is not enough. The agency in CISA's advisory was scanning, and its scanner knew about this CVE. It missed the library because of where it was installed. Deserialisation flaws are also difficult to detect from outside: there is often no error message and no reflected output, and the vulnerable parameter may be a binary or encrypted blob that a scanner cannot meaningfully alter. Finding them reliably means knowing which components and formats an application uses, which comes from inventory and from reading responses, not from waiting for a signature to match.

How we test for it

  1. Identify every serialised value the application exchanges. We look for the recognisable markers of each format in requests, responses, cookies, hidden fields and uploaded files — including values that are base64-encoded, compressed or wrapped in a signature — and record where each is produced and where it is consumed.
  2. Fingerprint the components. Framework versions, UI suites, upload handlers and reporting tools are identified from responses, script paths and resource names, and checked against published vulnerabilities. This is the step that would have found CVE-2019-18935, regardless of where on disk the library sat.
  3. Test the integrity protection, not just the content. Where a serialised value is signed or encrypted, we establish whether the protection is enforced, whether keys are default or published values, and whether a modified value is rejected or processed.
  4. Probe type handling safely. For formats that accept type information, we test whether the server will construct a type the client names, using benign types and out-of-band detection that confirm the behaviour without executing anything harmful.
  5. Trace internal channels from the outside. Where the application uses queues, caches or job systems, we establish whether any externally reachable input can write to them, directly or through another vulnerability.
  6. Confirm impact within agreed limits. Where deserialisation of attacker-controlled types is confirmed, we demonstrate it with a harmless proof — such as a DNS lookup to a server we control — and stop. Execution of anything more is only performed if it is explicitly agreed in the rules of engagement beforehand.

Each finding is written up with the request, the evidence, and the specific change that fixes it rather than a generic "validate input". That is the standard described in what's in a penetration test report.

Six things to check in your own application this week

  • Search every server — not only the paths your scanner checks — for third-party UI, upload and reporting libraries by filename, and record their versions. Telerik's own guidance lists the safe versions and settings.
  • Replace language-native serialisation of anything that crosses a trust boundary with a data-only format such as JSON, parsed into a type your code chooses.
  • Check your serialisers' configuration for polymorphic or type-name handling, and disable it for external input or restrict it to an explicit allowlist of types.
  • Rotate framework machine keys and signing keys away from defaults, keep them out of source control, and treat any key that has ever been exposed as compromised.
  • Verify the integrity of serialised values before deserialising them, so that a tampered value is rejected without being parsed.
  • Run web server processes with the least privilege they need, and monitor for the web server process launching unexpected programs or writing executables to temporary folders — the behaviour CISA observed in this campaign.

Deserialisation, outdated components and access control are all classes where the decisive work is understanding what an application is actually built from and how it behaves. That is the gap our testing is built around — fixed price, agreed before we start, with the retest included.

Build your quote