Vulnerability Spotlight: RCE in Ajax.NET Professional

Vulnerability / exploitation details for CVE-2021-23758

In Fall of 2021, MOGWAI LABS conducted a penetration test for a customer. In the process, our team encountered an out-of-scope application that was on “Ajax.NET Professional”. A brief analysis of the framworks source revealed a deserialization vulnerability, which MOGWAI LABS reported to the developer (CVE-2021-23758).

This blog post includes findings from Markus Wulftange (Code White GmbH) who also analyzed the library for a different project and pinned us to some key points during a discussion.

A chinese security researcher (we believe sirifu4k1 but could be wrong) already released a blog post with a detailed analysis of this vulnerability, however, we would still want to take the opportunity to blog about it, as the analysis missed some important points.

Introducing Ajax.NET Professional

Ajax.NET Professional was very popular in 200x and you can still find applications that are based on this framework. The official GitHub repository provides a great summary, which we quote here:

Ajax.NET Professional (AjaxPro) is one of the first AJAX frameworks available for Microsoft ASP.NET.

The framework will create proxy JavaScript classes that are used on client-side to invoke methods on the web server with full data type support working on all common web browsers including mobile devices. Return your own classes, structures, DataSets, enums,… as you are doing directly in .NET.

Phrases like “full datatype support” and “own classes” always raise our attention, as this means that the serialization/deserialization process is quite flexible, but more on that later.


ASP.NET applications that want to use AjaxPro need to configure a HTTP handler within their web.config file. This handler will receive the incoming client requests, deserialize the arguments, and then invoke the actual method on the server side. It further serves the proxy JavaScript classes that are used to invoke the .NET methods:

2	<system.web>
3		<httpHandlers>
4			<add verb="POST,GET" path="ajaxpro/*.ashx" type="AjaxPro.AjaxHandlerFactory, AjaxPro.2"/>
5		</httpHandlers>
6	</system.web>

A method that can be called from JavaScript must be marked with the [AjaxPro.AjaxMethod] or [AjaxMethod] attribute. The class must also be registered using “RegisterTypeForAjax” so that the framework will generate the actual JavaScript code that will be used by the client (line 7).

 1namespace MyDemo
 3	public class DefaultWebPage
 4	{
 5		protected void Page_Load(object sender, EventArgs e)
 6		{
 7			AjaxPro.Utility.RegisterTypeForAjax(typeof(DefaultWebPage));
 8		}
10		[AjaxPro.AjaxMethod]
11		public static DateTime GetServerTime()
12		{
13			return DateTime.Now;
14		}
15	}

