Talos Vulnerability Report


WAGO PFC200 iocheckd service "I/O-Check" external tool information exposure vulnerability

December 16, 2019
CVE Number



An exploitable information exposure vulnerability exists in the iocheckd service "I/O-Check" functionality of WAGO PFC 200. A specially crafted set of packets can cause an external tool to fail, resulting in uninitialized stack data to be copied to the response packet buffer. An attacker can send unauthenticated packets to trigger this vulnerability.

Tested Versions

WAGO PFC200 Firmware version 03.01.07(13) WAGO PFC200 Firmware version 03.00.39(12) WAGO PFC100 Firmware version 03.00.39(12)

Product URLs

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

CVSSv3 Score

5.3 CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N


CWE-201: Information Exposure Through Sent Data


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 function to get stdout output from tools that run as subprocesses on the system. When the subprocess fails, the stack buffer remains uninitialized. When responding to the messages outlined below, uninitialized stack data is copied to the response buffer and sent over the network. When used in conjunction with other vulnerabilities this information can be used to bypass security protections such as ASLR.

There are three different messages that call the vulnerable code that triggers this vulnerability as outlined in the PoC code below.

The type and amount of data exposed by this vulnerability varies greatly between firmware revisions. This is due to stack data being copied until the first NULL is encountered since sprintf is used.

[Annotated Disassembly / Decompilation output]

.text:000149E4                 PUSH            {R4-R11,LR}
.text:000149E8                 SUB             SP, SP, #0x2FC
.text:000149EC                 ADD             R3, SP, #0x320+var_228
.text:000149F0                 ADD             R5, SP, #0x320+var_268
.text:000149F4                 MOV             R1, #0x40 
.text:000149F8                 MOV             R11, R0 ; the first argument to this function is the response packet buffer
.text:000149FC                 MOV             R2, #0x800
.text:00014A00                 MOV             R0, R3
.text:00014A04                 MOVT            R2, #2
.text:00014A08                 STR             R3, [SP,#0x320+var_2FC]
.text:00014A0C                 BL              sub_12EA0
.text:00014A10                 MOV             R0, R5
.text:00014A14                 BL              sub_1340C
.text:00014A18                 ADD             R9, SP, #0x1B8
.text:00014A1C                 ADD             R0, SP, #0x38
.text:00014A20                 ADD             R4, SP, #0x278 ; stack buffer stored in R4
.text:00014A24                 BL              sub_13308
.text:00014A28                 ADD             R0, SP, #0x78
.text:00014A2C                 BL              sub_13378
.text:00014A30                 ADD             R6, SP, #0x138
.text:00014A34                 MOV             R0, R9
.text:00014A38                 ADD             R8, SP, #0x178
.text:00014A3C                 BL              sub_13308
.text:00014A40                 MOV             R1, #0x40 ; '@'
.text:00014A44                 MOV             R0, R4 ; stack buffer used to get output from an external tool
.text:00014A48                 MOV             R2, #0x207CC ; string in .rodata containing command to execute
.text:00014A50                 ADD             R10, SP, #0x1F8
.text:00014A54                 BL              sub_12EA0 ; runs external tool and on success copies output to the buffer in the first argument

sub_12EA0 executes the command and if it succeeds, reads stdout into the buffer in the first argument

.text:00012EA0 sub_12EA0                               
.text:00012EA0                 PUSH            {R4-R8,LR}
.text:00012EA4                 MOV             R6, R0
.text:00012EA8                 MOV             R0, R2  
.text:00012EAC                 MOV             R4, R2
.text:00012EB0                 MOV             R7, R1
.text:00012EB4                 BL              strlen
.text:00012EB8                 ADD             R0, R0, #0xE 
.text:00012EBC                 BL              malloc
.text:00012EC0                 MOV             R2, R4
.text:00012EC4                 MOV             R3, #0x628
.text:00012EC8                 MOV             R1, #0x20E8
.text:00012ECC                 MOVT            R3, #2
.text:00012ED0                 MOVT            R1, #2  
.text:00012ED4                 MOV             R5, R0
.text:00012ED8                 BL              sprintf
.text:00012EDC                 MOV             R0, R5  
.text:00012EE0                 MOV             R1, #0x2128C 
.text:00012EE8                 BL              popen
.text:00012EEC                 SUBS            R4, R0, #0 ; if popen failed:
.text:00012EF0                 MOVEQ           R4, #7 ; return value is set to 7 and the input buffer is unchanged
.text:00012EF4                 BEQ             loc_12F24 ; return 7
.text:00012EF8                 MOV             R2, R4  
.text:00012EFC                 MOV             R1, R7  
.text:00012F00                 MOV             R0, R6  
.text:00012F04                 BL              fgets
.text:00012F08                 CMP             R0, #0
.text:00012F0C                 MOV             R0, R4  
.text:00012F10                 MOVEQ           R4, #8
.text:00012F14                 MOVNE           R4, #0
.text:00012F18                 BL              pclose
.text:00012F1C                 CMN             R0, #1
.text:00012F20                 MOVEQ           R4, #9
.text:00012F24 loc_12F24                               ; CODE XREF: sub_12EA0+54↑j
.text:00012F24                 MOV             R0, R5  
.text:00012F28                 BL              free
.text:00012F2C                 MOV             R0, R4
.text:00012F30                 POP             {R4-R8,PC}

Back in the caller - the return value of sub_12EA0 is ignored and it is assumed that the stack buffer contains valid data

.text:00014A58                 MOV             R0, R6 ; return value is ignored

Later on in the function, the stack data is copied into the response buffer

.text:00014A88                 LDR             R3, [SP,#0x24]
.text:00014A8C                 ADD             R2, SP, #0x38
.text:00014A90                 STR             R10, [SP,#0x1C]
.text:00014A94                 STR             R9, [SP,#0x18]
.text:00014A98                 MOV             R0, R11 ; this is our response packet buffer
.text:00014A9C                 STR             R3, [SP,#4]
.text:00014AA0                 MOV             R1, #0xE98
.text:00014AA4                 ADD             R3, SP, #0x78
.text:00014AA8                 MOVT            R1, #2  ; format
.text:00014AAC                 STR             R8, [SP,#0x14]
.text:00014AB0                 STR             R7, [SP,#0x10]
.text:00014AB4                 STR             R6, [SP,#0xC]
.text:00014AB8                 STR             R5, [SP,#8]
.text:00014ABC                 STR             R4, [SP] ; this is the uninitialized stack buffer being copied into the response packet
.text:00014AC0                 BL              sprintf


This vulnerability could be mitigated by disabling the iocheckd service "I/O-Check" via the Web-based management web application.


2019-07-30 - Vendor disclosure
2019-09-06 - 30+ day follow up
2019-10-02 - 60+ day follow up; vendor acknowledged
2019-10-31 - Vendor passed to CERT@VDE for coordination; Talos extended public disclosure deadline
2019-12-16 - Public Release


Discovered by Kelly Leuschner of Cisco Talos