Schwachstellenanalyse: RCE in Ajax.NET Professional

Schwachstelleninformationen zu CVE-2021-23758

Dies ist eine übersetzte Version. Das englische Original finden Sie hier.

Im Herbst 2021 führte MOGWAI LABS einen Penetrationstest für einen Kunden durch. Dabei stieß unser Team auf eine Anwendung, die das Framework “Ajax.NET Professional” verwendete. Bei einer kurzen Analyse des Framworks-Quellcodes fanden wir eine Deserialisierungsschwachstelle, welche wir an den Entwickler meldeten (CVE-2021-23758).

Dieser Blog-Beitrag enthält auch Findings von Markus Wulftange (Code White GmbH), der die Bibliothek im Rahmen eines anderen Projekt analysierte und uns in einer Diskussion auf einige weitere Schwachstellen aufmerksam machte.

Ein chinesischer Sicherheitsforscher (wir glauben sirifu4k1, könnten uns aber auch irren) hat bereits einen Blogpost mit einer detaillierten Analyse dieser Schwachstelle veröffentlicht. Wir möchten dennoch die Möglichkeit darüber zu schreiben, da diese Analyse einige wichtige Punkte nicht berücksichtigt.

Vorstellung von Ajax.NET Professional

Ajax.NET Professional war im Jahr 200x sehr beliebt und man findet nach wie vor Anwendungen, die dieses Framework verwenden. Das offizielle GitHub-Repository bietet eine hervorragende Zusammenfassung, die wir hier zitieren:

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.

Begriffe wie “full datatype support” und “eigene Klassen” erregen immer unsere Aufmerksamkeit, da dies auf einen sehr flexiblen Serialisierungs-/Deserialisierungsprozess hindeutet, doch dazu später mehr.

Grundlagen

ASP.NET-Anwendungen, die AjaxPro verwenden, müssen einen HTTP-Handler in ihrer web.config-Datei konfigurieren. Dieser Handler empfängt die eingehenden Client-Anfragen, deserialisiert die Argumente und ruft dann die eigentliche serverseitige Methode auf. Des Weiteren stellt er die Proxy-JavaScript-Klassen bereit, die zum Aufrufen der .NET-Methoden verwendet werden:

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

Eine Methode, die von JavaScript aus aufgerufen werden kann, muss mit dem Attribut [AjaxPro.AjaxMethod] oder [AjaxMethod] markiert werden. Die Klasse muss zudem mit “RegisterTypeForAjax” registriert werden, damit das Framework den vom Browser generierten JavaScript-Code generiert (Zeile 7).

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

Aus Platzgründen überspringen wir den JavaScript-Code, der zum Aufruf der Methode verwendet wird, und sehen uns direkt die bei einem Aufruf versendete HTTP-Anfrage an:

 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
12
13{}

Ajax.NET Professional verwendet eine HTTP-POST-Anfrage zum serverseitigen Aufruf der Methode. Während die Zielklasse Teil der URL ist, wird die aufzurufende Methode im Request-Header “X-Ajaxpro-Method” angegeben. Die Methodenargumente werden JSON-kodiert im Request Body übergeben.

Analyze des JavaScript Deserialisierer

Anmerkung: Wir verwenden den AJAX.NET Professional Commit f845e338904de7db6908617a2d33840b1dd8b62b als Grundlage für unsere Analyse.

Die Hauptklasse, die zur Deserialisierung des JSON-kodierten HTTP-Bodys verwendet wird, ist “JavaScriptDeserializer”. Bei der Deserialisierung eines Methodenarguments wird dabei die statische Methode “DeserializeFromJson” aufgerufen. Diese Methode besitzt zwei Argumente: die JSON-Daten mit dem serialisierten Objekt und den erwarteten Datentyp. Bei Erfolg wird das deserialisierte Objekt zurückgegeben:

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

Das JSON-Objekt wird in eine JavaScriptObject-Instanz umgewandelt und mit dem erwarteten Objekttyp an die Methode “Deserialize” übergeben. In der Methode können wir sehen, dass ein JSON-Objekt über den Schlüssel “__type” Typinformationen beinhalten kann. Wenn der darin übergebene Typ eine Unterklasse des erwarteten Typs ist, können wir den ursprünglich an den Deserialisierer übergebenen Typ überschreiben:

 1public static object Deserialize(IJavaScriptObject o, Type type)
 2{
 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		...

Die eigentliche Deserialisierung wird in der Klasse “DeserializeCustomObject” durchgeführt:

  1. Erstellen Sie einer neuen Instanz der erwarteten Klasse.
  2. Auslesen der public Properties.
  3. Durchlaufen Sie die im vorherigen Schritt erhaltenen Liste. Dabei wird geprüft, ob das JSON-Objekt ein Element mit diesem Schlüssel enthält. Ist dies der Fall, versucht der Code den Wert aus dem JSON-Objekt dieser Property über die entsprechende “setter”-Methode zuzuweisen.

Dieser Ansatz wird auch bei vielen anderen JSON-Deserialisierern verwendet. Daher sollten die meisten JSON-kompatiblen Gadgets aus dem ysoserial.net Framework ohne Probleme funktionieren.

Hier ein Beispiel für einen JSON-kodierter Payload. Dieser verwendet den “AssemblyInstaller”-Gadget, welches im Whitepaper Friday the 13th JSON Attacks von Alvaro Muñoz und Oleksandr Mirosh vorgestellt wurde:

{
"testpropert1" : "I'm just a string",
"testproperty2":{
      "__type":"System.Configuration.Install.AssemblyInstaller, System.Configuration.Install, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
      "Path":"file:///C:/exploit/MixedAssembly64_v2.dll"
   }
}

Für eine erfolgreiche Ausnutzung dieser Schwachstelle müssen Angreifer sich noch mit dem “erwarteten Datentyp” rumschlagen. Dieser Typ wird an den Deserialisierer übergeben und und unser Gadget muss eine Unterklasse dieses Typs sein. Zur Umgehung dieser Einschränkung existieren zwei Ansätze:

  • Identifizieren Sie eine Methode, die das Attribut [AjaxPro.AjaxMethod] gesetzt hat und eine sehr generische Klasse als Argument erwartet, idealerweise System.Object.
  • Identifizieren Sie eine Klasse, welche vom Client bereitgestellte Typinformation bei dem Deserialisierungsprozesses verwendet.

Interessanterweise bietet das Ajax.NET Professional Framework Beispiele für beide Szenarien:

AjaxPro.Services.IcartService

AjaxPro vor Version v21.10.30.1 enthält Beispielklassen, die nicht vom Framework verwendet werden. Eine dieser Klassen ist “ICartService”, welche die Verwendung von AjaxPro für eine Einkaufswagen-Anwendung demonstrieren soll. Die Klasse bietet eine “AddItem”-Methode, die das Attribut “AjaxMethod” gesetzt hat und ein beliebiges Objekt als Argument entgegennimmt. Wir können diese Methode aufrufen, um eigenen Code auf dem System auszuführen:

 1namespace AjaxPro.Services
 2{
 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);
14
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	}
23}

