Report and research by Kelly Leuschner.

WAGO makes several programmable automation controllers that are used in many industries including automotive, rail, power engineering, manufacturing and building management. Cisco Talos discovered 41 vulnerabilities in their PFC200 and PFC100 controllers. In accordance with our coordinated disclosure policy, Cisco Talos worked with WAGO to ensure that these issues were resolved and that a firmware update is available for affected customers.

Since a patch has been available to affected customers for some time, we wanted to take this opportunity to discuss several attack chains that exploit WAGO’s cloud connectivity client known as “dataagent” to gain root access to the device. You can also catch a technical presentation of these vulnerabilities at the virtual CS3Sthlm conference on Oct. 22, 2020.

WAGO provides a cloud connectivity feature for users to access remote telemetry from their devices and even issue firmware updates remotely. Cloud connectivity provides an interesting attack vector, where the attack originates from a trusted cloud provider but the cloud instance itself is attacker-controlled. The scenario we will dive into today is one where the attacker has access to legitimate cloud infrastructure and can abuse WAGO’s custom protocol to gain root privileges on the device.

We’ll first dive into the technical details of each of the vulnerabilities themselves. Then we’ll discuss how these vulnerabilities can be combined in two distinct attack chains that result in the ability to gain root privileges on the device.

Recon

Understanding the vulnerabilities

Firmware update command remote code execution (TALOS-2019-0954/CVE-2019-5161)

One of the features enabled by dataagent is the ability to update firmware on devices from the cloud. This feature immediately stood out to us, as it would involve multiple files and eventually execute system commands on the device to perform the update. Eventually, we discovered multiple vulnerabilities in the handling of the firmware update command of the dataagent service. This service handles communication with various cloud platforms that support the MQ Telemetry Transport (MQTT) protocol.

The firmware update functionality is designed such that the cloud application will send a JSON-encoded message via the MQTT protocol. Here’s an example of the command to begin a firmware update:

The firmware update process is handled using a state machine. The first state in the firmware update process is initiated upon receipt of the firmware update message from the cloud CommandId: 506. The protocol-parsing code within dataagent extracts each of the values contained in the firmware update JSON message above. Each of those is passed to an external utility called fwupdate in the form -i "<keyname>=<user-supplied-value>, resulting in the full command being executed by system().

Each of the <cloud-supplied-value> parameters is vulnerable to operating system command injection because the sudo fwupdate command is executed via a call to system() without any input sanitization. These vulnerabilities are identified as TALOS-2019-0948 through TALOS-2019-0950 (CVE-2019-5155 - CVE-2019-5157).

We’ve got OS command injection. So, what can we do with it? Turns out not much, as the dataagent service is running as a non-root user named iot. This won’t get us root access from the cloud like we’re looking for.

The next stage that the firmware update state machine enters is called DOWNLOAD_FIRMWARE. In this state, the dataagent process uses FirmwareStorageAccount, FirmwareStoragePath and FirmwareControlFile concatenated together to form a URL for a file that will be downloaded via curl to the /tmp directory of the device. From the firmware update message above, this would download the file hosted at https://attackerstorage.blob.core.windows.net/test/poc_controlFile.xml. After this file is downloaded, libxml2 parses it and follows a similar format to the one below.

The code snippet below shows some of the validation performed on the contents of the FirmwareControlFile. The code is building up a list of additional files to be downloaded. This shows that the Type attribute of the File node must be equal to raucb to be added to the download list.

Next, the additional files specified in the FirmwareControlFile will be downloaded from the URL specified by FirmwareStorageAccount and FirmwareStoragePath from the original Firmware Update command and the Name attribute from the File node within the FirmwareControlFile. So, in our example, https://attackerstorage.blob.core.windows.net/test/update_V030039_12_r38974.raucb will be downloaded via curl to the location specified by the TargetPath attribute within the FirmwareControlFile.

Now we can download any number of additional files and specify the path on the device where they should be stored. There is no limit on the TargetPath location or the number of files to be included if the Type attribute is raucb. But the Type attribute doesn’t have to match the file extension within the Name or TargetPath. However, we are still limited to locations on the device where the IoT user can write, which is limited to /tmp.

Now that all the required files have been downloaded, the next state in the firmware update process is START. In this state, the external utility fwupdate is executed. This time with the parameter start. The full command executed is system(‘sudo fwupdate start’). The fwupdate utility is a wrapper for other fwupdate* utilities. The command fwupdate start eventually leads to a call to fwupdate_background_service start-update. Below is a code snippet from that shell function:

Here, we see that if a firmware update preinstall hook exists, it will be executed. As a reminder, this fwupdate_background_service script runs with sudo privileges, so the preinstall hook will also run as the root user. We can see the variable defined below.

