CVE-2019-5176, CVE-2019-5177, CVE-2019-5178, CVE-2019-5179, CVE-2019-5180, CVE-2019-5181, CVE-2019-5182
An exploitable stack buffer overflow vulnerability 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 stack buffer overflow, resulting in 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
8.8 - CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
CWE-120 - Buffer Copy without Checking Size of Input (‘Classic Buffer Overflow’)
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, several parameters can be used to cause buffer overflows. Since the attacker can control the data that is being copied onto the stack, this vulnerability results in code execution.
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 each node extracted from the iocheckCache.xml
file. The following example shows the vulnerable code path for the hostname
parameter. The code paths for other vulnerable nodes are similar to this one:
.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:0001EAF8 MOV R1, #0x26B4
.text:0001EAFC LDR R0, [cur_node,#8]
.text:0001EB00 MOVT R1, #2 ; Comparing to string `hostname`
.text:0001EB04 BL xmlStrcmp
.text:0001EB08 CMP R0, #0
.text:0001EB0C STREQ R7, [SP,#0x868+var_84C] ; store xml contents in `var_84c`
.text:0001EB10 BEQ loc_1E460
...
.text:0001E85C LDR R3, [SP,#0x868+var_84C] ; `var_84c` contains xml contents of `hostname` node
.text:0001E860 CMP R3, #0
.text:0001E864 BEQ loc_1E888
.text:0001E868 ADD R5, SP, #0x440 ; destination buffer located on the stack 1024 bytes in length
.text:0001E86C MOV R2, R3 ; src - contents of `hostname` node contains overflow
.text:0001E870 MOV R1, #aEtcConfigTools_14 ; format - `/etc/config-tools/change_hostname hostname=%s`
.text:0001E878 MOV R0, R5 ; dest
.text:0001E87C BL sprintf ; overflow stack buffer sp+0x440
When repeatedly exploiting this overflow, the shared memory used by iocheckd must be deleted between exploitation attempts: rm /dev/shm/wago_IO_Check
The following example shows the crash for the hostname
parameter. The crashes for other vulnerable nodes are similar to this one:
Thread 2 "iocheckd" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 28508.28566]
0x58585858 in ?? ()
(gdb) i r
r0 0xa 10
r1 0x0 0
r2 0x1 1
r3 0x0 0
r4 0x41414141 1094795585
r5 0x41414141 1094795585
r6 0x41414141 1094795585
r7 0x41414141 1094795585
r8 0x41414141 1094795585
r9 0x41414141 1094795585
r10 0x41414141 1094795585
r11 0x41414141 1094795585
r12 0xa 10
sp 0xb64cae18 0xb64cae18
lr 0x1e3a4 123812
pc 0x58585858 0x58585858
cpsr 0x60070010 1611071504
fpscr 0x0 0
(gdb) bt
#0 0x58585858 in ?? ()
#1 0x0001e3a4 in ?? ()
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
At 0x1e900 the extracted gateway value from the xml file is used as an argument to /etc/config-tools/config_default_gateway number=0 state=enabled value=<contents of gateway node>
using sprintf()
. The destination buffer sp+0x40
is overflowed with the call to sprintf()
for any gateway
values that are greater than 512-len("/etc/config-tools/config_default_gateway number=0 state=enabled value=")
in length. A gateway
value of length 0x7e2
will cause the service to crash.
<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
<gateway>OVERFLOW</gateway>
</network>
</settings>
At 0x1e8a8 the extracted domainname value from the xml file is used as an argument to /etc/config-tools/edit_dns_server domain-name=<contents of domainname node>
using sprintf()
. The destination buffer sp+0x440
is overflowed with the call to sprintf()
for any domainname
values that are greater than 1024-len("/etc/config-tools/edit_dns_server domain-name=")
in length. A domainname
value of length 0x3fa
will cause the service to crash.
<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
<domainname>OVERFLOW</domainname>
</network>
</settings>
At 0x1e87c the extracted hostname value from the xml file is used as an argument to /etc/config-tools/change_hostname hostname=<contents of hostname node>
using sprintf()
. The destination buffer sp+0x440
is overflowed with the call to sprintf()
for any hostname
values that are greater than 1024-len("/etc/config-tools/change_hostname hostname=")
in length. A hostname
value of length 0x3fd
will cause the service to crash.
<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
<hostname>OVERFLOW</hostname>
</network>
</settings>
At 0x1e840 the extracted ntp value from the xml file is used as an argument to /etc/config-tools/config_sntp time-server-1=<contents of ntp node>
using sprintf()
. The destination buffer sp+0x440
is overflowed with the call to sprintf()
for any ntp
values that are greater than 1024-len("/etc/config-tools/config_sntp time-server-1=")
in length. A ntp
value of length 0x3fc
will cause the service to crash.
<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
<ntp>OVERFLOW</ntp>
</network>
</settings>
At 0x1ea48 the extracted ip value from the xml file is used as an argument to /etc/config-tools/config_interfaces interface=X1 state=enabled ip-address=<contents of ip node>
using sprintf()
. The destination buffer sp+0x440
is overflowed with the call to sprintf()
for any ip
values that are greater than 1024-len("/etc/config-tools/config_interfaces interface=X1 state=enabled ip-address=")
in length. A ip
value of length 0x3da
will cause the service to crash.
<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
<interfaces>
<X1>
<ip>OVERFLOW</ip>
</X1>
</interfaces>
</network>
</settings>
At 0x1ea68 the extracted subnetmask value from the xml file is used as an argument to /etc/config-tools/config_interfaces interface=X1 state=enabled subnet-mask=<contents of subnetmask node>
using sprintf()
. The destination buffer sp+0x440
is overflowed with the call to sprintf()
for any subnetmask
values that are greater than 1024-len("/etc/config-tools/config_interfaces interface=X1 state=enabled subnet-mask=")
in length. A subnetmask
value of length 0x3d9
will cause the service to crash.
<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
<interfaces>
<X1>
<subnetmask>OVERFLOW</subnetmask>
</X1>
</interfaces>
</network>
</settings>
At 0x1ea28 the extracted type value from the xml file is used as an argument to /etc/config-tools/config_interfaces interface=X1 state=enabled config-type=<contents of type node>
using sprintf()
. The destination buffer sp+0x440
is overflowed with the call to sprintf()
for any type
values that are greater than 1024-len("/etc/config-tools/config_interfaces interface=X1 state=enabled config-type=")
in length. A type
value of length 0x3d9
will cause the service to crash.
<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
<interfaces>
<X1>
<type>OVERFLOW</type>
</X1>
</interfaces>
</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