← Back to portfolio

OTForge · Meridian Petrochemical, simulated chemical batch plant

TRITON/TRISIS: defending a safety instrumented system

An attack that pivots through a default credentialed IP camera and writes a Modbus coil to a Triconex safety controller. Detection here has to be scoped by destination address, because the safety system and the process controller are indistinguishable on the wire.

  • Modbus
  • Triconex SIS
  • XENOTIME
  • Pivoting
  • Suricata

Why a safety system is a different target

A Safety Instrumented System is the last independent layer between an unsafe process and an explosion, a toxic release, or a loss of life. Stuxnet and Industroyer manipulated the process, opening a valve or tripping a breaker. TRITON went after the system whose entire job is to stop the process from hurting anyone once something has already gone wrong.

XENOTIME (also tracked as TEMP.Veles) is the group behind the 2017 TRITON/TRISIS malware, exposed by FireEye/Mandiant and Dragos after an incident at a Saudi petrochemical facility. A Schneider Electric Triconex SIS unexpectedly tripped and shut the process down. Initially blamed on a mechanical fault, investigators instead found the attackers had reached an engineering workstation running the legitimate TriStation software, reverse engineered the proprietary protocol, and were pushing unauthorized logic to the safety controller. A bug in the attackers' own code, not any defensive control, caused the fail safe trip that led to the malware being discovered at all.

Researchers called it the first malware family with the potential to directly cause loss of life rather than only financial or operational damage. Targeting a SIS is an escalation in intent regardless of whether the attack succeeds.

The pivot

The attacker started with no route into the plant. The way in was an internet facing IP camera at 10.200.50.15 still running its factory password. That camera was dual homed, sitting on both the internet facing DMZ subnet and the OT field subnet at once, so compromising it created a live bridge between the public internet and the safety critical network.

An SSH local port forward tunnel through the camera mapped local port 5060 to the SIS's Modbus port at 10.200.10.11:502, and forwarded a Modbus function code 5 (Write Single Coil) to the Triconex controller. Coil 0 on that device is wired to emergency shutdown relay K1, the trip output that closes feed isolation valve SDV-101 on high high reactor pressure. Writing that coil off is an attempt to defeat the plant's last defense against an overpressure event.

SSH local port forward tunnel and its confirmation
The SSH tunnel maps local port 5060 through the compromised camera to the SIS's Modbus port, confirmed with nc -zv 127.0.0.1 5060.

What the sensors could, and couldn't, see

The Zeek Modbus log recorded the write coming from 10.200.10.201: the camera's OT-side interface, not the attacker subnet where Kali actually lives. Because the packet left the camera on its OT interface, every sensor in the plant saw the connection as originating from a device that legitimately had a path to the field zone. Suricata and Zeek sit on the OT network and cannot see back through the encrypted tunnel. That is not a flaw in either tool, it is a direct consequence of how a port forward pivot works.

Zeek Modbus log showing the write single coil request
Zeek Modbus log: a WRITE_SINGLE_COIL from 10.200.10.201 (the camera's OT-side interface) to 10.200.10.11 (the Triconex SIS) on port 502.

A firewall rule blocking the attacker subnet would never have fired. The traffic never used that path. This is the practical argument for detection that is scoped to what is being protected rather than to where attacks are expected to come from.

Scoping the rule by destination

In the DNP3 lab, a protocol keyword (dnp3_func:3) was enough to separate malicious from routine. Here it isn't, because the SIS and the process controller speak the identical Modbus wire format on the identical port, and coil writes to the process controller are perfectly normal.

SID 9000002 (preloaded) is scoped to $HOME_NET and matches only the raw function code byte 0x05 at offset 7, so it fires on any Write Single Coil to any device, including routine BPCS writes during normal operation.

SID 9100001 (the lab rule) uses the same byte match but is scoped to destination IP 10.200.10.11, so it fires only on a coil write aimed at the safety controller and stays silent on legitimate writes to the process controller. The generic rule fired because a coil write crossed the wire at all; the host scoped rule fired because that write targeted the one device that should never receive an unauthorized command.

Suricata alerts in Grafana Explore showing allowed and blocked actions
The same coil write fires SID 9000002 (generic, allowed), SID 9100001 (host scoped alert, allowed), and SID 9100002 (reject, blocked).
The lab Suricata rules scoped to the SIS destination address
Both lab rules scoped to destination 10.200.10.11, matching function code 0x05 at offset 7.

Defense in depth around the safety system

  • Close the foothold. The camera is still dual homed and still running default credentials. No Suricata rule solves that, credential and asset management does.
  • True segmentation. A device bridging the DMZ and the OT field network should not exist; the safety system in particular warrants its own conduit.
  • Change management on the SIS. Logic changes should require documented authorization and independent review, so an unauthorized push stands out against a controlled baseline.
  • Firmware integrity verification. A memory implant of the kind TRITON used is invisible to a network sensor sitting only on the conduit.

What I took from it

Independence on paper is not independence in practice. IEC 61511 requires an SIS to be independent from the Basic Process Control System: separate logic solver, separate I/O, ideally separate networking. Here they shared a subnet, firewall rules, and Modbus port 502, so nothing on the network distinguished the safety controller from the process controller it exists to protect. That is precisely why the rule had to be scoped by destination address: when two devices speak the same protocol on the same port, only the destination separates a write to the safety system from a routine process command.

The reject action is a verified mitigation, it stopped the write before it touched the trip relay. But it is a compensating control, not a fix. A protocol that cannot protect its own messages has to be wrapped in layers, so the last line of defense is never one keyword away from being switched off.

Lab write up from industrial control systems security coursework at UTSA. All addresses and devices belong to a simulated training environment.