# 502/tcp - Pentesting Modbus Protocol

{{#include ../banners/hacktricks-training.md}}

Basic Information

In 1979, the Modbus Protocol was developed by Modicon, serving as a messaging structure. Its primary use involves facilitating communication between intelligent devices, operating under a master-slave/client-server model. This protocol plays a crucial role in enabling devices to exchange data efficiently.

Default port: 502

PORT    STATE SERVICE
502/tcp open  modbus

Modbus TCP usually carries a 7-byte MBAP header followed by the Modbus PDU:

  • Transaction ID: matches requests and responses
  • Protocol ID: normally 0x0000
  • Length: remaining bytes in the frame
  • Unit ID: important when a Modbus/TCP gateway forwards traffic to serial/RTU devices
  • Function Code + Data: the actual read/write/diagnostic operation

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.

Modbus traffic is typically plaintext and unauthenticated, so passive captures often reveal:

  • The in-use unit/slave IDs
  • Which function codes the process actually accepts
  • The register map being polled by the HMI/SCADA server
  • Whether writes are rare enough that replaying one stands out immediately

Enumeration

Automatic

nmap -sV --script modbus-discover -p 502 <IP>
nmap --script modbus-discover --script-args='modbus-discover.aggressive=true' -p 502 <IP>
msf> use auxiliary/scanner/scada/modbusdetect
msf> use auxiliary/scanner/scada/modbus_findunitid

modbus-discover is useful because it tries to enumerate legal slave IDs and extract device identification data such as vendor and firmware strings.

Passive Recon

If you can sniff traffic, extract the parameters you need before sending anything intrusive:

tshark -r modbus.pcap -Y modbus \
  -T fields -e ip.src -e ip.dst -e modbus.unit_id \
  -e modbus.func_code -e modbus.reference_num -e modbus.word_cnt

Useful Wireshark display filters:

modbus
modbus.func_code == 3
modbus.func_code == 16
modbus.exception_code

Manual Enumeration with PyModbus

python3 -m pip install pymodbus
from pymodbus.client import ModbusTcpClient

host = "10.10.10.10"
unit = 1
client = ModbusTcpClient(host, port=502)
client.connect()

print(client.read_coils(address=0, count=16, device_id=unit))
print(client.read_holding_registers(address=0, count=10, device_id=unit))
print(client.read_device_information(device_id=unit))

client.close()

Notes:

  • Addressing is a common pitfall: many operators document holding register 400001, while libraries expect the zero-based offset (0).
  • When crossing a gateway, keep the same TCP endpoint and iterate the Unit ID.
  • Some servers expose only a subset of function codes and return Modbus exception responses for unsupported ones.

Offensive Tooling

git clone https://github.com/TacticalGator/modbuster
cd modbuster && pipx install .
modbuster getfunctions <IP>
modbuster read -s 1 <IP> 400001 10
modbuster diag -s 1 <IP>

Other useful frameworks/tools:

  • smod: function-code enumeration, UID brute force, fuzzing modules
  • scapy.contrib.modbus: packet crafting for unsupported or custom workflows

Interesting Function Codes

Read / Recon

  • 0x01 / 0x02: read coils / discrete inputs
  • 0x03 / 0x04: read holding / input registers
  • 0x11: report server ID
  • 0x2B/0x0E: Read Device Identification (vendor, product code, revision, optional objects)
  • 0x08: diagnostics, especially interesting on serial devices and some gateways

Write / Impact

These are the first function codes to validate carefully during authorized testing because they can directly alter process state:

  • 0x05: write single coil
  • 0x06: write single holding register
  • 0x0F: write multiple coils
  • 0x10: write multiple holding registers
  • 0x16: mask write register
  • 0x17: read/write multiple registers

Less Common but Worth Testing

These are not universally implemented, but when present they are valuable because defenders often monitor them less:

  • 0x14: read file record
  • 0x15: write file record
  • 0x18: read FIFO queue

A practical workflow is:

  1. Find the valid Unit ID.
  2. Verify read-only access with 0x01-0x04.
  3. Pull device identification with 0x2B/0x0E.
  4. Enumerate supported function codes.
  5. Only then validate whether write functions are accepted.

Scapy Packet Crafting

Scapy already ships Modbus packet definitions for the common read/write primitives and for Read Device Identification.

from scapy.contrib.modbus import ModbusADURequest, ModbusPDU2B0EReadDeviceIdentificationRequest
from scapy.all import sr1

pkt = ModbusADURequest(transId=1, unitId=1) / ModbusPDU2B0EReadDeviceIdentificationRequest()
resp = sr1(pkt, timeout=2)
if resp:
    resp.show()

This is useful when you need to:

  • Replay a packet seen in a PCAP with only minimal edits
  • Test uncommon function codes not exposed cleanly by higher-level clients
  • Validate how a target reacts to malformed length/function combinations during lab work

What Usually Leads to Impact

In real assessments, attackers rarely start with memory corruption. The usual path is:

  • Sniff or enumerate the correct Unit ID and register ranges
  • Learn the meaning of the values from traffic patterns or vendor docs
  • Replay or slightly modify legitimate write requests
  • Abuse weak process assumptions such as "this register is only written by the HMI"

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.

References

{{#include ../banners/hacktricks-training.md}}