Den aufmerksamen Lesern mag auffallen, dass es sich um eine abstrakte Klasse handelt, wir also keine Instanz dieser Klasse erstellen können. Dies ist jedoch für uns nicht relevant, da die Deserialisierung der Argumente vor der versuchten Instanziierung der Klasse durchgeführt wird.

Ausnutzen von impliziten Typkonvertern

Anmerkung:
Dieser Ansatz wurde nicht von uns, sondern von Markus Wulftange entdeckt. Er hat auch schon die zuvor erwähnte ICartService-Schwachstelle gefunden, während wir noch dabei waren, den Quellcode zu analysieren.

Ajax.Pro bietet mehrere implizite Typkonverter, um gängige .NET-Datentypen wie DataRows, Hashtables, Exceptions, etc. zu serialisieren/deserialisieren. Einige dieser Konverter erlauben es ebenfalls, eigenen Code auszuführen. Wir werden uns hier nur die Klasse “HashtableConverter” ansehen, andere Konverter sind aber ebenfalls betroffen.

Die Hashtable Klasse des .NET Frameworks erlaubt das Speichern von Daten in Form einer einfachen Schlüssel/Objekt Tabelle. Die Klasse erlaubt dabei das Verwenden beliebiger Objekte als Schlüssel oder Wert:

Hashtable myTable = new Hashtable();

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

Wenn ein solches Hashtable-Objekt per Ajax.NET Professional serialisiert wird, müssen die Typinformationen für jeden Schlüssel und Wert im JSON-Objekt gespeichert werden. Hier ist ein Beispiel, wie das Framework die Hashtable “myTable” aus dem vorherigen Beispiel serialisieren würde.

[
	[
		"key",
		"MOGWAI LABS",
		"System.String",
		"System.String"
	]
]

An diesem Punkt wird die Schwachstelle offensichtlich. Ersetzt man die Typinformationen für den Schlüssel oder Wert durch “System.Object” und gibt ein manipuliertes serialisiertes Objekt als Wert an. Das Funktioniert bei jeder Methode, für die das Attribut [AjaxPro.AjaxMethod] gesetzt wurde und die eine Hashtabelle als Argument erwartet.

Schwachstellenbehebung / Migration

MOGWAI LABS meldete diese Schwachstellen an Michael Schwarz (Entwickler von Ajax.NET Professional), welcher umgehend ein Sicherheitsupdate bereitzustellte. Seit der Version v21.11.11.1 müssen die deserialisierbaren benutzerdefinierten Typen mit einer Block- oder Allowlist konfiguriert werden.

<configuration>
	<ajaxNet>
		<ajaxSettings>
			<jsonDeserializationCustomTypes default="deny">
				<allow>MyOwnNamespace.*</allow>
			</jsonDeserializationCustomTypes>
		</ajaxSettings>
	</ajaxNet>
  ...
</configuration>

Sollte Software auf Ajax.NET basieren, empfehlen wir dringend, auf die neueste Version des Frameworks zu wechseln. Michael Schwarz stellt sogar ein nuget-Paket zur Verfügung, um ein einfaches Update zu ermöglichen.

Falls kein einfacher Wechsel der Frameworkversion möglich sein sollte, empfehlen wir den Zugriff auf “AjaxPro.Services” zu blockieren. Dies kann beispielsweise über folgende Anpassung der “web.config” erfolgen.

<rewrite>
    <globalRules>
        <rule name="RequestBlockingRule1" patternSyntax="Wildcard" stopProcessing="true">
            <match url="*" />
            <conditions logicalGrouping="MatchAny">
                <add input="{URL}" pattern="*AjaxPro.Services*" />
            </conditions>
            <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." />
        </rule>
    </globalRules>
</rewrite>

Dies blockiert die “generische” Ausnutzung der Schwachstelle, die Möglichkeit über TypeConverter eigenen Code auszuführen wird dabei aber nicht verhindert.


Vielen Dank an Komang Gita Krishna Murti bei Unsplash für das Titelbild.