Analyze the ICS-FuelStation compromise using PCAP network traffic analysis.
An internal host on the fuel station subnet swept the network with a rapid SYN scan and found a tank gauge controller exposing an FTP service and the Veeder-Root automated tank gauge port. The operator then opened a TCP session to the gauge and ran the vendor’s plain-text query commands in sequence: inventory, delivery, leak test, shift and alarm status reports. Having mapped the station, they wrote new product names to the gauge, replacing the tank labels with a ransom message and a contact address. The gauge protocol has no authentication, so every read and write rode in clear text and is fully recoverable from the capture.
LD-ICS-FUELSTATION-COMPROMISE.pcapng → open with Wireshark or tsharkTimestamp note: Q1 and Q2 are capture times in UTC (
frame.time_utc). The tank gauge prints its own clock inside each report, and Q10, Q11 and Q12 use those device times.
Filter for bare SYNs (no ACK) and group by source. One internal host, 10.10.32.130, fires them at sequential addresses and ports. The first SYN is the scan start, shown in UTC.
tshark -r LD-ICS-FUELSTATION-COMPROMISE.pcapng -Y 'tcp.flags.syn==1 && tcp.flags.ack==0 && ip.src==10.10.32.130' -T fields -e frame.time_utc -e ip.dst -e tcp.dstport | head -1
The same SYN list shows a burst of about a thousand probes that ends within three seconds. Four further SYNs appear later from the same host, but those are the follow-up connections to the services found open, not part of the sweep. The end of the burst is the stop time.
tshark -r LD-ICS-FUELSTATION-COMPROMISE.pcapng -Y 'tcp.flags.syn==1 && tcp.flags.ack==0 && ip.src==10.10.32.130' -T fields -e frame.time_utc -e ip.dst -e tcp.dstport | sed -n '1000,1005p'
Gotcha: The last SYN from the scanner is not the end of the scan. The later SYNs (07:28:46 onward) are service connections to open ports, so stop at the end of the burst.
From Q1 the scanner is the host sending the SYN burst. It sits on the same subnet as the gauge controller it later queries.
tshark -r LD-ICS-FUELSTATION-COMPROMISE.pcapng -Y 'tcp.flags.syn==1 && tcp.flags.ack==0 && ip.src==10.10.32.130' -T fields -e frame.time_utc -e ip.dst -e tcp.dstport | head -1
Look at the SYN-ACKs returned to the scanner. The target 10.10.32.128 answers on two ports only: the gauge port (Q5) and FTP on port 21.
tshark -r LD-ICS-FUELSTATION-COMPROMISE.pcapng -Y 'tcp.flags.syn==1 && tcp.flags.ack==1 && ip.dst==10.10.32.130' -T fields -e ip.src -e tcp.srcport | sort | uniq -c
The other open port on the same target is TCP 10001. The scanner’s later connections go there, and the payload is tank gauge protocol text, which confirms it.
tshark -r LD-ICS-FUELSTATION-COMPROMISE.pcapng -q -z follow,tcp,ascii,1004
The TCP 10001 stream opens with commands such as I20100, the in-tank inventory request. That command format is the Veeder-Root TLS serial protocol. The vendor was reasoned from the command set and is not printed anywhere in the traffic.
tshark -r LD-ICS-FUELSTATION-COMPROMISE.pcapng -q -z follow,tcp,ascii,1004
Every report returned by the gauge carries the station name as its header line, below the date and time.
tshark -r LD-ICS-FUELSTATION-COMPROMISE.pcapng -q -z follow,tcp,ascii,1005
The gauge host 10.10.32.128 announces itself in its DHCP Request, and the hostname option holds the answer.
tshark -r LD-ICS-FUELSTATION-COMPROMISE.pcapng -Y 'dhcp && ip.src==10.10.32.128' -T fields -e dhcp.option.hostname
In the second TCP 10001 stream (tcp.stream 1005) the client sends a series of SOH-prefixed commands. The one returning a DELIVERY REPORT is I20200.
tshark -r LD-ICS-FUELSTATION-COMPROMISE.pcapng -q -z follow,tcp,ascii,1005
The I20200 response lists the delivery for tank 1 with START and END rows. The last delivery is the END timestamp. This is the gauge’s own clock, so it differs from the capture time of the request.
tshark -r LD-ICS-FUELSTATION-COMPROMISE.pcapng -q -z follow,tcp,ascii,1005
The leak test report is I20300. Tank 2 (FUELOIL) shows TEST STATUS: ON and a LEAK DETECTED AT line. Tank 1 has no leak data, and tanks 3 and 4 read HEALTHY.
tshark -r LD-ICS-FUELSTATION-COMPROMISE.pcapng -q -z follow,tcp,ascii,1005 | grep -A2 'TANK 2'
The alarm and status report is I20500, listing a status per tank. The first tank (gasoline) reads as a fill in progress.
tshark -r LD-ICS-FUELSTATION-COMPROMISE.pcapng -q -z follow,tcp,ascii,1005 | grep -A6 I20500
Shift reports are I20400. The report header carries the date and time of the request, 08/27/2025 07:35, which matches that command’s position in the stream.
tshark -r LD-ICS-FUELSTATION-COMPROMISE.pcapng -q -z follow,tcp,ascii,1005 | grep -A3 I20400
The inventory (I20100) lists four tanks, and the second is not gasoline but FUELOIL. Its TEMP column is the value wanted, and it is unchanged in the post-tamper inventory.
tshark -r LD-ICS-FUELSTATION-COMPROMISE.pcapng -q -z follow,tcp,ascii,1005 | grep -A6 'IN-TANK INVENTORY'
Gotcha: The question says “2nd Gasoline product”, but tank 2 is FUELOIL. Read the second row of the table, not the label.
Tank 4 in the original inventory is the jet fuel tank. The same row later carries a truncated ransom string, so read the earlier report.
tshark -r LD-ICS-FUELSTATION-COMPROMISE.pcapng -q -z follow,tcp,ascii,1005 | grep -E '^ +4 '
Late in the stream the client sends four S602 commands that set the product labels for tanks 1 to 4. They read, in order, a hacked notice, a demand for payment, a contact line and an address. The 07:39 inventory shows the new names in place of the products. The amount is in the second label.
tshark -r LD-ICS-FUELSTATION-COMPROMISE.pcapng -q -z follow,tcp,ascii,1005 | grep S602
The fourth S602 write carries the contact address. The inventory view truncates it to 20 characters, so take the full string from the write command.
tshark -r LD-ICS-FUELSTATION-COMPROMISE.pcapng -q -z follow,tcp,ascii,1005 | grep S60204
| Phase | Action |
|---|---|
| Discovery | SYN scan of the subnet from an internal host, about a thousand probes in three seconds |
| Discovery | Service connections to FTP (21) and the tank gauge port (10001) on the gauge host |
| Collection | Inventory, delivery, leak test, shift and alarm status reports read over the unauthenticated gauge protocol |
| Impact | S602 writes replace the four product names with a ransom message and contact address |
| Impact | A later inventory request shows the tampered names |
| Timestamp (UTC) | Event |
|---|---|
| 2025-08-27 07:27:31 | First SYN of the scan from 10.10.32.130 |
| 2025-08-27 07:27:34 | Scan burst ends |
| 2025-08-27 07:28 | First I20100 inventory request on TCP 10001 |
| 2025-08-27 07:32 | Second session to the gauge opens; I20100 repeated |
| 2025-08-27 07:34 | Delivery report (I20200) and leak test (I20300) requested |
| 2025-08-27 07:35 | Shift report (I20400) and alarm status (I20500) requested |
| 2025-08-27 07:39 | Inventory shows the ransom text in the product names |
| Type | Value |
|---|---|
| Attacker host | 10.10.32.130 |
| Target | 10.10.32.128 (LD-PETROLEUM-SITE-6), TCP 21 and 10001 |
legiongroup@protonmail[.]com | |
| Ransom text | YOU_ARE_HACKED, PAY_US_50_ETH, CONTACT_US_AT |
| Technique | ID | Description |
|---|---|---|
| Network Service Discovery | T1046 | SYN scan finding FTP and the gauge port |
| Remote System Discovery | T1018 | Sweep of the subnet for live hosts |
| Monitor Process State (ICS) | T0801 | Inventory, delivery, leak and shift reports read from the gauge |
| Modify Parameter (ICS) | T0836 | S602 writes changing the product labels |
| Defacement: Internal Defacement | T1491.001 | Ransom text shown in the tank inventory |
The tank gauge protocol on TCP 10001 has no authentication. Anyone who can reach the port can read every report and change labels. Put it behind a firewall that admits only the site’s management systems, so an ordinary internal host cannot reach it.
A thousand SYNs in three seconds from one internal host is an easy alert. The scan came from inside the subnet, so perimeter controls saw nothing. East-west scan detection or flow baselining on the OT segment would have flagged it at 07:27.
Write commands to the gauge deserve their own detection. Read commands (I20x00) are routine for monitoring, while S602 is a configuration change. A sensor that parses the protocol can alert on any write from a host that is not the known head-end.
The gauge host also runs FTP. The FileZilla service on port 21 is an extra way in and a way to move files. Close it unless there is a documented need.
The attacker needed no exploit: an internal foothold, a port scan and plain-text commands were enough to read the station’s reports and rewrite the tank labels into a ransom note. Anything that can reach an unauthenticated ICS port can change what operators see.