July 01, 2025
Exploiting a Siemens S7 PLC Using Modbus and Python
Introduction
This post documents a Capture the Flag (CTF) challenge I completed that simulated an industrial environment. The scenario involved a Siemens S7 Programmable Logic Controller (PLC) that communicated using the Modbus TCP protocol. The objective was to explore the system, identify vulnerable functionality, and trigger a simulated event to capture the flag.
Along the way, I learned how to apply basic Modbus read and write operations in Python, how to interpret register values, and how easily unprotected industrial devices can be manipulated with simple tools.
The Setup
The CTF environment provided a target system accessible via IP address. A quick Nmap scan confirmed that TCP port 502 was open:
$ nmap -sV -p 502 <target_ip>
PORT STATE SERVICE VERSION
502/tcp open modbus Modbus TCP
This suggested the device was running a Modbus service, which is commonly used by industrial equipment like PLCs. Modbus has no built-in authentication, and many real-world devices are exposed on internal or even public networks. That made this a realistic and valuable exercise.
Preparing the Environment
I used a Kali Linux virtual machine as my attack platform. To interact with the Modbus service, I installed the pymodbus library:
pip install pymodbus==3.9.2
I also had Wireshark available for monitoring the traffic, although it wasn’t strictly necessary for this challenge.
Step 1: Reading Registers
The first step was to see what the device would return from a basic read operation. In Modbus, devices expose values through registers. I started by trying to read ten holding registers starting from address 0:
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient('<target_ip>', port=502)
client.connect()
response = client.read_holding_registers(0, 10, unit=1)
print(response.registers)
client.close()
This returned the following output:
[70, 0, 0, 0, 0, 0, 0, 0, 0, 0]
The first register contained the value 70, while the rest were zero. That caught my attention. In CTFs, a non-zero register often points to something meaningful, such as a control value or status indicator.
Step 2: Interpreting the Data
At this point, I didn’t know whether the value 70 was meant to be read-only or if it was part of a writable control interface. Since Modbus allows writing to registers just as easily as reading from them, I decided to test whether the device would accept changes.
However, I didn’t want to start blindly overwriting values. First, I reviewed how Modbus write operations are structured, especially write_register() for single values. I also double-checked the pymodbus documentation to confirm the function signature.
Step 3: Attempting a Write
I decided to try writing a new value to the first register, which previously contained 70. I modified the script:
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient('<target_ip>', port=502)
client.connect()
client.write_register(0, 99, unit=1)
client.close()
I chose 99 as a test value, thinking it might trigger some predefined behavior if this register was acting as a control point.
Shortly after running the script, the simulated environment responded with success. I also received a message containing the flag:
THM{flag_redacted}
That confirmed two things. First, the register was writable. Second, the value 99 triggered the specific logic the CTF designer had built in to simulate system failure or compromise.
Reflections
This was a simple challenge, but it demonstrated a few important principles that apply to real-world systems:
-
Modbus has no security features by default. If a system is accessible on the network, anyone who understands the protocol can read and write values.
-
Protocol understanding goes a long way. This challenge didn’t require exploits or fuzzing. Just basic knowledge of Modbus and some patient experimentation.
-
CTFs are a great way to safely explore offensive techniques. Practicing this against real hardware without permission would be unethical and illegal. Doing it in a simulation builds real skills with none of the risk.
Tools Used
- Python 3 with
pymodbus - Kali Linux (running in a VM)
- Nmap (for service discovery)
- Wireshark (for optional protocol inspection)
Next Steps
This experience made me want to explore more advanced Modbus attacks, such as crafting custom packets, replaying traffic, or brute-forcing unknown register ranges. I’m also looking into how these protocols interact with HMI software and SCADA systems in the real world.
I’ll document those experiments as I go. For now, this challenge was a fun and practical example of how easily industrial protocols can be manipulated if proper segmentation and access controls aren’t in place.