Talos Vulnerability Report

TALOS-2019-0952

WAGO e!COCKPIT file path improper input validation vulnerability

March 9, 2020
CVE Number

CVE-2019-5159

Summary

An exploitable improper input validation vulnerability exists in the firmware update functionality of WAGO e!COCKPIT automation software. A specially crafted firmware update file can allow an attacker to write arbitrary files to arbitrary locations on WAGO controllers as a part of executing a firmware update, potentially resulting in code execution. An attacker can create a malicious firmware update package file using any zip utility. The user must initiate a firmware update through e!COCKPIT and choose the malicious wup file using the file browser to trigger the vulnerability.

Tested Versions

WAGO e!COCKPIT 1.6.0.7

Product URLs

https://www.wago.com/us/ecockpit-engineering-software

CVSSv3 Score

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

CWE

CWE-73 External Control of File Name or Path

Details

WAGO is a manufacturer of programmable automation controllers that are used in many industries including automotive, rail, power engineering, manufacturing, and building management. WAGO’s e!COCKPIT automation software provides an all in one utility that enables Programming, Visualization and Diagnostics for WAGO’s entire family of PLC’s.

The e!COCKPIT software supports updating WAGO controllers’ firmware via wup (WAGO update package). Typically these wup files are downloaded automatically by e!COCKPIT from WAGO servers. However, the user also has the option of choosing any file on disk to be used by the firmware update mechanism as long as it conforms to the expected data format of a wup file.

The wup file format consists of a zip file archive that is optionally encrypted with ZipCrypto. A hard-coded password is used to encrypt this zip archive, however an un-encrypted file is also accepted by the software. Each directory in the archive contains an xml file referred to as the control file. This control file specifies information about the firmware contained in the zip archive. It also lists additional files in within the zip archive that will be written to the device.

The Control File is expected to be called package-info.xml and exist at the top-level directory of the archive. Inside, it contains an XML node <AssociatedFiles> which contains any number of children nodes of the type <File>. These <File> nodes contain a Name property which is the name of the file within the archive. The <File> node also contains a TargetPath property which specifies the location on the controller to write the file.

While performing a firmware update, e!COCKPIT will loop through each <File> node and write it to the specified TargetPath location on the device via SFTP. The user of e!COCKPIT must enter administrator or root credentials for the controller in order to perform a firmware update. With a malicious wup file, the specified files will be written to the device with the level of credentials that the user has entered into e!COCKPIT. The files are written with global read permissions. If the file already exists on the device, it will retain it’s original file permissions.

This code snippet is from the .NET assembly Wago.FirmwareUpdate.Series750.dll version 1.2.0.0 FirmwareUpdateSequnce class of Wago.FirmwareUpdate.Series750 namespace. The function FirmwareUpdate.DownloadFirmware() loops over each file listed in the firmware control file and calls UploadFile to the specified TargetPath.

public bool DownloadFirmware(IUpdatePackage updatePackage, CancellationToken cancellationToken, CancellationToken waitCancellationToken)
{
    this.LogSequence(SequenceState.DownloadingFirmware);
    this._targetName = "";
    if (!cancellationToken.IsCancellationRequested)
    {
        using (this.Controller.UseConnection())
        {
            foreach (AssociatedFile associatedFile in updatePackage.GetAssociatedFiles())
            {
                using (Stream stream = this.CacheStream(associatedFile.Name, updatePackage.GetFileStream(associatedFile)))
                {
                    if (this.Controller.UploadFile(stream, associatedFile.TargetPath, cancellationToken))
                    {
                        this._targetName = associatedFile.TargetPath;
                    }
                    else
                    {
                        if (cancellationToken.IsCancellationRequested)
                        {
                            break;
                        }
                        return false;
                    }
                }
            }
        }
    }
    if (cancellationToken.IsCancellationRequested)
    {
        FwUpdate fwu = new FwUpdate(this.Controller, CancellationToken.None, this.Timing.RebootTimeout, this.Timing.MinimumPollInterval);
        this.CancelUpdateAndWait(fwu, false, waitCancellationToken, false);
        cancellationToken.ThrowIfCancellationRequested();
    }
    return true;
}

This code snippet is from the .NET assembly Wago.FirmwareUpdate.Series750.SshNet.dll version 1.2.0.0 SshNetController class of Wago.FirmwareUpdate.Series750 namespace. The function SshNetController.UploadFile() loops over each file listed in the firmware control file and calls UploadFile to the specified TargetPath.

public virtual bool UploadFile(Stream source, string targetpath, CancellationToken cancellationToken)
{
    for (int i = 0; i <= 3; i++)
    {
        using (SftpClient sftpClient = new SftpClient(this._host, this._username, this._password))
        {
            try
            {
                sftpClient.Connect();
                cancellationToken.ThrowIfCancellationRequested();
                int fileSize = sftpClient.GetFileSize(targetpath);
                if ((long)fileSize == source.Length)
                {
                    return true;
                }
                if (fileSize > 0)
                {
                    bool flag = false;
                    SftpFileStream sftpFileStream = sftpClient.Open(targetpath, FileMode.Append, FileAccess.Write);
                    source.Seek((long)fileSize, SeekOrigin.Begin);
                    byte[] buffer = new byte[1048576];
                    for (;;)
                    {
                        int num = source.Read(buffer, 0, 1048576);
                        if (num == 0)
                        {
                            break;
                        }
                        IAsyncResult asyncResult = sftpFileStream.BeginWrite(buffer, 0, num, null, null);

Mitigation

Users should execute firmware updates via e!COCKPIT using administrator credentials rather than root for the controller. This will restrict the writable locations on the device to only those writable by the admin user.

Timeline

2019-10-31 - Vendor Disclosure
2019-10-31 - Vendor acknowledged and passed to CERT@VDE for coordination/handling
2020-01-28 - Talos discussion with vendor; disclosure deadline extended
2020-03-09 - Public Release

Credit

Discovered by Kelly Leuschner of Cisco Talos.