What we have now is the ability to write any file to /tmp on the device, and if we write a file to the location /tmp/fwupdate/fwupdate_hook_preinstall.sh it will be run as root.


Improper host validation (TALOS-2019-0953/CVE-2019-5160)

Now that we have found vulnerabilities in the firmware update command supported by dataagent, let’s see if there are any constraints regarding which cloud platform may send that command. The dataagent service can be configured to connect to multiple cloud infrastructure vendors including WAGO Cloud, Microsoft Azure, IBM Cloud, Amazon Web Service and SAP IoT Services.
Here, we see that if a firmware update preinstall hook exists, it will be executed. As a reminder, this fwupdate_background_service script runs with sudo privileges, so the preinstall hook will also run as the root user. We can see the variable defined below.

The code snippet above shows that the firmware update command is processed only if the dataagent service is configured to use the WAGO Cloud cloud type. Otherwise, the message is discarded. When configured for WAGO Cloud, the service uses the Azure IoT SDK to communicate with an Azure IoT Hub instance which is running a custom WAGO Cloud application. This application is a subscription service offered by WAGO which is hosted at wagocloud.azure-devices.net.

Dataagent configuration is performed through the Web-Based Management (WBM) web application provided by the device. When selecting the cloud platform as WAGO Cloud, the web application does not perform any validation on the provided hostname. All Azure IoT Hub instances authenticate with a wildcard certificate — ensuring that the hostname is *.azure-devices.net, which is not a problem itself. However, it is a problem with WAGO’s implementation of its firmware update command. WAGO intended to limit the firmware update functionality so that the feature would only be available to the WAGO cloud application. The problem is any Azure IoT Hub instance will have a valid certificate to authenticate with our device which is an Azure IoT Hub client. This means that any Azure IoT Hub instance can use WAGO’s firmware update functionality. The lack of validation of the hostname of the configured WAGO cloud application is TALOS-2019-0953/CVE-2019-5160. This vulnerability enables an attacker to configure the dataagent service to connect to an attacker-controlled Azure IoT Hub instance.


WBM information exposure of admin credentials (TALOS-2019-0923/CVE-2019-5134 & TALOS-2019-0924/CVE-2019-5135)

WBM admin credentials are needed to configure dataagent with an attacker-controlled hostname. The WBM users are isolated from the Linux system users as a security measure on the device. With two vulnerabilities in the PasswordCorrect function from login.php we could leak the hashed user credentials. Below is the PasswordCorrect function:

The password file is separate from the Linux users and is stored in /etc/lighttpd/lighttpd-htpassword.user. The regular expressions used in the PasswordCorrect function are unanchored which allows user input to bypass the regular expression filter (TALOS-2019-0923/CVE-2019-5134). For example, a $username value of (?x)admin.... will match the line in the password file containing admin:$6$. The impact of bypassing the regular expression filter is that we are now able to test user input against the password hash contained in the password file. If that first check for the username within the password file line passes, the PHP crypt() function will be called. The crypt() function introduces a noticeable delay into the response, which allows us to perform a timing attack (TALOS-2019-0924/CVE-2019-5135). With our bypass username value of (?x)admin.... we can append ASCII characters to that value and measure the delay in the response to determine if the crypt() function was called or not. This will reveal which characters are contained in the password hash.


iocheckCache.xml command injection and code execution (TALOS-2019-0961/CVE-2019-5166-TALOS-2019-0963/CVE-2019-5182)


Previously, we discussed that we could specify a file to be downloaded through the dataagent firmware update control file to the device as well as the file name and path. However, we are limited to the /tmp directory where the limited IoT user can write.

Lucky for us, it turns out that a separate service on the device called iocheckd uses a file called iocheckCache.xml in the /tmp directory to store cache files for some network settings that require multiple messages to properly configure. Typically, a message sent to the iocheckd service will generate /tmp/iocheckCache.xml containing new parameters like hostname or IP address. Next, a save parameter message will read the contents of iocheckCache.xml and execute system commands to modify the settings of the device. Because there are many possible XML nodes that share similar vulnerabilities, there are a total of 18 vulnerabilities relating to iocheckd’s parsing of this cache file. This post will focus on OS command injection (TALOS-2019-0962/CVE-2019-5167-CVE-2019-5175). Below is the vulnerable piece of code:

OS command injection is possible because the xmlHostnameNodeValue from the XML file is copied directly to the command buffer using sprintf(). Without any validation, that string is passed to a call to system(). By adding a shell expansion character, we can execute additional OS commands. The iocheckd service is running as the root user, so our OS commands will also be executed as root.