To keep it short we skip the generated JavaScript code that will be used to call the method and look directly at the generated HTTP request:

 1POST /ajaxpro/AjaxProTest2.MyDemo,AjaxProTest2.ashx HTTP/2
 2Host: localhost:44382
 3Content-Length: 2
 4X-Ajaxpro-Method: GetServerTime
 5Content-Type: text/plain; charset=UTF-8
 6User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
 7Sec-Ch-Ua-Platform: "Windows"
 8Accept: */*
 9Referer: https://localhost:44382/MyDemo.aspx
10Accept-Encoding: gzip, deflate
11Accept-Language: en-US,en;q=0.9

Ajax.NET Professional uses an HTTP POST request to call the method server-side. While the target class is part of the URL, the method to be called is specified in the “X-Ajaxpro-Method” request header. Method arguments get passed as JSON encoded request body.

Analyzing the JavaScript Deserializer

Note: We use AJAX.NET Professional commit f845e338904de7db6908617a2d33840b1dd8b62b in our analysis.

The main class that is used to deserialize the JSON encoded HTTP body is “JavaScriptDeserializer”. When deserializing a method argument, the static “DeserializeFromJson” method gets called. This method has two arguments, the JSON data with the serialized object, and the expected data type. On success, the deserialized object is returned:

1string json = "[1,2,3,4,5,6]";
2object o = JavaScriptDeserializer.DeserializeFromJson(json, typeof(int[]);
3if(o != null)
5	foreach(int i in (int[])o)
6    {
7        Response.Write(i.ToString());
8	}

The JSON data gets converted to an JavaScriptObject instance and will be passed with the expected object type to the “Deserialize” method. Within this method, we can see that the JSON object can contain type information through the “__type” key. If the type is a subclass of the expected type, we can overwrite the original type that was passed to the deserializer:

 1public static object Deserialize(IJavaScriptObject o, Type type)
 3		...
 4		// If the IJavaScriptObject is an JavaScriptObject and we have a key
 5		// __type we will override the Type that is passed to this method. This
 6		// will allow us to use implemented classes where the method will use
 7		// only the interface.
 8		JavaScriptObject jso = o as JavaScriptObject;
 9		if (jso != null && jso.Contains("__type"))
10		{
11			Type t = Type.GetType(jso["__type"].ToString());
12			if (type == null || type.IsAssignableFrom(t))
13				type = t;
14		}
15		...

The actual deserialization is performed within the “DeserializeCustomObject” class using the following steps:

  1. Create an object instance of the expected class.
  2. Retrieve a list of all public properties.
  3. Traverse through the property list and check if the JSON object contains a element with this key. If so, the code attempts to assign the value from the JSON object to that property.

This approach is also used by many other JSON deserializers. Therefore most JSON compatible gadgets from the framework should work without problems.

Here a small example for a JSON encoded object, using the “AssemblyInstaller” gadget that was presented in the Friday the 13th JSON Attacks whitepaper from Alvaro Muñoz and Oleksandr Mirosh:

"testpropert1" : "I'm just a string",
      "__type":"System.Configuration.Install.AssemblyInstaller, System.Configuration.Install, Version=, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",

For successful exploitation, attackers still need to deal with the expected type restriction. Remember that the expected type is passed to the deserializer and our gadget must be a subclass of that type. To bypass this limitation attackers have two options:

  • Identify a method that has the [AjaxPro.AjaxMethod] attribute set and expects a really generic class as argument, ideally System.Object.
  • Identify a class that uses a client-provided type information during the deserialization process.

Interestingly the Ajax.NET professional framework provides examples for both cases:


AjaxPro versions before v21.10.30.1 contain example classes that are actually not used by the framework. One of these classes is the “ICartService” class which should demonstrate the usage of AjaxPro for a shopping cart application. The class provides an “AddItem” method which has the “AjaxMethod” attribute set, and takes a arbitrary object as argument. We can invoke this method to gain remote code execution:

 1namespace AjaxPro.Services
 3	[AjaxNamespace("AjaxPro.Services.Cart")]
 4	public abstract class ICartService
 5	{
 6        /// <summary>
 7        /// Adds the item.
 8        /// </summary>
 9        /// <param name="cartName">Name of the cart.</param>
10        /// <param name="item">The item.</param>
11        /// <returns></returns>
12		[AjaxMethod]
13		public abstract bool AddItem(string cartName, object item);
15        /// <summary>
16        /// Gets the items.
17        /// </summary>
18        /// <param name="cartName">Name of the cart.</param>
19        /// <returns></returns>
20		[AjaxMethod]
21		public abstract object[] GetItems(string cartName);
22	}

An careful reader might notice that this is a abstract class, therefore we can’t create an actual instance of this class. However this does not concern us, as argument deserialization takes place before the attempted instantiation of the class.

Abusing implicit type converters

Note: This approach was not discovered by us, but by Markus Wulftange. He also already discovered the previously mentioned ICartService vulnerability while we were still analyzing the source code.

Ajax.Pro contains multiple implicit type converters to serialize/deserialize common .NET data types like DataRows, Hashtables, Exceptions, etc. Several of these converters can also be abused to gain code execution. We will only have a detailed look at the “HashtableConverter” class but other converters are also affected.

In .NET, the Hashtable class provides a simple key/value collection. It allows to add arbitrary objects as key and value:

Hashtable myTable = new Hashtable();

// Add some elements to the hash table. 
myTable.Add("key", "MOGWAI LABS");

If a Hashtable instance gets serialized by Ajax.NET Professional, the type information for each key and value must be stored in the JSON object. As an example, here is how the framework would serialize the “myTable” hashtable.


From here, the attack becomes obvious. Just replace the type information for the key or value with “System.Object” and provide a malicious serialized object as value. This payload can be passed to any method that has the [AjaxPro.AjaxMethod] attribute set and expects a hash table as argument.


When MOGWAI LABS reported these issues, Michael Schwarz (developer of Ajax.NET Professional) took no time to provide a security update. Since version v21.11.11.1, the deserializable custom types must be configured using a block- or allow list.

			<jsonDeserializationCustomTypes default="deny">

MOGWAI LABS highly recommends to update to the latest version. Nichael Schwarz even provides a nuget package to allow an easy update.

If developers can’t update for some reason any attempt to access the “AjaxPro.Services” should be blocked, for example by updating the web.config as follows:

        <rule name="RequestBlockingRule1" patternSyntax="Wildcard" stopProcessing="true">
            <match url="*" />
            <conditions logicalGrouping="MatchAny">
                <add input="{URL}" pattern="*AjaxPro.Services*" />
            <action type="CustomResponse" statusCode="403" subStatusCode="7" statusReason="Forbidden: Access is denied." statusDescription="You do not have permission to view this directory or page using the credentials that you supplied." />

Developers need to take into account this would still allow the exploitation using TypeConverters but would at least block “generic” exploitation.