Talos Vulnerability Report

TALOS-2019-0961

WAGO PFC200 iocheckd service "I/O-Check" cache DNS code execution vulnerability

March 9, 2019
CVE Number

CVE-2019-5166

Summary

An exploitable stack buffer overflow 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.

Tested Versions

WAGO PFC200 Firmware version 03.02.02(14)

Product URLs

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

CVSSv3 Score

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

CWE

CWE-120 - Buffer Copy without Checking Size of Input (‘Classic Buffer Overflow’)

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 DNS parameter can be used used to cause a buffer overflow. Since the attacker can control the data that is being copied onto the stack, this vulnerability results in code execution.

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.

At 0x1e3f0 the extracted dns value from the xml file is used as an argument to /etc/config-tools/edit_dns_server add=add dns-server-nr=1 dns-server-name=<contents of dns node> using sprintf(). The destination buffer sp+0x8 is overflowed with the call to sprintf() for any dns values that are greater than 1024-len("/etc/config-tools/edit_dns_server add=add dns-server-nr=1 dns-server-name=") in length. A dns value of length 0x3b9 will cause the service to crash.

Below is the vulnerable code path for this buffer overflow:

.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:0001EA78                 MOV             R1, R5 ; comparing to string `dns`
.text:0001EA7C                 LDR             R0, [cur_node,#8]
.text:0001EA80                 BL              xmlStrcmp
.text:0001EA84                 CMP             R0, #0
.text:0001EA88                 BNE             loc_1EAB8
.text:0001EA8C                 ADD             R10, R10, #1
.text:0001EA90                 LDR             R0, [SP,#0x868+var_850] ; array of pointers to dns entries on the heap
.text:0001EA94                 MOV             R3, R10,LSL#2
.text:0001EA98                 STR             R3, [SP,#0x868+ptr]
.text:0001EA9C                 MOV             R1, R3  ; size
.text:0001EAA0                 BL              realloc ; allocate space for another dns pointer 
.text:0001EAA4                 LDR             R3, [SP,#0x868+ptr]
.text:0001EAA8                 ADD             R3, R0, R3
.text:0001EAAC                 STR             R0, [SP,#0x868+var_850]
.text:0001EAB0                 STR             R7, [R3,#-4] ; store pointer to xml node containing dns entry in array of pointers to dns entries
...
.text:0001E7AC                 LDR             R3, [R4,#8]
.text:0001E7B0                 ADD             R5, R5, #1
.text:0001E7B4                 CMP             R3, #3 ; 
.text:0001E7B8                 BNE             loc_1E7A0
.text:0001E7BC                 LDM             R4, {R0,R1} ; R0 contains the pointer to dns entry string R1 contains value 1
.text:0001E7C0                 MOV             R2, R6  ; R6 contains string "add=add"
.text:0001E7C4                 BL              _callDnsTool
... ; Function _callDnsTool
.text:0001E3D0                 PUSH            {LR} 
.text:0001E3D4                 SUB             SP, SP, #0x400
.text:0001E3D8                 SUB             SP, SP, #0xC
.text:0001E3DC                 MOV             R3, R1 ; R1 now contains the value 1
.text:0001E3E0                 MOV             R1, #aEtcConfigTools_13 ; format string "/etc/config-tools/edit_dns_server %s dns-server-nr=%d  dns-server-name=%s"
.text:0001E3E8                 STR             R0, [SP,#0x410+var_410] ; storing dns entry value on the stack. R2 contains string "add=add" 
.text:0001E3EC                 ADD             R0, SP, #0x410+var_408 ; destination buffer 1024 bytes in length
.text:0001E3F0                 BL              sprintf ; buffer is overflowed here
.text:0001E3F4                 ADD             R0, SP, #0x410+var_408 
.text:0001E3F8                 BL              _callConfigTool
.text:0001E3FC                 ADD             SP, SP, #0x400
.text:0001E400                 ADD             SP, SP, #0xC
.text:0001E404                 POP             {PC}

Crash Information

Thread 2 "iocheckd" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 18357.18368]
0x58585858 in ?? ()
(gdb) i r
r0             0xa      10
r1             0x0      0
r2             0x1      1
r3             0x0      0
r4             0xb5b00998       3048212888
r5             0x1      1
r6             0x227c8  141256
r7             0xb5b00a80       3048213120
r8             0xb5b00988       3048212872
r9             0x0      0
r10            0x1      1
r11            0x0      0
r12            0xa      10
sp             0xb64bb5b0       0xb64bb5b0
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?)

Exploit Proof of Concept

<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
    <dns>OVERFLOW</dns>
</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