The real slim shady || Ivanti Endpoint Manager (EPM) Pre-Auth RCE

CVE-2024-29847

Introduction

ivanti just pushed a patch for a Critical CVSS 9.8 (Critical) Remote Code Execution Vulnerability that I reported on May 1st 2024, impacting Ivanti Endpoint Manager (EPM). the following blog post I will be publishing the fully working unauthenticated exploit and detail how this bug class works.

ivantiEPM

P.S: this blog post is full of grammar errors.

Wasn’t this covered already?

Folks at horizon3 tried to publish a writeup and exploit for the bug I found, but they unintentially made a mistake and published details and writeup for a different CVE instead ivantiEPM

“The Real” Ivanti EPM Insecure Deserialization (CVE-2024-29847)

ivantiEPM

Inside of the Agentportal.exe executable the LANDesk.AgentPortal.AgentPortal.OnStart method is invoked when ever the “Agent portal" service starts, this service is always running by default and it is possible to achieve unauthenticated remote code execution by exploiting this service.

Following is the decompiled view of this service executable, the OnStart method will first create an instance of the System.Runtime.Remoting.Channels.Tcp.TcpChannel.TcpChannel class [1] by invoking its constructor and passing 0 for its port property, this causes the port for this service to be dynamically chosen, and then at [2] the code proceeds to register a TCP channel by invoking System.Runtime.Remoting.Channels.ChannelServices.RegisterChannel passing in the tcp channel object and false for the ensureSecurity flag.

following the code path at [3] the LANDesk.AgentPortal.IAgentPortal service which is a MarshalByRefObject is registered as a WellKnownServiceType, then at [4] an absolute address is constructed and a call to LANDesk.AgentPortal.IAgentPortalBase.PublishChannelURL is made

protected override void Dispose(bool disposing)
		{
			if (disposing && this.components != null)
			{
				this.components.Dispose();
			}
			base.Dispose(disposing);
		}

		// Token: 0x06000005 RID: 5 RVA: 0x00002114 File Offset: 0x00001114
		protected override void OnStart(string[] args)
		{
			TcpChannel tcpChannel = new TcpChannel(0); // [1]
			ChannelServices.RegisterChannel(tcpChannel, false); // [2]
			RemotingConfiguration.RegisterWellKnownServiceType(Type.GetType("LANDesk.AgentPortal.IAgentPortal"), "LDSM", WellKnownObjectMode.SingleCall); // [3]
			RemotingConfiguration.ApplicationName = "LANDeskAgentPortal";
			string[] urlsForUri = tcpChannel.GetUrlsForUri("LANDeskAgentPortal");
			if (urlsForUri.Length >= 1)
			{
				IAgentPortalBase.PublishChannelURL(urlsForUri[0] + "/LDSM"); // [4]
			}
			AgentPortal.JobList = new NamedCollection();
			if (!this.CleanupTimer.Enabled)
			{
				this.CleanupTimer.Start();
			}
		}

		// Token: 0x06000006 RID: 6 RVA: 0x00002192 File Offset: 0x00001192
		protected override void OnStop()
		{
			this.CleanupTimer.Stop();
		}

the LANDesk.AgentPortal.IAgentPortalBase.PublishChannelURL will save the constructed URL absolute address into the registry

		// Token: 0x06000008 RID: 8 RVA: 0x00002098 File Offset: 0x00001098
		public static void PublishChannelURL(string url)
		{
			RegistryKey registryKey = IAgentPortalBase.OpenRegKey("Software\\Landesk\\SharedComponents", true);
			if (registryKey != null)
			{
				registryKey.SetValue("LANDeskAgentPortal", url);
			}
		}

The vulnerability here is the fact that Microsoft .NET Remoting is a dangerous and powerful technology which it’s use is extremely prohibited by Microsoft but to this day it’s uses are seen in a lot of critical infrastructures.

as a reminder, the port number has been set to “0" which causes this service to start on a dynamic port.

TcpChannel

By default the type filter is set to low which prevents the easy exploitation of the .NET Remoting Stack, but it’s achievable and requires more effort. Following is a detailed introduction to .NET remoting exploitation.

Introduction to Exploiting .NET Remoting

Disclaimer: Everything in the .NET Remoting exploitation internals explained here is based on James Forshaw’s research in .net remoting exploitation/internals, I am just a student of his work

A big thank you to my dear friend Markus Wulftange for always answering my questions, you have my utmost respect

TcpChannel

As James Forshaw published, normally when we are exploiting a .NET Remoting instance, we need to target objects that are remotable, in order for an object to be “remotable” it should either be Serializable or inherit MarshalByRefObject class. now forshaw figured out that we can abuse the fact that certain classes such as DirectoryInfo and FileInfo are both derived from MarshalByRefObject (MBR) and are also Serializable.

