Normal view

There are new articles available, click to refresh the page.
Before yesterdayZero Day Initiative - Blog

Control Your Types or Get Pwned: Remote Code Execution in Exchange PowerShell Backend

16 November 2022 at 16:55

By now you have likely already heard about the in-the-wild exploitation of Exchange Server, chaining CVE-2022-41040 and CVE-2022-41082. It was originally submitted to the ZDI program by the researcher known as “DA-0x43-Dx4-DA-Hx2-Tx2-TP-S-Q from GTSC”. After successful validation, it was immediately submitted to Microsoft. They patched both bugs along with several other Exchange vulnerabilities in the November Patch Tuesday release.

It is a beautiful chain, with an ingenious vector for gaining remote code execution. The tricky part is that it can be exploited in multiple ways, making both mitigation and detection harder. This blog post is divided into two main parts:

·       Part 1 – where we review details of the good old ProxyShell Path Confusion vulnerability (CVE-2021-34473), and we show that it can still be abused by a low-privileged user.
·       Part 2 – where we present the novel RCE vector in the Exchange PowerShell backend.

 Here’s a quick demonstration of the bugs in action:

Part 1: The ProxyShell Path Confusion for Every User (CVE-2022-41040)

There is a great chance that you are already familiar with the original ProxyShell Path Confusion vulnerability (CVE-2021-34473), which allowed Orange Tsai to access the Exchange PowerShell backend during Pwn2Own Vancouver 2021. If you are not, I encourage you to read the details in this blog post.

Microsoft patched this vulnerability in July of 2021. However, it turned out that the patch did not address the root cause of the vulnerability. Post-patch, unauthenticated attackers are no longer able to exploit it due to the implemented access restrictions, but the root cause remains.

First, let’s see what happens if we try to exploit it without authentication.

HTTP Request

HTTP Response

As expected, a 401 Unauthorized error was returned. However, can you spot something interesting in the response? The server says that we can try to authenticate with either Basic or NTLM authentication. Let’s give it a shot.

HTTP Request

HTTP Response

Exchange says that it is cool now! This shows us that:

·       The ProxyShell Path Confusion still exists, as we can reach the PowerShell backend through the autodiscover endpoints.
·       As the autodiscover endpoints allow the use of legacy authentication (NTLM and Basic authentication) by default, we can access those endpoints by providing valid credentials. After successful authentication, our request will be redirected to the selected backend service.

Legacy authentication in Exchange is described by Microsoft here. The following screenshot presents a fragment of the table included in the previously mentioned webpage.

Figure 1 - Legacy authentication in Exchange services, source: https://learn.microsoft.com/

According to the documentation and some manual testing, it seems that an Exchange instance was protected against this vulnerability if:

·       A custom protection mechanism was deployed that blocks the Autodiscover SSRF vector (for example, on the basis of the URL), or
·       If legacy authentication was blocked for the Autodiscover service. This can be done with a single command (though an Exchange Server restart is probably required):

Set-AuthenticationPolicy -BlockLegacyAuthAutodiscover:$true

So far, we have discovered that an authenticated user can access the Exchange PowerShell backend. We will now proceed to the second part of this blog post to discuss how this can be exploited for remote code execution.

Part 2: PowerShell Remoting Objects Conversions – Be Careful or Be Pwned (CVE-2022-41082)

In this part, we will focus on the remote code execution vulnerability in the Exchange PowerShell backend. It is a particularly interesting vulnerability, and is based on two aspects:

·       PowerShell Remoting conversions and instantiations.
·       Exchange custom converters.

It has been a very long ride for me to understand this vulnerability fully and I find that I am still learning more about PowerShell Remoting. The PowerShell Remoting Protocol has a very extensive specifications and there are some hidden treasures in there. You may want to look at the official documentation, although I will try to guide you through the most important aspects. The discussion here should be enough to understand the vulnerability.

PowerShell Remoting Conversions Basics and Exchange Converters

There are several ways in which serialized objects can be passed to a PowerShell Remoting instance. We can divide those objects into two main categories:

·       Primitive type objects
·       Complex objects

Primitive types are not always what you would think of as “primitive”. We have some basic types here such as strings and byte arrays, but “primitive types” also include types such as URI, XMLDocument and ScriptBlock (the last of which is blocked by default in Exchange). Primitive type objects can usually be specified with a single XML tag, for example:

Complex objects have a completely different representation. Let’s take a quick look at the example from the documentation:

First, we can see that the object is specified with the “Obj” tag. Then, we use the “TN” and “T” tags to specify the object type. Here, we have the System.Drawing.Point type, which inherits from System.ValueType.

An object can be constructed in multiple ways. Shown here is probably the simplest case: direct specification of properties. The “Props” tag defines the properties of the object. You can verify this by comparing the presented serialized object and the class documentation.

One may ask: How does PowerShell Remoting deserialize objects? Sadly, there is no single, easy answer here. PowerShell Remoting implements multiple object deserialization (or conversion) mechanisms, including quite complex logic and as well as some validation. I will focus on two main aspects, which are crucial for our vulnerability.

a)     Verifying if the specified type can be deserialized
b)     Converting (deserializing) the object

Which Types Can Be Deserialized?

PowerShell Remoting will not deserialize all .NET types. By default, it allows those types related to the remoting protocol itself. However, the list of allowed types can be extended. Exchange does that through two files:

·       Exchange.types.ps1xml
·       Exchange.partial.types.ps1xml

 An example entry included in those files will be presented soon.

In general, the type specified in the payload that can be deserialized is referenced as the “Target Type For Deserialization”. Let’s move to the second part.

How Is Conversion Performed?

In general, conversion is done in the following way.

·       Retrieve properties/member sets, deserializing complex values if necessary.
·       Verify that this type is allowed to be deserialized.
·       If yes, perform the conversion.

Now the most important part. PowerShell Remoting implements multiple conversion routines. In order to decide which converter should be used, the System.Management.Automation.LanguagePrimitives.FigureConversion(Type, Type) method is used. It accepts two input arguments:

·       Type fromType – the type from which the object will be obtained (for example, string or byte array).
·       Type toType – the target type for deserialization.

The FigureConversion method contains logic to find a proper converter. If it is not able to find any converter, it will throw an exception.

As already mentioned, multiple converters are available. However, the most interesting for us are:

·       ConvertViaParseMethod – invokes Parse(String) method on the target type. In this case, we control the string argument.
·       ConvertViaConstructor – invokes the single-argument constructor that accepts an argument of type fromType. In this case, we can control the argument, but limitations apply.
·       ConvertViaCast – invokes the proper cast operator, which could be an implicit or explicit cast.
·       ConvertViaNoArgumentConstructor – invokes the no-argument constructor and sets the public properties using reflection.
·       CustomConverter – there are also some custom converters specified.

As we can see, these conversions are very powerful and provide a strong reflection primitive. In fact, some of them were already mentioned in the well-known Friday the 13th JSON Attacks Black Hat paper. As we have mentioned, though, the toType is validated and we are not able to use these converters to instantiate objects of arbitrary type. That would certainly be a major security hole.

SerializationTypeConverter – Exchange Custom Converter

Let’s have a look at one particular item specified in the Exchange.types.ps1xml file:

There are several basic things that we can learn from this XML fragment:

·       Microsoft.Exchange.Data.IPvxAddress class is included in the list of the allowed target types.
·       The TargetTypeForDeserialization member gives the full class name.
·       A custom type converter is defined: Microsoft.Exchange.Data.SerializationTypeConverter

The SerializationTypeConverter wraps the BinaryFormatter serializer with ExchangeBinaryFormatterFactory. That way, the BinaryFormatter instance created will make use of the allow and block lists. 

To sum up, some of our types (or members) can be retrieved through BinaryFormatter deserialization. Those types must be included in the SerializationTypeConverter allowlist, though. Moreover, custom converters are last-resort converters. Before they are used, PowerShell Remoting will try to retrieve the object through a constructor or a Parse method.

RCE Payload Walkthrough

It is high time to show you the RCE payload and see what happens during the conversion.

This XML fragment presents the specification of the “-Identity” argument of the “Get-Mailbox” Exchange Powershell cmdlet. We have divided the payload into three sections: Object type, Properties, and Payload.

·       Object type section – specifies that there will be an object of type System.ServiceProcess.ServiceController.
·       Properties section – specifies the properties of the object. One thing that should catch your attention here is the property with the name TargetTypeForDeserialization. You should also notice the byte array with the name SerializationData. (Note that Powershell Remoting accepts an array of bytes in the form of a base64 encoded string).
·       Payload section – contains XML in the form of a string. The XML is a XAML deserialization gadget based on ObjectDataProvider.

Getting Control over TargetTypeForDeserialization

In the first step, we are going to focus on the Properties section of the RCE payload. Before we do that, let’s quickly look at some fragments of the deserialization code. The majority of the deserialization routines are implemented in the System.Management.Automation.InternalDeserializer class.

Let’s begin with this fragment of the ReadOneObject(out string) method:

At [1], it invokes the ReadOneDeserializedObject method, which may return an object.

At [2], the code flow continues, provided an object has been returned. We will focus on this part later.

Let’s quickly look at the ReadOneDeserializedObject method. It goes through the XML tags and executes appropriate actions, depending on the tag. However, only one line is particularly interesting for us.

At [1], it calls ReadPSObject. This happens when the tag name is equal to “Obj”.

Finally, we analyze a fragment of the ReadPSObject function.

At [1], the code retrieves the type names (strings) from the <TN> tag.

At [2], the code retrieves the properties from the <Props> tag.

At [3], the code retrieves the member set from the <MS> tag.

At [4], the code tries to read the primary type (such as string or byte array).

At [5], the code initializes a new deserialization procedure, provided that the tag is an <Obj> tag.

 

So far, we have seen how InternalDeserializer parses the Powershell Remoting XML. As shown earlier, the Properties section of the payload contains a <Props> tag. It seems that we must look at the ReadProperties method.

At [1], the adaptedMembers property of the PSObject object is set to some PowerShell-related collection.

At [2], the property name is obtained (from the N attribute).

At [3], the code again invokes ReadOneObject in order to deserialize the nested object.

At [4], it instantiates a PSProperty object, based on the deserialized value and the property name.

Finally, at [5], it extends adaptedMembers by adding the new PSProperty. This is a crucial step, pay close attention to this.

Let’s again look at the Payload section of our RCE payload:

We have two properties defined here:

·       The Name property, which is of type string and whose value is the string “Type”.

·       The TargetTypeForDeserialization property, whose value is a complex object specified as follows:

o   The type (TN tag) is System.Exception.
o   There is a value stored as a base64 encoded string, representing a byte array.

We have already seen that nested objects (defined with the Obj tag) are also deserialized with the ReadOneObject method. We have already looked at its first part (object retrieval). Now, let’s see what happens further:

At [1], the code retrieves the Type targetTypeForDeserialization through the GetTargetTypeForDeserialization method.

At [2], the code tries to retrieve a new object through the LanguagePrimitives.ConvertTo method (if GetTargetTypeForDeserialization returned anything). The targetTypeForDeserialization is one of the inputs. Another input is the object obtained with the already analyzed ReadOneDeserializedObject method.

As we have specified the object of the System.Exception type (TN tag), the GetTargetTypeForDeserialization method will return the System.Exception type. Why does the exploit use Exception? For two reasons:

·       It is included in the allowlist exchange.partial.types.ps1xml.
·       It has a custom converter registered: Microsoft.Exchange.Data.SerializationTypeConverter.

 These two conditions are important because they allow the object to be retrieved using the SerializationTypeConverter, which was discussed above as a wrapper for BinaryFormatter. Note that there are also various other types available besides System.Exception that meet the two conditions mentioned here, and those types could be used as an alternative to System.Exception.

 Have you ever tried to serialize an object of type Type? If yes, you probably know that it is serialized as an instance of System.UnitySerializationHolder. If you base64-decode the string provided in the Properties part of our payload, you will quickly realize that it is a System.UnitySerializationHolder with the following properties:

·       m_unityType = 0x04,
·       m_assemblyName = "PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
·       m_data = "System.Windows.Markup.XamlReader".

To sum up, our byte array holds the object, which constructs a XamlReader type upon deserialization! That is why we want to use the SerializationTypeConverter – it allows us to retrieve an object of type Type. An immediate difficulty is apparent here, though, because Exchange’s BinaryFormatter is limited to types on the allowlist. Hence, it’s not clear why the deserialization of this byte array should succeed. Amazingly, though, System.UnitySerializationHolder is included in the SerializationTypeConverter’s list of allowed types!

Let’s see how it looks in the debugger:

Figure 2 - Deserialization leading to the retrieval of the XamlReader Type

Even though the targetTypeForDeserialization is Exception, LanguagePrimitives.ConvertTo returned the Type object for XamlReader (see variable obj2). This happens because the final type of the retrieved object is not verified. Finally, this Type object will be added to the adaptedMembers collection (see the ReadProperties method).

Getting Code Execution Through XamlReader, or Any Other Class

We have already deserialized the TargetTypeForDeserialization property, which is a Type object for the XamlReader type. Perfect! As you might expect, allowing users to obtain an arbitrary Type object through deserialization is not the best idea. But we still need to understand: why does PowerShell Remoting respect such a user-defined property? To begin answering this, let’s consider what the code should do next:

·       It should deserialize the <S> tag defined after the <Props> tag (payload section of the input XML). This is a primitive string type, thus it retrieves the string.
·       It should take the type of the main object, which is defined in the <TN> tag (here: System.ServiceProcess.ServiceController).
·       It should try to create the System.ServiceProcess.ServiceController instance from the provided string.

Our goal is to switch types here. We want to perform a conversion so that the System.Windows.Markup.XamlReader type is retrieved from the string. Let’s analyze the GetTargetTypeForDeserialization function to see how this can be achieved.

At [1], it tries to retrieve an object of the PSMemberInfo type using the GetPSStandardMember method. It passes two parameters: backupTypeTable (this contains the Powershell Remoting allowed types/converters) and the hardcoded string “TargetTypeForDeserialization”.

At [2], the code retrieves the Value member from the obtained object and tries to cast it to Type. When successful, the Type object will be returned. If not, null will be returned.

GetPSStandardMember method is not easy to understand, especially when you are not familiar with the classes and methods used here. However, I will try to summarize it for you in two points:

At [1], the PSMemberSet object is retrieved through the TypeTableGetMemberDelegate method. It takes our specified type (here, System.ServiceProcess.ServiceController) and compares it against the list of allowed types. If the provided type is allowed, it will extract its properties and create the new member set.

The following screenshot presents the PSMemberSet retrieved for the System.ServiceProcess.ServiceController type:

Figure 3 - PSMemberSet retrieved for the System.ServiceProcess.ServiceController type

At [2], the collection of members is created from multiple sources. If a member is not included in the basic member set (obtained from the list of allowed types), it will try to find such a member in a different source. This collection includes the adapted members, which contain the deserialized properties obtained through the Props tag.

Finally, it will try to retrieve the TargetTypeForDeserialization member from the final collection.

Let’s have a quick look at the specification of the System.ServiceProcess.ServiceController in the list of allowed types. It is defined in the default Powershell Remoting types list, located in C:\Windows\System32\WindowsPowerShell\v1.0\types.ps1xml.

As you can see, this type does not have the TargetTypeForDeserialization member specified. Only the DefaultDisplayPropertySet member is defined. According to that, the targetTypeForDeserialization will be retrieved from adaptedMembers. As the Exchange SerializationTypeConverter converter allows us to retrieve a Type through deserialization, we can provide a new conversion type to adaptedMembers!

Following screenshot presents the obtained psmemberinfo, which defines the XamlReader type:

Figure 4 - Retrieved XamlReader type

Success! GetTargetTypeForDeserialization returned the XamlReader type. You probably remember that PowerShell Remoting contains several converters. One of them allows calling the Parse(String) method. According to that, we can call the XamlReader.Parse(String) method, where the input will be equal to the string provided in the <S> tag. Let’s quickly verify it with the debugger.

The following screenshot presents the debugging of the LanguagePrimitive.ConvertTo method. The resultType is indeed equal to the XamlReader:

Figure 5 - Debugging of the ConvertTo method - resultType

The next screenshot presents the valueToConvert argument. It includes the string (XAML gadget) included in our payload:

Figure 6 - Debugging of the ConvertTo method - valueToConvert

We will soon reach the LanguagePrimitives.FigureParseConversion method. The following screenshot illustrates debugging this method. One can see that:

·       fromType is equal to String.
·       toType is equal to XamlReader.
· methodInfo contains the XamlReader.Parse(String string) method.

Figure 7 – Debugging the LanguagePrimitives.FigureParseConversion method

Yes! We have been able to get the XamlReader.Parse(String string) method through reflection! We also fully control the input that will be passed to this function. Finally, it will be invoked through the System.Management.Automation.LanguagePrimitives.ConvertViaParseMethod.ConvertWithoutCulture method, as presented in the following screenshot:

Figure 8 - Execution of the XamlReader.Parse method

As you may be aware, XamlReader allows us to achieve code execution through loading XAML (see ysoserial.net). When we continue the process, our command gets executed.

Figure 9 - Remote Code Execution through the Exchange PowerShell backend

There are also plenty of other classes besides XamlReader that could be abused in a similar way. For example, you can call the single-argument constructor of any type, so you can be creative here!

TL;DR – Summary

Getting to understand this vulnerability has been a long and complicated process. I hope that I have provided enough details for you to understand this issue. I would like to summarize the whole Microsoft Exchange chain in several points:

·       The path confusion in the Autodiscover service (CVE-2021-34473) was not fixed, but rather it was restricted to unauthenticated users. Authenticated users can still easily abuse it using Basic or NTLM authentication.

