# 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 modulesscapy.contrib.modbus: packet crafting for unsupported or custom workflows
Interesting Function Codes
Read / Recon
0x01/0x02: read coils / discrete inputs0x03/0x04: read holding / input registers0x11: report server ID0x2B/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 coil0x06: write single holding register0x0F: write multiple coils0x10: write multiple holding registers0x16: mask write register0x17: 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 record0x15: write file record0x18: read FIFO queue
A practical workflow is:
- Find the valid Unit ID.
- Verify read-only access with
0x01-0x04. - Pull device identification with
0x2B/0x0E. - Enumerate supported function codes.
- 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
- Nmap NSE: modbus-discover
- MOSTO: A toolkit to facilitate security auditing of ICS devices using Modbus/TCP
{{#include ../banners/hacktricks-training.md}}