Taking another page from forshaw’s book, here is the exploitation flow when targeting .NET Remoting that has the Full Type Filtering

  1. Classes Deriving from MarshalByRefObject (MBR) and Serializable: DirectoryInfo and FileInfo are mentioned as examples of classes that meet these criteria. These classes can be serialized and deserialized across a network and are derived from MarshalByRefObject.
  2. Crafted Hashtable with MBR Instance of IEqualityComparer: By deserializing an instance of one of these special classes (DirectoryInfo or FileInfo) inside a carefully crafted Hashtable, along with a MarshalByRefObject instance of IEqualityComparer, the server can be tricked into passing back the instance.
  3. Marshalling by Reference (MBR): As the object is passed back over a remoting channel, the DirectoryInfo or FileInfo objects are marshalled by reference and remain on the server.
  4. Arbitrary File Operations: With the DirectoryInfo or FileInfo objects now stuck inside the server, methods can be called on these objects to perform arbitrary file operations, such as reading and writing files.
  5. Remote Code Execution: This technique potentially allows an attacker to achieve full code execution on the server by leveraging the ability to manipulate files and execute arbitrary commands.

Remotable

Here is how James Forshaw describes the flow which is absolutely amazing

.NET Remoting exploitation flow when Type Filter is set to Full, designed by James Forshaw

.NET Remoting exploitation flow when Type Filter is set to Full, designed by James Forshaw

Exploiting .NET Remoting (Low Type Filter)

When the Low Type Filter is enabled the following restrictions are applied:

Now forshaw figured out if we go about using the previous technique we’ll encounter some obstacles, first, in order to get the instance of the special object such as DirectoryInfo and FileInfo we need to pass a MarshalByRef IEqualityProvider which gets blocked when the Type Filter is set to Low.

Second, even if we do manage to get a hold of the DirectoryInfo or FileInfo and want to use it, when our message is sent over the proxy channel and is deserialized, a Demand is made for the FileIOPermission For the “Path" we have referenced, sadly, this causes a “Permission Demand" and falls into the restriction rules that only SerializationFormatter permissions are allowed

Untitled

And lastly, even if we do manage pass the MarshalByRef IEqualityProvider and we do manage to have the server deserialize our malicious FileInfo or DirectoryInfo without raising permission demands, we’ll encounter another problem, for a target server to “send back the reference" to an special object, it needs to use the network stack and connect back to us, this will raise another permission demand and falls into the restrictions category one more time.

Bypassing MarshalByRefObject Type Checking

Forshaw found out it is possible to bypass the first restriction that prevents MarshalByRef objects being deserialized by not using MethodCall or MethodReturn as the “Top Level Record", so by passing a Hashtable as the top level object, it’ll cause the remoting server code to fault when trying to call methods on the message object but by then it’d be too late and the bypass is done. This is how the top level record is checked:

internal void ReadMethodObject(BinaryHeaderEnum binaryHeaderEnum) {
  SerTrace.Log(this, "ReadMethodObject");
  if (binaryHeaderEnum == BinaryHeaderEnum.MethodCall) {
    BinaryMethodCall record = new BinaryMethodCall();
    record.Read(this);
    record.Dump();
    objectReader.SetMethodCall(record);
  } else {
    BinaryMethodReturn record = new BinaryMethodReturn();
    record.Read(this);
    record.Dump();
    objectReader.SetMethodReturn(record);
  }
}

Bypassing FileInfo/DirectoryInfo CAS Permission Demand

When System.Runtime.Remoting.dll receives an incoming request if the type filter level has been set to low, the following branch is taken

PermissionSet currentPermissionSet = null;                  
if (this.TypeFilterLevel != TypeFilterLevel.Full) {                    
 currentPermissionSet = new PermissionSet(PermissionState.None);                
 currentPermissionSet.SetPermission(
      new SecurityPermission(
          SecurityPermissionFlag.SerializationFormatter));                    
}
                                    
try {
 if (currentPermissionSet != null)
  currentPermissionSet.PermitOnly();
                        
 // Deserialize Request - Stream to IMessage
 requestMsg = CoreChannel.DeserializeBinaryRequestMessage(
    objectUri, requestStream, _strictBinding, this.TypeFilterLevel);                    
}
finally {
 if (currentPermissionSet != null)
  CodeAccessPermission.RevertPermitOnly();
} 

As you can see if the type filter level is NOT set to TypeFilterLevel.Full then the currentPermissionSet will only contain one permission which is SecurityPermissionFlag.SerializationFormatter, what does this mean? this means first the CAS is enforced by calling .PermitOnly() then our Serialized Message gets Deserialized under the influence of CAS and then finally the CAS is lifted when the .RevertPermitOnly() is invoked.

So as long as the DeserializeBinaryRequestMessage does not violate CAS during the deserialization phase we can get around this security protection, but how can we get passed that?

That was exactly what forshaw did, he figured out, The PermitOnly security behaviors of Low Type Filter only apply when calling a method on a server object, not deserializing the return value.

Therefore if we could find somewhere in the server which calls back to a MBR object we control then we can force the server to deserialize an arbitrary object. This object can be used to mount the attack as the deserialization would not occur under the PermitOnly CAS grant and we can use the same Hashtable trick to capture a DirectoryInfo or FileInfo object.

But how can we find a method that calls back to us? Normally you would go about reverse engineering the server application to find such a method, but forshaw decided to come up with a universal technique that allows to make the server perform a callback to us, after reviewing multiple candidates he published the ILease::Register technique.