·       PowerShell Remoting allows us to perform object deserialization/conversion operations.

·       PowerShell Remoting includes several powerful converters, which can:

o   Call the public single-argument constructor of the provided type.
o   Call the public Parse(String) method of the provided type.
o   Retrieve an object through reflection.
o   Call custom converters.
o   Other conversions may be possible as well.

·       PowerShell Remoting implements a list of allowed types, so an attacker cannot (directly) invoke converters to instantiate arbitrary types.

·       However, the Exchange custom converter named SerializationTypeConverter allows us to obtain an arbitrary object of type Type.

·       This can be leveraged to fully control the type that will be retrieved through a conversion.

·       The attacker can abuse this behavior to call the Parse(String) method or the public single-argument constructor of almost any class while controlling the input argument.

·       This behavior easily leads to remote code execution. This blog post illustrates exploitation using the System.Windows.Markup.XamlReader.Parse(String) method.

It was not clear to us how Microsoft was going to approach fixing this vulnerability. Direct removal of the System.UnitySerializationHolder from the SerializationTypeConverter allowlist might cause breakage to Exchange functionality. One potential option was to restrict the returned types, for example, by restricting them to the types in the “Microsoft.Exchange.*” namespace. Accordingly, I started looking for Exchange-internal exploitation gadgets. I found more than 20 of them and reported them to Microsoft to help them with their mitigation efforts. That effort appears to have paid off. Microsoft patched the vulnerability by restricting the types that can be returned through the deserialization of System.UnitySerializationHolder according to a general allowlist, and then restricting them further according to a specific denylist. It seems that the gadgets I reported had an influence on that allowlist. I will probably detail some of those gadgets in a future blog post. Stay tuned for more…

Summary

I must admit that I was impressed with this vulnerability. The researcher clearly invested a good amount of time to fully understand the details of PowerShell Remoting, analyze Exchange custom converters, and find a way to abuse them. I had to take my analysis to another level to fully understand this bug chain and look for potential variants and alternate gadgets.

Microsoft patched these bugs in the November release. They also published a blog with additional workarounds you can employ while you test and deploy the patches. You should also make sure you have the September 2021 Cumulative Update (CU) installed. This adds the Exchange Emergency Mitigation service. This automatically installs available mitigations and sends diagnostic data to Microsoft. Still, the best method to prevent exploitation is to apply the most current security updates as they are released. We expect more Exchange patches in the coming months.

In a future blog post, I will describe some internal Exchange gadgets that can be abused to gain remote code execution, arbitrary file reads, or denial-of-service conditions. These have been reported to Microsoft, but we are still waiting for these bug reports to be addressed with patches.   Until then, you can follow me @chudypb and follow the team on Twitter or Instagram for the latest in exploit techniques and security patches.

Control Your Types or Get Pwned: Remote Code Execution in Exchange PowerShell Backend

Vulnerabilities in Apache Batik Default Security Controls – SSRF and RCE Through Remote Class Loading

31 October 2022 at 16:14

Introduction

I stumbled upon the Apache Batik library while researching other Java-based products. It immediately caught my attention, as this library parses Scalable Vector Graphics (SVG) files and transforms them into different raster graphics formats (i.e., PNG, PDF, or JPEG). I was even more encouraged when I looked at the Batik documentation. It was obvious that such a library could be prone to Server-Side Request Forgery (SSRF) issues (e.g., loading of images from remote resources). However, the documentation shows that Batik can also:

·      Execute JavaScript through the Rhino interpreter.
·      Load and execute remote Java classes.

Those are some neat features! On the other hand, Apache Batik protects its users from both SSRF and remote code execution (RCE) vulnerabilities through the various security modes it offers. In this blog post, I am going to show you:

·      How I bypassed the default security modes: DefaultScriptSecurity (CVE-2022-40146) and DefaultExternalResourceSecurity (CVE-2022-38398).
·      How to abuse the SSRF in the default Batik Transcoder to make arbitrary HTTP GET requests or to trigger an NTLM challenge.
·      What configurations are vulnerable to the RCE through remote class loading.
·      What configurations are vulnerable to the RCE through the Rhino interpreter and JavaScript execution.

Before we get into the details, here’s a quick video demonstrating a remote code execution vulnerability exploited through remote JAR loading.

Sample Web Application Endpoint

Apache Batik can be used in different ways. There is a pretty good chance that you have already seen it in web applications during the conversion of SVG to PDF/PNG/JPEG. Let’s define a sample endpoint that performs such a conversion:

```

This endpoint performs the following actions:

·      Retrieves and decodes the base64-encoded SVG file.
·      Creates the Apache Batik JPEG Transcoder.
·      Converts the SVG to JPEG.

This is a common usage of Apache Batik. Note that there is a risk of an easy SSRF if the SVG loads an image from an external resource. Batik tries to protect against such a scenario with its external resource controls, although some of these have existing bypasses with their own CVEs assigned. Let’s quickly review those resource controls.

Apache Batik External Resource Controls

Apache Batik resource controls can be divided into two main categories:

·      Script execution
·      External resources controls (like images). 

Let’s take a brief look at the scripting controls. They control whether script provided within an SVG will be executed. The external resource controls are similar to the scripting controls but apply to the fetching of resources such as images. For a full description of security controls, you can access the documentation here. Here’s a quick overview of the available ScriptSecurity implementations:

·      NoLoadScriptSecurity – scripts are completely blocked.
·      EmbededScriptSecurity [sic] scripts embedded in the document can be executed when properly referenced.
·      DefaultScriptSecurity Embedded external scripts (as above) plus scripts coming from the same origin as the document referencing them are allowed.
·      RelaxedScriptSecurity scripts from any location can be loaded.

In my research so far, I have focused only on the default security controls.

Note that the default security control has a concept of “origin”, but what exactly does this mean? It means that the resource or script will be loaded only if it originates from the same “host” as the SVG file. For example:

·      If we load a local SVG file, we can also load local scripts or resources.
·      If we load a local SVG file, we cannot load scripts or resources from remote origins (e.g., through HTTP or SMB).
·      If we load an SVG file through the HTTP protocol, we can load remote scripts from the same host through either HTTP or any other supported protocol, such as SMB.

In order to avoid any confusion, let’s quickly describe what “loading an SVG through the HTTP protocol” means in Batik. It specifically means that the HTTP URL is directly provided to the Batik TranscoderInput.

This does not look like a particularly common scenario, because the attacker would need to control the URL from which the SVG file is (directly) loaded into Batik.

Accordingly, the default security controls seem to be appropriate. When a local SVG file is loaded, or an SVG is provided as an InputStream, the default controls should block the loading of any remote resources.

Now let’s see how we can bypass those security checks.

DefaultScriptSecurity and DefaultExternalResourceSecurity vs URL.getHost

To start, let’s dig into the DefaultScriptSecurity constructor, which is responsible for our security check. Please note that the code of DefaultExternalResourceSecurity is almost identical, thus it will not be presented.

At [1], the scriptURL and docURL of ParsedURL type are provided. For the sake of simplicity, let’s say that ParsedURL wraps the Java URL class.

At [2] and [3], the respective host strings are retrieved with the getHost method. Under the hood, it retrieves the output of the Java URL.getHost.

At [4], hosts obtained in points [2] and [3] are compared. If they are the same, the code flow continues, and the exception will not be thrown.

At [5], a SecurityException is thrown if the security check is not successful.

If the document host and the script host are the same, the exception will not be thrown, and the script or resource will be loaded. Next, let’s look at how the Java URL.getHost behaves for different protocols:

It’s pretty simple. The host for a local file will be equal to null, whereas for the remote files shown here (referenced by either UNC path or HTTP) it will be equal to evil.com. If the Apache Batik TranscoderInput is created with the InputStream, the getHost method will also return null.

It seems that the default security routines work properly, and we will not be able to load remote resources during processing of SVGs coming from local files or input streams.

Unfortunately, a trivial bypass exists, and honestly, Java itself is the likely culprit. Let’s see the output of the getHost getter for the JAR protocol:

What? It seems that the host for the JAR protocol is also null. In order to properly retrieve the host from the JAR URL, the following cumbersome code can be used. We retrieve the file member from the URL and then use it to create a new URL, from which we can get the host.

In this way, the security check can be easily bypassed with the JAR protocol. Let’s have a look at a sample SVG that leads to SSRF through the image tag.

This SVG will bypass the DefaultExternalResourceSecurity control and will make an HTTP GET request to the attacker’s server. Please note that we can also use the syntax “jar:file://…”, in order to get an NTLMv2 challenge-response in a Windows environment and potentially perform an NTLM relaying attack.

Now that we have shown how to bypass the default external resources controls to get an easy SSRF through the image tag, let’s see how to get remote code execution. This has been fixed in Batik 1.15.

Apache Batik Remote Class Loading Feature

While digging through the Apache Batik documentation, I found an intriguing feature: “Referencing Java Code From a Document”.

Batik implements the Java bindings for SVG, and thus allows Java code to be referenced from script elements…
In order to use this extension, the type attribute of a script element must be set to application/java-archive. In addition, the xlink:href attribute must be the URI of a jar file that contains the code to run.

It seems that we can provide a script of type application/java-archive referencing a properly structured JAR file. We already know that we can bypass the DefaultScriptSecurity check and load files from remote locations, thus it looks like a win!

Luckily, the script execution is not enabled by default in the Apache Batik transcoder and some configuration modifications must be applied:

1)    Enable script execution

Script execution can be enabled in the transcoder with the following line of code:

Please note that the Apache Batik is a “do it yourself” library and often requires a lot of customization. There is a chance that you will see such a configuration in the wild. Also, Batik defines some methods that allow dynamic verification of whether the SVG file contains scripts or not. This method can be used to automatically enable script execution if needed. 

2)    Fix the logical flaw to allow execution of scripts of type “application/java-archive”.

It seems that Apache Batik’s definition of default-allowed script types contains an unintended error. Let’s have a quick look:

You can clearly see that the script types are separated with a comma followed by a space. Now, let’s look at the method that retrieves the list of allowed types:

The list is created with StringTokenizer, where the delimiter is set to a comma without a space! According to that, all the allowed types other than the first one (ECMASCRIPT) will contain a leading space. For example, the allowed type will be “ application/java-archive” instead of “application/java-archive”.

The code will compare the script type declared in the SVG against the type specified in the list. You may think that this is not a problem, since we can declare a script type in the SVG that also includes a space:

<script type=” application/java-archive”>

This will not work, though. The type check for remote JAR loading is performed twice. The second check verifies if the script type is equal to “application/java-archive”, without a leading space. As a result, we will not be able to pass both checks. To enable remote class loading, we must manually modify the list of allowed script types by making an API call as follows:

Now, let’s look at how to perform remote JAR loading in Batik. To begin, we must create a class that implements the EventListenerInitializer interface. Then, we must define the initializeEventListeners method. This method will be executed after the JAR is loaded. Here’s an example of a malicious class:

The MANIFEST.MF file included in the JAR needs to specify SVG-Handler-Class. The following snippet presents an exemplary manifest:

Finally, we specify a malicious SVG that loads such a JAR and bypasses the security policies:

The following screenshot presents a proof of concept where the JAR was loaded through the HTTP protocol and the code was executed, spawning a curl process and making a request to our HTTP server.

Figure 1 - Code Execution through remote JAR loading

Success! We were able to bypass the restrictions defined in DefaultScriptSecurity, load the JAR file from the remote location, and achieve remote code execution.

Scripts Enabled with Remote JAR Loading Disallowed

Due to the logical flaw described in the previous section, it’s likely that you will encounter the following configuration:

·      Scripts enabled.
·      The setting for allowed script types is untouched, so that, in practice, only ECMAScript is enabled.

Luckily, you can still get an easy RCE. ECMAScript is interpreted with Mozilla Rhino, an open-source implementation of JavaScript written in Java. This code execution vector has been already presented by the researcher known as pyn3rd.

The following snippet presents an SVG file with an ugly looking script that will execute the attacker’s command.

The following screenshot demonstrates the achieved code execution:

Figure 2 - Code Execution through ECMAScript

Execution of ECMAScript is a well-documented feature of Apache Batik and this behavior will not be fixed. The official security guide recommends securing your application with the Java SecurityManager when scripting is enabled. I’m taking a guess, but I think that we will never see a completely secure implementation of SecurityManager within Batik. It would likely break too much functionality and make the application unusable. This is still merely speculation on my part. It should also be noted that running scripts is disabled by
default and must be explicitly enabled.

UPDATE: Apache Batik 1.16 has been recently released. It introduces a hardening to Rhino script execution mechanisms. However, it’s hard to call this hardening a strong one. You are still able to call almost any class whose full name starts with “org.”. Saying that, you can still easily abuse many methods included in Apache libraries. This security check can be found here.

Summary

To wrap things up, the security checks implemented in the default external resource controllers of Apache Batik could be bypassed prior to Batik 1.15, in order to:

·      Perform SSRF through the JAR protocol, producing either an HTTP GET request or NTLM relaying via an UNC path.
·      Potentially achieve RCE through remote JAR loading, provided that a non-default configuration was applied.

The bugs discussed here in DefaultScriptSecurity and DefaultExternalResourceSecurity were fixed in version 1.15. Still, Apache Batik may allow the execution of arbitrary Java code when script execution is enabled, even in the latest version 1.16.

If you are using Batik or are interested in finding additional bugs, here are some things to consider:

·      Developers – do not allow scripts to execute through Batik. Apply the strictest controls possible.
·      Pentesters/Bug Hunters – when you test functionality that accepts SVG, try to use the SVG files presented in this blog post.
·      Vulnerability researchers – when your target uses Apache Batik for SVG parsing or conversions, analyze its configuration carefully. You might have an easy win over there.

Thanks for reading, and I hope you’ve enjoyed this post. You can follow me @chudypb and follow the team on Twitter or Instagram for the latest in exploit techniques and security patches.

Vulnerabilities in Apache Batik Default Security Controls – SSRF and RCE Through Remote Class Loading

Riding the InfoRail to Exploit Ivanti Avalanche – Part 2

8 September 2022 at 16:07

In my first blog post covering bugs in Ivanti Avalanche, I covered how I reversed the Avalanche custom InfoRail protocol, which allowed me to communicate with multiple services deployed within this product. This allowed me to find multiple vulnerabilities in the popular mobile device management (MDM) tool. If you aren’t familiar with it, Ivanti Avalanche allows enterprises to manage mobile device and supply chain mobility solutions. That’s why the bugs discussed here could be used by threat actors to disrupt centrally managed Android, iOS and Windows devices. To refresh your memory, the following vulnerabilities were presented in the previous post:

 ·       Five XStream insecure deserialization issues, where deserialization was performed on the level of message handling.
·       A race condition leading to authentication bypass, wherein I abused a weakness in the protocol and the communication between services.

This post is a continuation of that research. By understanding the expanded attack surface exposed by the InfoRail protocol, I was able to discover an additional 20 critical and high severity vulnerabilities. This blog post takes a detailed look at three of my favorite vulnerabilities, two of which have a rating of CVSS 9.8:

·       CVE-2022-36971 – Insecure deserialization.
·       CVE-2021-42133 – Arbitrary file write/read through the SMB server.
·       CVE-2022-36981 – Path traversal, delivered with a fun authentication bypass.

Each of these three vulnerabilities leads to remote code execution as SYSTEM.

CVE-2022-36971: A Tricky Insecure Deserialization

I discovered the first vulnerability when I came across an interesting class named JwtTokenUtility, which defines a non-default constructor that could be a potential target:

At [1], the function base64-decodes one of the arguments.

At [2], it checks if the publicOnly argument is true.

If not, it deserializes the base64 decoded argument at [3].

This looks like a possible insecure deserialization sink. In addition, it is invoked from many locations within the codebase. The following screenshot illustrates several instances where it is invoked with the first argument set to false:

Figure 1 - Example invocations of JwtTokenUtility non-default constructor

It turned out that most of these potential vectors require control over the SQL database. The serialized object is retrieved from the database, and I found no direct way to modify this value. Luckily, there are two services with a more direct attack vector: the Printer Device Server and the Smart Device Server. The exploitation of both services is almost identical. We will focus on the Printer Device Server (PDS).

Let’s have a look at the PDS AmcConfigDirector.createAccessTokenGenerator method:

At [1], it uses acctApi.getGlobal to retrieve an object that implements IGlobal.

At [2], it retrieves the pkk string by calling global.getAccessKeyPair.

At [3], it decrypts the pkk string by calling PasswordUtils.decryptPassword. We are not going to analyze this decryption routine. This decryption function implements a fixed algorithm with a hardcoded key, thus the attacker can easily perform the encryption or decryption on their own.

At [4], it invokes the vulnerable JwtTokenUtility constructor, passing the pkk string as an argument.

At this point, we are aware that there is potential for abusing the non-default JwtTokenUtility constructor. However, we are missing two things:

       -- How can we control the pkk string?
       -- How can we reach createAccessTokenGenerator?

Let’s start with the control of the pkk string.

Controlling the value of pkk

To begin, we know that:

       -- The code retrieves an object to assign to the global variable. This object implements IGlobal.
       -- It calls the global.getAccessKeyPair getter to retrieve pkk.

There is a Global class that appears to control the PDS service global settings. It implements the IGlobal interface and both the getter and the setter for the accessKeyPair member, so this is probably the class we’re looking for.

Next, we must look for corresponding setAccessKeyPair setter calls. Such a call can be found in the AmcConfigDirector.processServerProfile method.

At [1], processServerProfile accepts the config argument, which is of type PrinterAgentConfig.

At [2], it retrieves a list of PropertyPayload objects by calling config.getPayload.

At [3], the code iterates over the list of PropertyPayload objects.

At [4], there is a switch statement based on the property.name field.

At [5], the code checks to see if property.name is equal to the string "webfs.ac.ppk".

If so, it calls setAccessKeyPair at [6].

So, the AmcConfigDirector.processServerProfile method can be used to control the pkk value. Finally, we note that this method can be invoked remotely through a ServerConfigHandler InfoRail message:

At [1], we see that this message type can be accessed through the subcategory 1000000 (see first blog post - Message Processing and Subcategories section).

At [2], the main processMessage method is defined. It will be called during message handling.

At [3], the code retrieves the message payload.

At [4], it calls the second processMessage method.

At [5], it deserializes the payload and casts it to the PrinterAgentConfig type.

At [6], it calls processServerProfile and provides the deserialized config object as an argument.

Success! We can now deliver our own configuration through the ServerConfigHandler method of the PDS server. This method can be invoked through the InfoRail protocol. Next, we need to get familiar with the PrinterAgentConfig class to prepare the appropriate serialized object.

It has a member called payload, which is of type List<PropertyPayload>.

PropertyPayload has two members that are interesting for us: name and value. Recall that the processServerProfile method does the following:

       -- Iterates through the list of PropertyPayload objects with a for loop.
       -- Executes switch statement based on PropertyPayload.name.
       -- Sets values based on PropertyPayload.value.

With this in mind, we can understand how to deliver a serialized object and control the pkk variable. We have to prepare an appropriate gadget (we can use the Ysoserial C3P0 or CommonsBeanutils1 gadgets), encrypt it (decryption will be handled by the PasswordUtils.decryptPassword method) and deliver through the InfoRail protocol.

The properties of the InfoRail message should be as follows:

       -- Message subcategory: 1000000.
       -- InfoRail distribution list address: 255.3.5.15 (PDS server).

Here is an example payload:

The first step of the exploitation is completed. Next, we must find a way to call the createAccessTokenGenerator function.

Triggering the Deserialization

Because the full flow that leads to the invocation of createAccessTokenGenerator is extensive, I will omit some of the more tedious details. We will instead focus on the InfoRail message that allows us to trigger the deserialization via the needFullConfigSync function. Be aware that the PDS server frequently performs synchronization operations, but typically does not perform a synchronization of the full configuration. By calling needFullConfigSync, a full synchronization will be performed, leading to execution of doPostDeploymentCleanup:

At [1], the code invokes our target method, createAccessTokenGenerator.

The following snippet presents the NotificationHandler message, which calls the needFullConfigSync method:

At [1], the message subcategory is defined as 2200.

At [2], the main processMessage method is defined.

At [3], the payload is deserialized and casted to the NotifyUpdate type (variable nu).

At [4], the code iterates through the entries of the NotifyUpdateEntry object that was obtained from nu.getEntries.

At [5], [6], and [7], the code checks to see if entry.ObjectType is equal to 61, 64, or 59.

If one of the conditions is true, the code sets the universalDeployment variable to true value at [8], [9], or [10], so that needFullConfigSync will be called at [11].

The last step is to create an appropriate serialized message object. An example payload is presented below. Here, the objectType field is equal to 61.

The attacker must send this payload through a message with the following properties:

-- Message subcategory: 2200.
-- InfoRail distribution list address: 255.3.5.15 (PDS server).

To summarize, we must send two different InfoRail messages to exploit this deserialization issue. The first message is to invoke ServerConfigHandler, which delivers a serialized pkk string. The second message is to invoke NotificationHandler, to trigger the insecure deserialization of the pkk value. The final result is a nice pre-auth remote code execution as SYSTEM.

CVE-2021-42133: One Vuln to Rule Them All - Arbitrary File Read and Write

Ivanti Avalanche has a File Store functionality, which can be used to upload files of various types. This functionality has been already abused in the past, in CVE-2021-42125, where an administrative user could:

-- Use the web application to change the location of the File Storage and point it to the web root.
-- Upload a file of any extension, such as a JSP webshell, through the web-based functionality.
-- Use the webshell to get code execution.

The File Store configuration operations are performed through the Enterprise Server, and they can be invoked through InfoRail messages. I quickly discovered three interesting properties of the File Store:

  1. It supports Samba shares. Therefore, it is possible to connect it to any reachable SMB server. The attacker can set the File Store path pointing to his server by specifying a UNC path.
  2. Whenever the File Store path is changed, Avalanche copies all files from the previous File Store directory to the new one. If a file of the same name already exists in the new location, it will be overwritten.
  3. The File Store path can also be set to any location in the local file system.

These properties allow an attacker to freely exchange files between their SMB server and the Ivanti Avalanche local file system. In order to modify the File Store configuration, the attacker needs to send a SetFileStoreConfig message:

At [1], the subcategory is defined as 1501.

At [2], the standard Enterprise Server processMessage method is defined. The implementation of message processing is a little bit different in the Enterprise Server, although the underlying idea is the same as in previous examples.

At [3] and [4], the method saves the new configuration values.

The only thing that we must know about the saveConfig method is that it overwrites all the properties with the new ones provided in the serialized payload. Moreover, some of the properties, such as the username and password for the SMB share, are encrypted in the same manner as in the previously described deserialization vulnerability.

To sum up this part, we must send an InfoRail message with the following properties:

--Message subcategory: 1501.
--Message distribution list: 255.3.2.5 (Enterprise Server).

Below is a fragment of an example payload, which sets the File Store path to an attacker-controlled SMB server:

Arbitrary File Read Scenario

The whole Arbitrary File Read scenario can be summarized in the following picture:

Figure 2 - Example scenario for the Arbitrary File Read exploitation

  1. The attacker points the File Store to a non-existent SMB share. This step is optional, but makes the exploit cleaner by ensuring that files from the current File Store location will not be copied to the location where the attacker wants to retrieve the files.
  2. The attacker points the File Store to a desired local file system path from which he wants to disclose files.
  3. The attacker points the File Store to his SMB share.
  4. Files from the previous File Store path (local file system) are transferred to the attacker’s SMB share.

The following screenshot presents an example exploitation of this scenario:

Figure 3 - Exploitation of the Arbitrary File Read scenario

As shown, the exploit is targeting the main Ivanti Avalanche directory: C:\Program Files\Wavelink\Avalanche.

The following screenshot presents the exploitation results. Files from the Avalanche main directory were gradually copied to the attacker’s server:

Figure 4 - Exploitation of the Arbitrary File Read scenario - results

Arbitrary File Write Scenario

The following screenshot presents the Arbitrary File Write scenario:

Figure 5 - Example scenario for the Arbitrary File Write scenario

  1. The attacker creates an SMB share that contains the JSP webshell.
  2. The attacker points the File Store to a non-existent SMB share. This step is optional, but makes the exploit cleaner by ensuring that files from the current File Store location will not be copied to the Avalanche webroot.
  3. The attacker points the File Store to the SMB share containing the webshell file.
  4. The attacker points the File Store to the Ivanti Avalanche webroot directory.
  5. Avalanche copies the webshell to the webroot.
  6. The attacker executes code through the webshell.

The following screenshot presents an example exploitation attempt. It uploads a file named poc-upload.jsp to C:\Program Files\Wavelink\Avalanche\Web\webapps\ROOT:

Figure 6 - Exploitation of the Arbitrary File Write scenario

Finally, one can use the uploaded webshell to execute arbitrary commands.

Figure 7 - Executing arbitrary code via the webshell

CVE-2022-36981: Path Traversal in File Upload, Plus Authentication Bypass

We made it to the final vulnerability we will discuss today. This time, we will exploit a path traversal vulnerability in the Avalanche Smart Device Server, which listens on port TCP 8888 by default. However, InfoRail will play a role during the authentication bypass that allows us to reach the vulnerable code.

Path Traversal in File Upload

Our analysis begins with examining the uploadFile method.

At [1], the endpoint path is defined. The path contains two arguments: uuid and targetbasename.

At [2], the doUploadFile method is called.

Let’s start with the second part of the doUploadFile method, as I want to save the authentication analysis for later in this section.

At [1], the uploadPath string is obtained by calling getUploadFilePath. This method accepts two controllable input arguments: uuid and baseFileName.

At [2], the method instantiates a File object based on uploadPath.

At [3], the method invokes writeToFile, passing the attacker-controlled input stream together with the File object.

We will now analyze the crucial getUploadFilePath method, as this is the method that composes the destination path.

At [1], it constructs deviceRoot as an object of type File. The parameters passed to the constructor are the hardcoded path obtained from getCachePath() and the attacker-controllable uuid value. As shown above, uuid is not subjected to any validation, so we can perform path traversal here.

At [2], the code verifies that the deviceRoot directory exists. From here we see that uuid is intended to specify a directory. If the directory does not exist, the code creates it at [3].

At [4], it validates the attacker-controlled baseFileName against a regular expression. If the validation fails, baseFileName is reassigned at [5].

At [6], it creates a new filename fn, based on the current datetime, an integer value, and baseFileName.

At [7], it instantiates a new object of type File. The path for this File object is composed from uuid and fn.

After ensuring that the file does not already exist, the file path is returned at [8].

After analyzing this method, we can draw two conclusions:

       -- The uuid parameter is not validated to guard against path traversal sequences. An attacker can use this to escape to a different directory.
       -- The extension of baseFileName is not validated. An attacker can use this to upload a file with any extension, though the filename will be prepended with a datetime and an integer.

Ultimately, when doUploadFile calls writeToFile, it will create a new file with this name and write the attacker-controlled input stream to the file. This makes it seem that we can exploit this as a path traversal vulnerability and write an arbitrary file to the filesystem. However, there are two major obstacles that will be presented in the next section.

Authentication and Additional UUID Verification

Now that we’ve covered the second part, let’s go back and analyze the first part of the doUploadFile method.

At [1], the code retrieves the mysterious testFlags.

At [2], it validates the length of the uuid, to ensure it is at least 5 characters long.

At [3], it performs an authorization check (perhaps better thought of as an authentication check) by calling isAuthorized. This method accepts uuid, credentials (authorization), and testFlags.

At [4], the code retrieves the deviceId based on the provided uuid.

At [5], the code checks to see if any device was retrieved. If not, it checks for a specific value in testFlags at [6]. If this second check is also not successful, the code raises an exception.

At [7], it calls allowUpload to perform one additional check. However, this final check has nothing to do with validating uuid. It only verifies the amount of available disk space, and this should not pose any difficulties for us.

We can spot two potential roadblocks:

       -- There is an authentication check.
       -- There is a check on the value of uuid, in that it must map to a known deviceId. However, we can bypass this check if we could get control over testFlags. If testFlags & 0x100 is not equal to 0, the exception will not be thrown, and execution will proceed.

Let’s analyze the most important fragments of the isAuthorized method:

At [1], the method retrieves enrollmentId, found within the token submitted by the requester.

At [2], it tries to retrieve the enrollment object from the database, based on enrollmentId.

At [3], it checks to see if enrollment was retrieved.

Supposing that enrollment was not retrieved successfully, the code checks for a particular value in testFlags at [4]. If not, it will return false at [6]. But if the relevant value is found in testFlags, the authentication routine will return true at [5], even though the requester’s authorization token did not contain a valid enrollmentId.

Note that this method also checks an enrollment password, although that part is not important for our purposes.

Here as well, testFlags can also be used to bypass the relevant check. Hence, if we can control testFlags, neither the authentication nor the uuid validation will cause any further trouble for us.

Here is where InfoRail comes into play. It turns out that the Smart Device Server AgentTaskHandler message can be used to modify testFlags:

At [1], it retrieves flagsToSet from the sds.modflags.set property.

At [2], it obtains the Config Directory API interface.

At [3], it uses flagsToSet to calculate the new flags value.

At [4], it saves the new flag value.

To sum up, an attacker can control testFlags, and use this to bypass both the authentication check and the uuid check.

Exploitation

Exploitation includes two steps.

1) Set testFlags to bypass the authentication and the uuid check.

To modify the testFlags, the attacker must send an InfoRail message with the following parameters:

       -- Message subcategory: 2500.
       -- Distribution list: 255.3.2.17 (SDS server).
       -- Payload:

2) Exploit the path Traversal through a web request

The path traversal can be exploited with an HTTP Request, as in the following example:

The response will return the name of the uploaded webshell:

Finally, an attacker can use the uploaded JSP webshell for remote code execution as SYSTEM.

Figure 8 - Remote Code Execution with the uploaded webshell

Conclusion

I really hope that you were able to make it through this blog post, as I was not able to describe those issues with a smaller number of details (believe me, I have tried). As you can see, undiscovered attack surfaces can lead to both cool and dangerous vulnerabilities. It is something that you must look for, especially in products that are responsible for the administration of many other devices.

This blog post is the last in this series of articles on Ivanti Avalanche research. However, I am planning something new, and yes, it concerns Java deserialization. Until then, you can follow me @chudypb and follow the team on Twitter or Instagram for the latest in exploit techniques and security patches.

Riding the InfoRail to Exploit Ivanti Avalanche – Part 2

Riding the InfoRail to Exploit Ivanti Avalanche

19 July 2022 at 13:59

Back in 2021, I stumbled upon a proof of concept describing an arbitrary file read vulnerability in the Ivanti Avalanche mobile device management tool. As I was not aware of this product, I decided to take a quick look at the vendor’s website to learn more:

“Avalanche Enterprise Mobile Device Management manages some of the most demanding, high-profile supply chain mobility solutions in the world. So, we understand the pressure you're under to maximize worker (and device) uptime.”

“Manage all mobile devices in one place. Smartphones, barcode scanners, wearables and more—configure, deploy, update, and maintain them all in one system.”

“30,000 organizations. Over 10 million devices.”

As the product specification seemed to be both intriguing and encouraging, I decided to give it a shot and look for some vulnerabilities.

I was able to quickly identify a chain of three vulnerabilities in the Ivanti Avalanche Web Application:

- ZDI-21-1298 (CVE-2021-42124): Session takeover vulnerability, which requires user interaction.
- ZDI-21-1300 (CVE-2021-42126): privilege escalation vulnerability, which allows an attacker to gain administrative privileges.
- ZDI-21-1299 (CVE-2021-42125) – Remote code execution vulnerability, which can be exploited from the level of an administrator account.

Even though this chain is powerful, its first part heavily depends on factors that are not within the attacker’s control. We can do better, right?

The Inspiration

Later, I identified several interesting facts concerning Ivanti Avalanche:

- It contains multiple XStream 1.4.12 jar packages in its directories.
- It implements multiple ObjectGraph classes, which wrap the XStream serializer.
- Some of those classes define an allowlist of classes permitted for deserialization, while others lack such an allowlist.

This led me to suspect that there are multiple untrusted deserialization issues in the product. However, I was not yet able to determine how to reach those deserialization routines. Fortunately, I decided to pursue the matter further, and it turned out to be a long, exciting journey. It allowed me to not only exploit the XStream deserialization issues but to significantly increase the attack surface and find many more 0-days.

This blog post describes the first part of my Ivanti Avalanche research, where I identified the Ivanti Avalanche custom network protocol and abused it to perform several remote code executions. Moreover, it describes a cool race condition vulnerability that leads to an authentication bypass.

Identifying Ivanti Avalanche Services

To begin, let’s have a look at the Avalanche services:

Figure 1 - Ivanti Avalanche Services

We find we are dealing with the Tomcat Web Application plus other services. “Wavelink Information Router” seems to be the most interesting of them, due to the following description: “Coordinates communication between Wavelink processes…”.

At this stage, it all seemed complicated. Because of this, I used the following methods to gain some more insight:
• Network traffic analysis
• Static analysis
• Log file analysis

After a while, I was able to see a bigger picture. It seemed that the Avalanche Web Application is not able to perform tasks by itself, not even a login operation. However, it can freely use different services to perform tasks. The Information Router, also known as the InfoRail service, stands in between, and is responsible for the distribution of messages between the services. The InfoRail service listens on TCP port 0.0.0.0:7225 by default.

Figure 2 - Inter-Services communication

As can be seen in the above diagram, the communication flow is straightforward. The Web Backend creates a message and sends it to the InfoRail service, which forwards it to the appropriate destination service. The destination service handles the message and returns the response to the Web Backend, once more via the InfoRail service. In the default installation, all these services are running on the same host. However, it is possible to place some of these services on remote machines.

On the other hand, it is not easy to get more detailed information about the messages. Network traffic analysis was not of much use, as the messages seem to be obfuscated or encrypted. In order to learn something more about them, I had to conduct a detailed code analysis.

The following section presents an overview of the InfoRail protocol and all the basics needed to:
• Recreate the protocol and messages
• Understand its basics
• Understand the payload delivery mechanism

InfoRail Protocol Basics

A typical InfoRail message consists of three main parts:
• Preamble
• Header
• Optional XML payload

The following picture presents a message structure:

Figure 3 - Message Structure

The InfoRail Preamble

The preamble always has a length of 24 bytes. It consists of the following parts:
• Bytes 1-4: Length of the whole message
• Bytes 5-8: Length of the header
• Bytes 9-12: Length of the payload
• Bytes 13-16: Length of the uncompressed payload (payload can be optionally compressed)
• Bytes 17-20: Message ID (typically an incremented number, starting from 1)
• Byte 21: Protocol version (typically 0x10)
• Bytes 22-23: Reserved • Byte 24: Encryption flag (payload and header can be optionally encrypted) – 0 or 1

The InfoRail Header

The header of the typical message consists of multiple keys together with their corresponding values. Those keys and values are included in the header in the following way:
• 3 null bytes
• 0x02 byte
• 3 null bytes
• 1 byte which provides the length of a key (e.g. 0x08)
• 3 null bytes
• 1 byte which provides the length of a value (e.g. 0x06)
• Key + value

Here’s an example key and value:

         \x00\x00\x00\x02\x00\x00\x00\x08\x00\x00\x00\x06h.msgcat999999

As was mentioned before, a header can contain multiple keys. The most important ones that appear in most or all messages are:
h.msgcat - in typical requests, it is equal to 10 (request)
h.msgsubcat - information about the request type, thus it is crucial for the payload processing
h.msgtag - usually stores the JSESSIONID cookie, although no method that verifies this cookie was spotted, thus this value can be random.
h.distlist - specifies one or more services to which the message will be forwarded by InfoRail

The header must be padded with null bytes to a multiple of 16 bytes, due to the optional encryption that can be applied.

The Payload

As was mentioned before, the majority of messages contain an XML payload. An XStream serializer is used for the payload serialization and deserialization operations.

Different serialized objects can be transferred depending on the target service and the message subcategory. However, in most cases we are dealing with the RequestPayload object . The following snippet presents an example of the XML that is sent during an authentication operation. In this case, Avalanche Web Backend sends this XML to the Enterprise Server service, which then verifies user credentials.

RequestPayload.xml

As you can see, this message contains a serialized UserCredentials object, which consists of the loginName, encrypted password, domain, and clientIpAddress. You can also see that the RequestPayload contains the web cookie (sessionId). However, this cookie is never verified, so it can be set to any MD5 hash. As the payload can be optionally encrypted (and compressed), it must be padded to a multiple of 16 bytes.

Typically, every service implements its own ObjectGraph class, which defines the instance of the XStream serializer and configures it. Here’s a fragment of an example implementation:

ObjectGraphPart.java

When the service receives the XML payload, it deserializes it with the ObjectGraph.fromXML method.

Encryption

As was mentioned before, the header and the payload of the message can be encrypted. Furthermore, the payload can be compressed. InfoRail does not use the SSL/TLS protocol for encryption. Instead, it manually applies an encryption algorithm using a hardcoded encryption key.

As both the encryption algorithm and the encryption key can be extracted from the source code, the potential attacker can perform both the decryption and encryption operations without any obstacles.

Distribution to Services

The InfoRail service needs to know where to forward the received message. This information is stored in the h.distlist key of the message header. Values that can be used are stored in the IrConstants class. Variables that start with IR_TOPIC define the services where the message can be forwarded. The following code snippet presents several hardcoded variables:

DistributionVariables.java

If the sender wants the message to be forwarded to the license server, the h.distlist value must be set to 255.3.2.8.

Message Processing and Subcategories

This is probably the most important part of the protocol description. As was mentioned before, the message category is included in the message header under the h.msgsubcat key. As seen in the last line of the code snippet below, services protect themselves against the handling of an unknown message and, if the provided message subcategory is not implemented, the message is dropped.

Message processing may be implemented in slightly different ways depending on the service. Here, we are going to take a brief look at the Avalanche Notification Server. The following snippet represents the processMessage method found in the MessageDispatcher class:

processMessage.java

At [1], the message subcategory is retrieved from the header. Then, it is passed to the MessageProcessorVector.getProcessor method to obtain a reference to the appropriate message handler

At [2], the switch-case statement begins.

If the category is equal to 10 (request), the processInfoRailRequest method is invoked at [3]. Its arguments contain the handler that was retrieved at step [1]. We can have a quick look at this method here:

processInfoRailRequest.java

Basically, if the handler is not null, the code will invoke the handler.ProcessMessage method.

Finally, we must investigate how handlers are retrieved. The most important part is as follows:

MessageProcessorVector.java

At [1], a HashMap<Integer, IMessageProcessor> is defined.

At [2] and subsequent lines, the code invokes the setProcessor method. It accepts the message subcategory integer and the corresponding object that implements IMessageProcessor, such as AnsCredentialsHandler.

At [3], the setProcessor method is defined. It inserts the subcategory and appropriate object into the HashMap defined at [1].

At [4], getProcessor retrieves the handler based on the provided subcategory.

To summarize, there is a HashMap that stores the message subcategories and their corresponding handler objects. If we send a message to the Avalanche Notification Server with the subtype equal to 3706, the AnsTestHandler.processMessage method will be invoked.

Let’s have a look at one example of a message handler, AnsTestHandler:.

AnsTestHandler.java

At [1], the SUBCATEGORY variable is defined. It is common for the message processors to define such a variable.

At [2], the processMessage method is defined.

At [3], it retrieves the XML payload from the message.

At [4], it deserializes the payload with the ObjectGraph.fromXML method. It then casts it to AnsTestPayload, although the casting occurs after the deserialization. According to that, if the ObjectGraph does not implement any additional protections (such as an allowlist), we should be able to do harm here.

Success! We have identified the message processing routine. Moreover, we were able to quickly map the subcategory to the corresponding fragment of code that will handle our message. It seems that right now, we should be able to craft our own message.

Is There Any Authentication?

At this point, it seemed that I had everything necessary to send my own message. However, when I sent one, I saw no reaction from the targeted service. I looked at the InfoRail service log file and spotted these interesting lines:

Figure 4 InfoRail Log Files – Dropping the Unauthenticated Message

It seems that I had missed an important part: authentication. Having all the details regarding the protocol and encryption, I was able to quickly identify a registration message in the network traffic. It is one of the rare examples where the payload is not stored in the XML form. The following screenshot presents a fragment of a registration message:

Figure 5 - Fragment of an Exemplary Registration Message

Several important things can be spotted here:
         -- reg.appident – specifies the name of the service that tries to register.
         -- reg.uname / reg.puname – specifies something that looks like a username.
         -- reg.cred / reg.pcred – specifies something that looks like a hashed password.

After a good deal of code analysis, I determined the following:
         -- Uname and puname are partially random.
         -- Cred and pcred values are MD5 hashes, based on the following values:
                  -- Username (.anonymous.0.digits).
                  -- Appropriate fragment of the key, which is hardcoded in the source code.

Once more, the only secret that is needed is visible in the source code. The attacker can retrieve the key and construct his own, valid registration message.

Finally, we can properly register with the InfoRail service and send our own messages to any of the Avalanche services.

Just Give Me Those Pwns Please – Code Execution on Four Services

At this stage, one can verify the services that do not implement a allowlist in the ObjectGraph class. I identified 5 of them:

- Data Repository Service (ZDI-CAN-15169).
- StatServer Service (ZDI-CAN-15130).
- Notification Server Service (ZDI-CAN-15448).
- Certificate Management Server Service (ZDI-CAN-15449).
- Web File Server Service (ZDI-CAN-15330).

We have five XStream deserialization endpoints that will deserialize anything we provide. One can immediately begin to consider ways to exploit this deserialization. For starters, XStream is very transparent about its security. Their security page (available here) presents multiple gadgets based on classes available in the Java runtime. Sadly, no suitable gadget worked for the first four services that I tried to exploit, as the required classes were not loaded.

Going further, one can have a look at Moritz Bechler’s “Java Unmarshaller Security” paper:

XStream tries to permit as many object graphs as possible – the default converters are pretty much Java Serialization on steroids. Except for the call to the first non-serializable parent constructor, it seems that everything that can be achieved by Java Serialization can be with XStream – including proxy construction.

 Proxy construction does not seem to be a thing in the newer versions of XStream. However, we should still be able to exploit it with ysoserial gadgets. Ysoserial gadgets for XStream were not available back then, so I created several myself. They can be found in this GitHub repository.

 Using my ysoserial XStream gadgets, I was successful in getting remode code execution in four Avalanche services. Here is a summary of the services I was able to exploit, and the required gadgets:

- StatServer: Exploited using AspectJWeaver and CommonsBeanutils1
- Data Repository: Exploited using C3P0 and CommonsBeanutils1
- Certificate Management Server: Exploited using CommonsBeanutils1
- Avalanche Notification Server: Exploited using CommonsBeanutils1

For no particular reason, let’s focus on the StatServer. To begin, we must find the topic and subcategory of some message that will deserialize the included XML payload. According to that, the InfoRail protocol message headers should contain the following keys and values:
h.distlist = 255.3.2.12
h.msgsubcat = 3502 (GetMuCellTowerData message)

In this example, we will make use of the AspectJWeaver gadget, which allows us to upload files. Below is the AspectJWeaver gadget for the XStream:

AspectJWeaver.xml

Several things can be highlighted in this gadget:
• The iConstant tag contains the Base64 file content.
• The folder tag contains a path to the upload directory. I am targeting the Avalanche Web Application root directory here.
• The key tag specifies the name of the file.

Having all the data needed, we can start the exploitation process:

Figure 6 - StatServer Exploitation - Upload of Web shell

The following screenshot presents the uploaded web shell and the execution of the whoami command.

Figure 7 - StatServer Exploitation - Webshell and Command Execution

Success! To sum up this part, an attacker who can send messages to Avalanche services can abuse the XStream deserialization in 4 different services.

I also got remote code execution on a fifth serviceHowever, exploiting this one was way trickier.

Exploiting JNDI Lookups Before It Was Cool

Java Naming and Directory Interface (JNDI) Lookups have a long history, and a lot of researchers were familiar with this vector long before the Log4Shell vulnerability. One of the proofs for that is CVE-2021-39146 – an XStream deserialization gadget that triggers a lookup. It has occurred to be the only XStream gadget that worked against the Web File Server service, for which I was not able to craft a valid ysoserial gadget.

Still, we are dealing with a fresh Java version. Thus, we are not able to abuse remote class loading. Moreover, the attacker is not able to abuse the LDAP deserialization vector (described here [PDF]). Using the JNDI injection, we can deliver the serialized payload, which will be then deserialized by our target. However, we are not aware of any deserialization gadgets that could be abused in the Web File Server.  If there were any gadgets, we would not need the JNDI lookup in the first place. Luckily, there are several interesting JAR packages included in the Web File Server Classpath.

Figure 8 - Web File Server - Tomcat JARs

As you can see, the Web File Server loads several Tomcat JAR packages. You might perhaps also be familiar with the great technique discovered by Michael Stepankin, which abuses unsafe reflection in Tomcat BeanFactory to execute arbitrary commands through JNDI Lookup. The original description can be found here.

In summary, we can execute the following attack:

  1. Set up malicious LDAP server, which will serve the malicious BeanFactory. We will use the Rogue JNDI tool.
  2. Register with the InfoRail service.
  3. Send the message containing the CVE-2021-39146 gadget, targeting the Web File Server pointing to the server defined in step 1.
  4. The Web File Server makes an LDAP lookup and retrieves data from the malicious server.
  5. Remote code execution.

The following screenshot presents the setup of the LDAP server:

Figure 9 - Setup of Rogue Jndi

The next snippet presents the CVE-2021-39146 gadget, which was used for this Proof of Concept:

jndiGadget.xml

If everything goes well and the lookup was performed successfully, Rogue JNDI should show the following message and the code should be executed on the targeted system.

Figure 10 - Triggered JNDI Lookup

To summarize, we were able to abuse the custom Ivanti Avalanche protocol and XML message deserialization mechanisms to exploit five different services and get remote code execution with SYSTEM privileges. I have found more deserialization vulnerabilities in Ivanti Avalanche, but I am leaving them for the second part of the blog post. Right now, I would like to stay with the protocol and inter-services communication.

Abusing a Race Condition in the Communication and Authentication Messages

As detailed above, the various Avalanche services communicate with each other with the help of InfoRail. When a service provides a response, the response is again forwarded via the InfoRail service. The idea for this research was: is it possible for an attacker to spoof a response? If so, it may be possible to abuse Ivanti Avalanche behavior and perform potentially malicious actions.

I focused on the authentication operation, which is presented in the following scheme (read from top to bottom):

Figure 11 - Authentication Scheme

When a user authenticates through the Avalanche Web Application login panel, the backend transfers the authentication message to the Enterprise Server. This service verifies credentials and sends back an appropriate response. If the provided credentials were correct, the user gets authenticated.

During this research, I learned two important things:
• The attacker can register as any service.
• The authentication message is distributed amongst every instance of the registered Enterprise Server.

According to that, the attacker can register himself as the Enterprise Server and intercept the incoming authentication messages. However, this behavior does not have much of an immediate consequence, as the transferred password is hashed and encrypted.

The next question was whether it is possible to deliver the attacker’s own response to the Avalanche Web and whether it will be accepted or not. It turns out that yes, it is possible! If you would like to deliver your own response, you must properly set two values in the message header:
Origin – The topic (ID) of the Avalanche Web backend that sent the message.
MsgId – The message ID of the original authentication message.

Both values are relatively easy to get, and thus the attacker can provide his own response to the message. It will be accepted by the service, which is waiting for the response. An example attack scenario is presented in the following figure (read from top to bottom ).

Figure 12 - Race Condition Scheme

The attack scenario works as follows:
         -- The attacker tries to log into the Web Application with the wrong credentials.
         -- Web Application sends the authentication message.
         --The InfoRail service sends the message to both Enterprise Servers: the legitimate one and the malicious one.
         --Race time:
                  --The legitimate server responds with the “wrong credentials” message.
                  --The malicious server responds with the “credentials OK” message.          --If the attacker’s server was first to deliver the message, it is forwarded to the Avalanche Web Application.
         --The attacker gets authenticated.

Please note that the “Login message” targeting the attacker’s server is not present here on purpose (although it is in reality transmitted to the attacker). I wanted to highlight the fact, that this issue can be exploited without a possibility to read messages. In such a case, the attacker has to brute-force the already mentioned message ID value. It complicates the whole attack, but the exploitation is still possible.

To summarize this part, the attacker can set up his own malicious Enterprise Server and abuse a race condition to deliver his own authentication response to the Web Application. There are two more things to investigate: what does the response message look like and are we able to win the race?

Authentication Handling

The following snippet presents an example response to a login message. Please note that those messages are way bigger during legitimate usage. However, for the Proof of Concept, I have minimized them and stored only those parts that are needed for exploitation.

The response message consists of several important parts:
• It contains a responseObject tag, which is a serialized User object.
• It contains a responseCode tag, which will be important.

At some point during authentication, the Web Backend invokes the UserService.doLogin method:

doLogin.java

At [1], the UserCredentials object is instantiated. Then, its members are set.

At [2], the authenticate method is called, and the object initialized at [1] is passed as an argument:

authenticate.java

At [1], the UserLogin object is initialized.

At [2], the UserCredentials object is serialized.

At [3], the message is being sent to the Enterprise Server, and the Web Backend waits for the response.

At [4], the responseCode included in the response is verified. We want it to be equal to 0.

At [5], the userLogin.authenticated is set to True.

At [6], the userLogin.currentUser is set to the object that was included in the responseObject.

At [7], the method returns the userLogin object.

Basically, the response should have a responseCode equal to 0. It should also include a properly serialized User object in the responseObject tag.

Finally, we analyze the fragment of UserBean.loginInner method that was responsible for the invocation of the doLogin function.

loginInner.java

At [1], the doLogin method is invoked. It retrieves the object of UserLogin type (see the returns of previous code snippets).

At [2], it sets this.currentUser to the userLogin.currentUser (the one obtained from the response).

At [3], it sets various other settings. These will not be relevant for us.

One very important thing to note: the Web Backend does not compare the username provided in the login panel and the username retrieved by the Enterprise Server. Accordingly, the attacker can:
• Trigger the authentication with the username “poc”.
• Win the race and provide the user “amcadmin” in the response.
• The attacker will then be authenticated as the “amcadmin”.

To summarize, it seems that there are no obstacles for the attacker and his response should be handled by the Web Backend without any problems. Next, we turn our focus to winning the race.

Winning the Race

In a default installation, the Enterprise Server and InfoRail services are located on the same host as the Web Backend. This makes the exploitation of the race condition harder, as the legitimate communication is handled by the local interface, which is much faster than communication through an external network interface.

Still, the attacker has some advantages. For example, he does not have to generate dynamic responses, as the response payload can be hardcoded in the exploit. The following table presents a general overview of actions that must be performed by both the attacker and the legitimate Enterprise Server.

Attacker Enterprise Server
·       Get the message ID from the original Login message and place it in the header. ·       Get the message ID from the original Login message and place it in the header.
·      Send static response. ·       Decrypt and verify the user’s credentials.
  ·       Retrieve the user’s details and create the User object.
  ·       Dynamically create the response.

The remote attacker needs to perform far fewer operations and can prepare his response more rapidly. It makes the race winnable. One may ask – what if the attacker’s position in the network too disadvantageous and he still has no chance to win the race? Even then, there is an effective solution.

The unauthenticated attacker could modify the Avalanche System Settings. This is due to a separate vulnerability that allows to bypassing domain authentication (ZDI-CAN-15919). The remote attacker could enable the LDAP-based authentication and set any address for the LDAP server configuration. In such a scenario, the legitimate Enterprise Server will first try to access this “rogue” authentication server. This will give the attacker an additional second or two (or even more if played properly). In this way, an attacker can gain more time to win the race, should this be necessary

Exploitation

Having all the pieces, the race condition vulnerability can be exploited. The following video shows an example of the attacker providing wrong credentials via the panel but was still successfully authenticating as the “amcadmin” user.

Conclusion

When you don’t have ideas for further research, make sure that you have discovered the complete attack surface of your recent targets. When you find a spot that was not already studied by others, you might find a path to discovering a whole new set of exciting vulnerabilities.

Keep an eye out for the second part of this blog, where I will share some details about my favorite vulnerabilities in the Ivanti Avalanche services. It will cover one more deserialization vulnerability, cool chains leading to the remote code execution, and a pretty rare information disclosure vulnerability.

Until then, you can follow me @chudypb  and the team @thezdi on Twitter for the latest in exploit techniques and security patches.

Riding the InfoRail to Exploit Ivanti Avalanche

❌
❌