daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

pentesting-modbus.md (7830B)


      1 ---
      2 title: "502/tcp - Pentesting Modbus Protocol"
      3 section: "Network Services"
      4 sectionSlug: "network-services-pentesting"
      5 sourcePath: "src/network-services-pentesting/pentesting-modbus.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-modbus.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # 502/tcp - Pentesting Modbus Protocol
     14 
     15 ## Basic Information
     16 
     17 Modicon introduced **Modbus** in 1979 for communication among industrial devices. Current specifications use client/server terminology, although many tools and deployed serial systems still say master/slave.<sup>[[3]](#references)</sup>
     18 
     19 **Default Modbus/TCP port:** 502. The separate Modbus Security profile wraps Modbus in TLS with X.509 authentication and uses port 802; do not assume every Modbus deployment is necessarily plaintext.<sup>[[3]](#references)[[4]](#references)</sup>
     20 
     21 ```text
     22 PORT    STATE SERVICE
     23 502/tcp open  modbus
     24 ```
     25 
     26 **Modbus TCP** carries a **seven-byte MBAP header** followed by the Modbus PDU:<sup>[[3]](#references)</sup>
     27 
     28 - **Transaction ID**: matches requests and responses
     29 - **Protocol ID**: normally `0x0000`
     30 - **Length**: remaining bytes in the frame
     31 - **Unit ID**: important when a Modbus/TCP gateway forwards traffic to serial/RTU devices
     32 - **Function Code + Data**: the actual read/write/diagnostic operation
     33 
     34 The **Unit ID** is frequently ignored by native Modbus/TCP devices, but it becomes critical when the target is a **TCP-to-RTU gateway**. In that case, you usually need to enumerate multiple unit/slave IDs to reach the device behind the bridge.
     35 
     36 Legacy Modbus/TCP traffic on port 502 is typically **plaintext and unauthenticated**, so passive captures often reveal:
     37 
     38 - The in-use **unit/slave IDs**
     39 - Which **function codes** the process actually accepts
     40 - The **register map** being polled by the HMI/SCADA server
     41 - Whether writes are rare enough that replaying one stands out immediately
     42 
     43 ## Enumeration
     44 
     45 ### Automatic
     46 
     47 ```bash
     48 nmap -sV --script modbus-discover -p 502 <IP>
     49 nmap --script modbus-discover --script-args='modbus-discover.aggressive=true' -p 502 <IP>
     50 msf> use auxiliary/scanner/scada/modbusdetect
     51 msf> use auxiliary/scanner/scada/modbus_findunitid
     52 ```
     53 
     54 `modbus-discover` is useful because it tries to enumerate **legal slave IDs** and extract **device identification** data such as vendor and firmware strings.<sup>[[1]](#references)</sup>
     55 
     56 ### Passive Recon
     57 
     58 If you can sniff traffic, extract the parameters you need before sending anything intrusive:
     59 
     60 ```bash
     61 tshark -r modbus.pcap -Y modbus \
     62   -T fields -e ip.src -e ip.dst -e modbus.unit_id \
     63   -e modbus.func_code -e modbus.reference_num -e modbus.word_cnt
     64 ```
     65 
     66 Useful Wireshark display filters:
     67 
     68 ```bash
     69 modbus
     70 modbus.func_code == 3
     71 modbus.func_code == 16
     72 modbus.exception_code
     73 ```
     74 
     75 ### Manual Enumeration with PyModbus
     76 
     77 ```bash
     78 python3 -m pip install pymodbus
     79 ```
     80 
     81 ```python
     82 from pymodbus.client import ModbusTcpClient
     83 
     84 host = "10.10.10.10"
     85 unit = 1
     86 client = ModbusTcpClient(host, port=502)
     87 client.connect()
     88 
     89 print(client.read_coils(address=0, count=16, device_id=unit))
     90 print(client.read_holding_registers(address=0, count=10, device_id=unit))
     91 print(client.read_device_information(device_id=unit))
     92 
     93 client.close()
     94 ```
     95 
     96 The current PyModbus client uses zero-based protocol addresses and names the unit argument `device_id`; older releases used `slave` or `unit`, so match the example to the installed version.<sup>[[5]](#references)</sup>
     97 
     98 Notes:
     99 
    100 - **Addressing is a common pitfall**: many operators document holding register `400001`, while libraries expect the **zero-based offset** (`0`).
    101 - When crossing a **gateway**, keep the same TCP endpoint and iterate the **Unit ID**.
    102 - Some servers expose only a subset of function codes and return Modbus exception responses for unsupported ones.
    103 
    104 ### Offensive Tooling
    105 
    106 ```bash
    107 git clone https://github.com/TacticalGator/modbuster
    108 cd modbuster && pipx install .
    109 modbuster getfunctions <IP>
    110 modbuster read -s 1 <IP> 400001 10
    111 modbuster diag -s 1 <IP>
    112 ```
    113 
    114 Other useful frameworks/tools:<sup>[[6]](#references)</sup>
    115 
    116 - `smod`: function-code enumeration, UID brute force, fuzzing modules
    117 - `scapy.contrib.modbus`: packet crafting for unsupported or custom workflows
    118 
    119 ## Interesting Function Codes
    120 
    121 The following standardized function codes and the `0x2B/0x0E` device-identification MEI are defined in the Modbus application protocol specification.<sup>[[3]](#references)</sup>
    122 
    123 ### Read / Recon
    124 
    125 - `0x01` / `0x02`: read coils / discrete inputs
    126 - `0x03` / `0x04`: read holding / input registers
    127 - `0x11`: report server ID
    128 - `0x2B/0x0E`: **Read Device Identification** (vendor, product code, revision, optional objects)
    129 - `0x08`: diagnostics, especially interesting on serial devices and some gateways
    130 
    131 ### Write / Impact
    132 
    133 These are the first function codes to validate carefully during authorized testing because they can directly alter process state:
    134 
    135 - `0x05`: write single coil
    136 - `0x06`: write single holding register
    137 - `0x0F`: write multiple coils
    138 - `0x10`: write multiple holding registers
    139 - `0x16`: mask write register
    140 - `0x17`: read/write multiple registers
    141 
    142 ### Less Common but Worth Testing
    143 
    144 These are not universally implemented, but when present they are valuable because defenders often monitor them less:
    145 
    146 - `0x14`: read file record
    147 - `0x15`: write file record
    148 - `0x18`: read FIFO queue
    149 
    150 A practical workflow is:
    151 
    152 1. Find the valid **Unit ID**.
    153 2. Verify read-only access with `0x01`-`0x04`.
    154 3. Pull **device identification** with `0x2B/0x0E`.
    155 4. Enumerate supported function codes.
    156 5. Only then validate whether write functions are accepted.
    157 
    158 ## Scapy Packet Crafting
    159 
    160 Scapy ships Modbus packet definitions for common read/write primitives and **Read Device Identification**.<sup>[[6]](#references)</sup>
    161 
    162 ```python
    163 from scapy.contrib.modbus import ModbusADURequest, ModbusPDU2B0EReadDeviceIdentificationRequest
    164 from scapy.all import sr1
    165 
    166 pkt = ModbusADURequest(transId=1, unitId=1) / ModbusPDU2B0EReadDeviceIdentificationRequest()
    167 resp = sr1(pkt, timeout=2)
    168 if resp:
    169     resp.show()
    170 ```
    171 
    172 This is useful when you need to:
    173 
    174 - Replay a packet seen in a PCAP with only minimal edits
    175 - Test uncommon function codes not exposed cleanly by higher-level clients
    176 - Validate how a target reacts to malformed length/function combinations during lab work
    177 
    178 ## What Usually Leads to Impact
    179 
    180 In real assessments, attackers rarely start with memory corruption. The usual path is:
    181 
    182 - Sniff or enumerate the correct **Unit ID** and register ranges
    183 - Learn the meaning of the values from traffic patterns or vendor docs
    184 - Replay or slightly modify legitimate **write** requests
    185 - Abuse weak process assumptions such as "this register is only written by the HMI"
    186 
    187 Recent Modbus research and datasets keep emphasizing the same attacker behaviors: **query flooding, false data injection, brute-force writes, replay, and malformed length/frame handling**. Those are usually more realistic test cases than hunting for a single vendor-specific CVE.<sup>[[2]](#references)</sup>
    188 
    189 ## References
    190 
    191 - [1] [Nmap NSE: modbus-discover](https://nmap.org/nsedoc/scripts/modbus-discover.html)
    192 - [2] [MOSTO: A toolkit to facilitate security auditing of ICS devices using Modbus/TCP](https://zaguan.unizar.es/record/127649)
    193 - [3] [Modbus Organization - Specifications and implementation guides](https://www.modbus.org/modbus-specifications)
    194 - [4] [Modbus/TCP Security Protocol Specification](https://www.modbus.org/file/secure/modbussecurityprotocol.pdf)
    195 - [5] [PyModbus client documentation](https://pymodbus.readthedocs.io/en/latest/source/client.html)
    196 - [6] [Scapy Modbus contrib-layer documentation](https://scapy.readthedocs.io/en/latest/api/scapy.contrib.modbus.html)