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.
Nothing to be scared about folks, just another CVSS 9.8 0day disclosed 0days ago that's gonna get code execution in 0 seconds (3 seconds to be more accurate), no limitation, no authentication, no shit, just straight up remote code execution#IvantiForLife pic.twitter.com/KrCTnFGgEB
— SinSinology (@SinSinology) May 2, 2024
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
“The Real” Ivanti EPM Insecure Deserialization (CVE-2024-29847)
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.
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
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
- Classes Deriving from MarshalByRefObject (MBR) and Serializable:
DirectoryInfo
andFileInfo
are mentioned as examples of classes that meet these criteria. These classes can be serialized and deserialized across a network and are derived fromMarshalByRefObject
. - Crafted Hashtable with MBR Instance of IEqualityComparer: By deserializing an instance of one of these special classes (
DirectoryInfo
orFileInfo
) inside a carefully craftedHashtable
, along with aMarshalByRefObject
instance ofIEqualityComparer
, the server can be tricked into passing back the instance. - Marshalling by Reference (MBR): As the object is passed back over a remoting channel, the
DirectoryInfo
orFileInfo
objects are marshalled by reference and remain on the server. - Arbitrary File Operations: With the
DirectoryInfo
orFileInfo
objects now stuck inside the server, methods can be called on these objects to perform arbitrary file operations, such as reading and writing files. - 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.
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
Exploiting .NET Remoting (Low Type Filter)
When the Low Type Filter is enabled the following restrictions are applied:
- Object types derived from
MarshalByRefObject
,DelegateSerializationHolder
,ObjRef
,IEnvoyInfo
andISponsor
can not be deserialized. - All objects which are deserialized must not Demand any CAS permission other than
SerializationFormatter
permission.
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
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.
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.
This new book has finally arrived. Thank's to @nostarch as well as @billpollock for making it happen as well as @Lee_Holmes as my tech reviewer. pic.twitter.com/BMUfZGrU2F
— James Forshaw (@tiraniddo) March 28, 2024
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.
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.
References
- https://forums.ivanti.com/s/article/Security-Advisory-EPM-September-2024-for-EPM-2024-and-EPM-2022?language=en_US
- https://www.zerodayinitiative.com/advisories/ZDI-24-1223/
- https://media.blackhat.com/bh-us-12/Briefings/Forshaw/BH_US_12_Forshaw_Are_You_My_Type_WP.pdf
- https://www.tiraniddo.dev/2014/11/stupid-is-as-stupid-does-when-it-comes.html
- https://www.tiraniddo.dev/2019/10/bypassing-low-type-filter-in-net.html
- https://github.com/tyranid/ExploitRemotingService
- https://research.nccgroup.com/2019/03/19/finding-and-exploiting-net-remoting-over-http-using-deserialisation/
- https://code-white.com/blog/2022-01-dotnet-remoting-revisited/