Basically, The InitializeLifetimeServer or GetLifetimeService methods return an MBR which implements the ILease interface. The ILease interface has a Register method which expects 1 argument that implements ISponsor, and the trick here is, invoking the ILease::Register with an argument that implements IConvertible instead of ISponsor, we cause an interesting behaviour to happen which forshaw has named “Coerce Arguments".

Forshaw proceeded by Digging further into default remoting implementation and noticed that if an argument being passed to a method isn’t directly of the required type the method StackBuilderSink::SyncProcessMessage will call Message::CoerceArgs to try the coerce the argument to the correct type. The fallback is to call Convert::ChangeType passing the needed type and the object passed from the client. To convert to the correct type the code will see if the passed object implements the IConvertible interface and call the ToType method on it. And that’s exactly what is happening in the ExploitRemotingService PoC published by James.


//    ExploitRemotingService
//    Copyright (C) 2019 James Forshaw
//

namespace ExploitRemotingService
{
    class SerializerRemoteClass : MarshalByRefObject, IRemoteClass, IEqualityComparer, IConvertible
    {
        private readonly CustomChannel _channel;
        private readonly ILease _lease;
        private readonly bool _useObjRef;
        private object _send_object;

        public SerializerRemoteClass(CustomChannel channel, ILease lease, bool useObjRef)
        {
            _channel = channel;
            _lease = lease;
            _useObjRef = useObjRef;
        }

     [..SNIP..]

        object IConvertible.ToType(Type conversionType, IFormatProvider provider)
        {
            return new DataSetMarshal(_send_object);
        }
    }
}

Now in order to begin this whole operation and “Set" a ISponsor object, we need to make an actual call to the server, however the Low Type Filter would stop us from passing an MBR ISponsor object as the top level object would be a MethodCall record type which would throw an exception when it is encountered during argument deserialization.

Forshaw figured there’s an easy way around this too, the framework provides us with a full serializable MethodCall class. Instead of using the MethodCall record type we can instead package up a serializable MethodCall (DIY) object as the top level object with all the data needed to make the call to Register. As the top level object is using a normal serialized object record type and not a MethodCall record type it’ll never trigger the type checking and we can call Register with our MBR ISponsor object and get around this limitation too, an absolute work of art from James, here is how he implemented it in the ExploitRemotingService project:

//    ExploitRemotingService
//    Copyright (C) 2019 James Forshaw
//

using System;
using System.Linq;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Serialization;

namespace ExploitRemotingService
{
    [Serializable]
    class MethodCallWrapper : ISerializable
    {
        private readonly string _uri;
        private readonly MethodBase _method;
        private readonly object[] _args;

        public MethodCallWrapper(string uri, MethodBase method, object[] args)
        {
            _uri = uri;
            _method = method;
            _args = args;
        }

        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.SetType(typeof(MethodCall));
            info.AddValue("__Uri", _uri);
            info.AddValue("__MethodName", _method.Name);
            info.AddValue("__MethodSignature", _method.GetParameters().Select(p => p.ParameterType).ToArray());
            info.AddValue("__Args", _args);
            info.AddValue("__TypeName", _method.DeclaringType.FullName);
            info.AddValue("__CallContext", string.Empty);
        }
    }
}

You might wonder if there’s another problem here, won’t deserializing the MBR cause the channel to be created and hit the PermitOnly CAS grant? Fortunately channel setup is deferred until a call is made on the object, therefore as long as no call is made to the MBR object during the deserialization process we’ll be out of the CAS grant and able to setup the channel when the Renewal method is called.

Following is how the final exploitation will look like, this diagram was originally made by james and I just remade it with a small edit.

Untitled

Making the exploit more difficult just for fun

For Ivanti I used a different technique when exploiting the Low Type Filter, usually you might be taking the approach of remotely causing a side load of the FakeAssembly, but I’ve decided to abuse the Write What Where MarshalByRefObj Primitive and write a web shell to one of the EPM Public IIS Application Pool directories, this is because comapred to the FakeAssembly, defender won’t catch this (which it will after this post, lol)

Proof of Concept

you can find the compiled version of the exploit at this github repo however for the time being I’ve made the exploit a bit difficult to utilize since this is a high value target and this might help in slowing APT Kiddies down

James Forshaw

Without the work of people such as james none of this would’ve been possible, james has published many more fantastic research over decades of grinding in the cybersecurity field, at the time of writing this post, james has published his second book titled “Windows Security Internals", I can’t recommend this book enough to all the windows security enthusiasts, make sure to check it out, it is an amazing read.

ZERO DAY INITIATIVE

If it wasn’t because of the talented team working at the Zero Day Initiative, Ivanti wouldn’t get this vulnerability report from me, shout out to all of you people working there to make the internet safer.

ZDI

Conclusion

Don’t use .NET Remoting, no matter how much you need it and if this post has gone over your head or you like to learn about .NET Exploitation, I have recently made my Advanced .NET Exploitation Training public, sign up and let me teach you how to pop shellz on .net targets.

AdvancedNetExploitationTraining

References