CVE-2019-5184
An exploitable double free vulnerability exists in the iocheckd service “I/O-Check” functionality of WAGO PFC 200. A specially crafted XML cache file written to a specific location on the device can cause a heap pointer to be freed twice, resulting in a denial of service and potentially code execution. An attacker can send a specially crafted packet to trigger the parsing of this cache file.
WAGO PFC200 Firmware version 03.02.02(14)
https://www.wago.com/us/pfc200
7.0 - CVSS:3.0/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE-415: Double Free
The WAGO PFC200 Controller is one of WAGO’s programmable automation controllers that boasts high cybersecurity standards by including VPN, SSL and firewall software. WAGO controllers are used in many industries including automotive, rail, power engineering, manufacturing, and building management. The WAGO PFC200 Controller communicates via both standard and custom protocols.
The iocheckd service “I/O-Check” implements a custom configuration protocol used by WAGO controllers. The iocheckd service “I/O-Check” functionality of WAGO PFC 200 uses a file-backed cache to perform some network configuration functionality. The file used for the cache is stored at /tmp/iocheckCache.xml
which is globally writeable. During parsing of the iocheckCache.xml
file the heap buffer used to store the values for gateway
is freed n times (as many gateway elements present in iocheckCache.xml
). This causes the iocheckd program to crash, without setting a status variable within the shared memory used by the process. Depending on the memory allocator used, this vulnerability could potentially result in code execution. Exploiting this vulnerability in its basic form results in a denial of service for the following iocheckd messages. These messages will respond with an error rather than carrying out the intended functionality after exploitation:
To restore normal functionality of iocheckd, the user must reboot the device or log in as the root user and delete /dev/shm/wago_IO_Check
.
In order to exercise this vulnerability, the attacker must place the malicious xml file at /tmp/iocheckCache.xml
. All users have write access for /tmp
and can write this file. The vulnerability can be triggered by sending the BC_SaveParameter message which will cause the iocheckCache.xml
file to be parsed.
The vulnerable code exists for the gateway
node extracted from the iocheckCache.xml
file. The following example shows the vulnerable code path:
.text:0001E478 MOV R0, cur_node
.text:0001E47C BL xmlNodeGetContent
.text:0001E480 MOV R1, R4
.text:0001E484 MOV R7, R0 ; R7 contains xml node contents
...
.text:0001EB34 LDR R0, [cur_node,#8]
.text:0001EB38 MOVT R1, #2 ; comparing to string `gateway`
.text:0001EB3C BL xmlStrcmp
.text:0001EB40 CMP R0, #0
.text:0001EB44 BNE loc_1E460
.text:0001EB48 LDR R3, [SP,#0xC] ; count of gateway nodes
.text:0001EB4C LDR R0, [SP,#8] ; array of pointers to gateway entries on the heap
.text:0001EB50 ADD R3, R3, #1 ; increment
.text:0001EB54 STR R3, [SP,#0xC]
.text:0001EB58 MOV R3, R3,LSL#2
.text:0001EB5C STR R3, [SP,#0x14]
.text:0001EB60 MOV R1, R3 ; size = sp+0xc * 4
.text:0001EB64 BL realloc ; create space for another gateway entry
.text:0001EB68 LDR R3, [SP,#0x14]
.text:0001EB6C ADD R3, R0, R3
.text:0001EB70 STR R0, [SP,#8] ; update local void* variable to reallocated chunk
...
.text:0001E8B4 LDR R3, [SP,#8] ; if the gateway entry array is not NULL
.text:0001E8B8 CMP R3, #0
.text:0001E8BC BEQ loc_1E5F8
.text:0001E8C0 LDR R2, [SP,#0xC] ; if the count is greater than 0
.text:0001E8C4 CMP R2, #0
.text:0001E8C8 BEQ loc_1E5F8
... ; enter gateway value processing loop.
.text:0001E904 MOV R0, R6 ; cmd
.text:0001E908 BL _callConfigTool
.text:0001E90C LDR R0, [SP,#8] ; free gateway entry array
.text:0001E910 BL free
.text:0001E914 LDR R3, [SP,#0xC]
.text:0001E918 CMP i, R3 ; if i == gateway entry count
.text:0001E91C BEQ loc_1E5F8 ; exit, otherwise continue looping. Will free the same pointer on next loop iteration
Output from strace:
[pid 28519] open("/dev/tty", O_RDWR|O_NOCTTY|O_NONBLOCK) = -1 ENXIO (No such device or address)
[pid 28519] writev(2, [{iov_base="*** Error in `", iov_len=14}, {iov_base="iocheckd", iov_len=8}, {iov_base="': ", iov_len=3}, {iov_base="double free or corruption (fastt"..., iov_len=35}, {iov_base=": 0x", iov_len=4}, {iov_base="b5c0b7c8", iov_len=8}, {iov_base=" ***\n", iov_len=5}], 7) = 77
[pid 28519] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb6f9a000
[pid 28519] futex(0xb6b945a4, FUTEX_WAKE_PRIVATE, 2147483647) = 0
[pid 28519] rt_sigprocmask(SIG_UNBLOCK, [ABRT], NULL, 8) = 0
[pid 28519] write(1, "config-tool failed: \"/etc/config"..., 324) = 324
[pid 28519] tgkill(28505, 28519, SIGABRT) = 0
[pid 28519] --- SIGABRT {si_signo=SIGABRT, si_code=SI_TKILL, si_pid=28505, si_uid=0} ---
[pid 28519] fstat64(2, {st_mode=S_IFREG|0644, st_size=414, ...}) = 0
[pid 28519] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb6f99000
[pid 28519] shutdown(5, SHUT_RD) = 0
[pid 28519] shutdown(6, SHUT_WR) = 0
[pid 28519] close(5) = 0
[pid 28519] close(6) = 0
[pid 28519] rt_sigaction(SIGABRT, {sa_handler=SIG_DFL, sa_mask=[ABRT], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0xb6a86990}, {sa_handler=0x12cd4, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART|SA_SIGINFO, sa_restorer=0xb6a869a0}, 8) = 0
[pid 28519] kill(28505, SIGABRT) = 0
[pid 28519] rt_sigreturn({mask=[]}) = 0
[pid 28519] --- SIGABRT {si_signo=SIGABRT, si_code=SI_USER, si_pid=28505, si_uid=0} ---
[pid 28505] <... nanosleep resumed> <unfinished ...>) = ?
[pid 28519] +++ killed by SIGABRT +++
+++ killed by SIGABRT +++
<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
<gateway>192.168.1.1</gateway>
<gateway>192.168.1.2</gateway>
</network>
</settings>
This vulnerability could be mitigated by disabling iocheckd caching
#Author : Kelly Leuschner, Cisco Talos
import argparse, socket
if __name__=="__main__":
parser = argparse.ArgumentParser(description="Disable iocheckd Caching on WAGO PFC200 via iocheckd:RC_WriteRegister")
parser.add_argument('ipAddr', help='ip address of PLC')
parser.add_argument('port', type = int, help='Service protocol port number (6626)')
args = parser.parse_args()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((args.ipAddr, args.port))
print("Sending RC_WriteRegister message to disable iocheckd caching")
s.send(b'\x88\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x04\x00\x00\x00\x00\n\x00\x0b\x00\x00\x00')
s.recv(1024)
s.close()
2019-12-05 - Vendor Disclosure
2020-01-28 - Talos discussion about vulnerabilities with Vendor; disclosure deadline extended
2020-03-09 - Public Release
Discovered by Kelly Leuschner of Cisco Talos