Exploitation

We’ll walk through two exploit chains, each executing commands from the cloud resulting in code execution in the root context on the device. The first will be using the firmware update preinstall hook, and the second will involve using the firmware update mechanism to place an iocheckCache.xml file on the device which will then be leveraged to gain command execution as the root user. These exploitation examples were performed on version 13 of the PFC 200 firmware which can be found here.

Connect to attacker-controlled cloud

In each of our attack scenarios, we need to configure the device so that it connects to an Azure IoT Hub under our control. Using TALOS-2019-0923 andTALOS-2019-0924 we will leak the WBM admin user-hashed credentials and then crack the password using hashcat. This is because we need to have WBM admin privileges to configure the dataagent service on the device. As mentioned above, we will use a timing attack against the PHP crypt() function to extract the web admin password hash. The leaked web admin user password hash is below.

Now that we’ve got the hashed password, we can crack it using hashcat.

As shown above, the WBM admin user’s password is T@l0s. Using TALOS-2019-0953, we can configure the dataagent service to connect to our IoT Hub instance attackerIotHub.azure-devices.net with the following post request:

Attack chain 1: Preinstall hook


Now that the device is configured to connect to our attacker-controlled cloud, we can exploit the firmware update preinstall hook vulnerability (TALOS-2019-0954) to execute OS commands as the root user. To exploit this vulnerability, the attacker must create two files: the XML control file and a shell script that will run as root on the system. These two files must be hosted on a server that the device can download via curl. For this example, we will host the files on a Microsoft Azure storage account. Here are the contents of the XML control file:

Recall that each file node with the Type attribute rauc will be written to the device in the location specified by the TargetPath attribute. The file “raucb.txt” is specified because a file must exist in the path /tmp/fwupdate with the file extension .raucb for the preinstall hook to be executed. The contents of the .raucb file do not matter for exploiting this vulnerability. The file rootShell.sh contains our script that will be run as root on the device:

This script will launch telnetd, which is a TELNET protocol server. This is the telnetd utility provided by busybox. The parameter -l specifies an executable to be run when a new connection is made. The purpose of this is to provide login functionality, but we will use this option to give us a root shell instead of logging in. The -p parameter specifies the port for telnetd to listen.

Microsoft provides a helpful extension to Visual Studio Code which allows users to monitor messages from IoT devices. When connecting to Azure IoT Hub for the first time, the device sends these two messages indicating the protocol versions the device supports:

To send messages to our IoT Hub device, we will use Microsoft Azure’s azure-sdk-for-js. The attack script mimics the behavior of WAGO’s cloud application by using the identical message sequences and structure. This script will send a CloudHello message to negotiate the protocol version with the device. Then, upon receipt of a Heartbeat message, this script will send the firmware update command which will exploit the device. The firmware update will fail, but only after executing the preinstall hook script. After the script is executed, we can connect to it using any telnet client on p:

Attack chain 2: IocheckCache.xml

We can use the firmware update command to place an iocheckCache file in the /tmp directory, and then send a message to the iocheckd service to exploit (TALOS-2019-0962). Similarly, we will need two files to exploit this vulnerability: the XML control file and the iocheckCache XML file. Below are the contents of the XML control file:

For this exploit, we will use the hostname node within the iocheckCache file to inject an OS system command:

Again, we are using the telnetd server to provide a root shell through port 1234. We will use the same attack script from before, only this time we will specify the control file above. In a similar fashion, the firmware update will fail since we did not provide a valid firmware image within the command. However, the file /tmp/iocheckCache.xml remains on the device:

Later, we can send a separate message to the iocheckd service running on the PLC on port 6626, which will trigger parsing of the iocheckCache.xml and execute our injected command. After causing iocheckd to parse the cache file, we can connect to it using any telnet client on port 1234:

Conclusion

This post highlighted an interesting attack vector, using attacker-controlled cloud infrastructure to impersonate a vendor cloud application. Although Azure IoT Hub does provide encryption and authentication, WAGO did not implement additional validation of the cloud application itself. Cloud connectivity is increasing in Industrial applications and manufacturers should keep in mind that data originating from the cloud should be treated as untrusted data. When chained together, these vulnerabilities provide an attacker with root remote access. An attacker could then use that access to disrupt any of the critical processes that the WAGO PFC handles.

Coverage

The following SNORTⓇ rules will detect exploitation attempts. Note that additional rules may be released at a future date and current rules are subject to change pending additional vulnerability information. For the most current rule information, please refer to your Firepower Management Center or Snort.org.

Snort Rules: 52023, 52131, 52237, 52238, 52407, 52408, 52412