Threat Hunt Plan: Pilz IndustrialPI 4 Industrial Computers — Unauthenticated Node-RED Command Execution and Webstatus Authentication Bypass
Version 1.0 — 6 August 2026
Hunt Objective and Scope
This hunt seeks evidence that an attacker has located, reached, and exploited a Pilz IndustrialPI 4 industrial computer through either of two unauthenticated network-reachable defects disclosed by CERT@VDE on 1 July 2025. CVE-2025-41656 (CVSS v3.1 10.0, CWE-306) leaves the Node-RED flow-programming service on Firmware Bullseye 2024-08 and earlier reachable with no authentication configured, giving any attacker who can open a TCP session to it the ability to run arbitrary operating system commands with privileged rights on the underlying Debian platform. CVE-2025-41648 (CVSS v3.1 9.8, CWE-704) is an incorrect type conversion in the IndustrialPI webstatus web application below version 2.4.6 that allows the login to be bypassed outright, exposing every configuration setting the device holds.
The hunt population is every Pilz IndustrialPI unit and every Raspberry Pi-derived industrial computer of the same class in the environment, together with the network segments from which those devices are reachable. Because these units are frequently installed by machine builders and system integrators as protocol gateways or edge data collectors, the hunt explicitly includes discovery: any device answering on the Node-RED port or presenting the webstatus interface is in scope whether or not it appears in the asset management system. Adjacent hosts matter as much as the devices themselves — engineering workstations, jump hosts, historians, and SCADA servers on the same or neighbouring segments are both the plausible source of an exploitation attempt and the plausible next hop after one succeeds.
The recommended time window is 90 days of endpoint and network telemetry, extended to the full retention period for any OT monitoring platform that holds longer history. The disclosure is over a year old, patching of embedded industrial computers is routinely deferred, and both flaws leave an unauthenticated attacker in a position to persist quietly, so a short window risks reading a dormant implant as a clean result. Where a platform retains only 30 days, state that limit explicitly in the hunt report rather than reporting an unqualified negative.
Out of scope: active scanning or probing of the IndustrialPI devices themselves. Every collection step in this plan is passive or executed against telemetry already held by the organisation. Any active interrogation of a device in a running process cell requires plant engineering sign-off and is handled as a separate, ticketed activity.
Hypotheses and Hunt Procedures
Hypothesis 1: An unauthenticated external or internal actor has reached the Node-RED service on an IndustrialPI 4 and used it to execute operating system commands with privileged rights, observable as inbound sessions to the Node-RED administrative port followed by shell or interpreter execution descending from the Node-RED process in endpoint and network telemetry.
MITRE ATT&CK: Initial Access | T1190 — Exploit Public-Facing Application | the Node-RED admin interface is an unauthenticated application surface reachable over the network, which is precisely the access path this technique describes. Execution | T1059.004 — Command and Scripting Interpreter: Unix Shell | Node-RED exec nodes invoke the underlying shell, so successful abuse surfaces as shell children of the Node-RED runtime. ICS | T0819 — Exploit Public-Facing Application and ICS | T0871 — Execution through API | the flow-deployment API is the control surface an attacker drives to reach the operating system.
Collection Queries
CrowdStrike Falcon LogScale (CQL) — enumerate every endpoint that opened a session to the Node-RED administrative port. Run each query as a separate search; CQL supports one pipeline per search.
#event_simpleName = NetworkConnectIP4
| RemotePort = 1880
| groupBy([aid, ComputerName, LocalAddressIP4, RemoteAddressIP4, RemotePort], function=count(as=hits), limit=max)
| sort(hits, order=desc, limit=max)
CrowdStrike Falcon LogScale (CQL) — attribute those connections to the initiating process. NetworkConnectIP4 does not carry ImageFileName, so ProcessRollup2 is the main query and the heavily-filtered connection event is the subquery (reverse-direction join); run over 90 days so long-running daemons still resolve.
#event_simpleName = ProcessRollup2
| join({
#event_simpleName = NetworkConnectIP4
| RemotePort = 1880
},
field=[aid, TargetProcessId], key=[aid, ContextProcessId], mode=inner)
| groupBy([ComputerName, UserName, ImageFileName, CommandLine], function=count(as=hits), limit=max)
| sort(hits, order=desc, limit=max)
CrowdStrike Falcon LogScale (CQL) — where a Falcon Linux sensor is deployed on an IndustrialPI-class host, enumerate the listening sockets that expose Node-RED and webstatus.
#event_simpleName = NetworkListenIP4 event_platform=Lin
| in(LocalPort, values=[1880, 80, 443, 8080])
| groupBy([aid, ComputerName, LocalPort, ContextProcessId], function=count(as=hits), limit=max)
| sort(hits, order=desc, limit=max)
BPF packet capture — rolling capture of all traffic to and from the Node-RED and webstatus ports on the segment carrying the affected devices. The strftime format in the output filename is mandatory with -G or each rotation overwrites the previous file.
tcpdump -i eth0 -s 0 -G 3600 -w /captures/industrialpi-%Y%m%d-%H%M%S.pcap 'tcp port 1880 or tcp port 80 or tcp port 443 or tcp port 8080'
tcpdump -i eth0 -s 0 -C 100 -w /captures/nodered-hostfocus.pcap 'host <industrialpi_ip> and tcp'
tcpdump -i eth0 -nn -c 5000 'tcp port 1880 and tcp[tcpflags] & tcp-syn != 0'
Datadog Log Search — parallel to the CQL connection queries above, for environments forwarding host or container logs rather than EDR telemetry.
source:kubernetes @kubernetes.namespace_name:"ot-edge" message:"1880"
// Analytics: Table view, group by @kubernetes.pod_name; time range: last 90 days
source:windows @evt.id:5156 @network.destination.port:1880
// Analytics: Table view, group by host, @network.client.ip; time range: last 90 days
Datadog Live Process Monitoring (Infrastructure > Processes — not a log source, no source: filter; requires process_config.process_collection.enabled: true on the Agent):
command:node user:root
command:node-red
Datadog data source gap — if Live Process Monitoring is not enabled on OT-adjacent hosts, or if VPC/flow logs are not forwarded, fall back to the log search below and note the gap in the hunt report.
source:windows "1880"
// Analytics: Table view, group by host; time range: last 90 days
Windows Event IDs to collect — from any Windows host on a segment able to reach an IndustrialPI:
- 5156 — Windows Filtering Platform permitted a connection (destination port 1880, 80, 443, 8080)
- 4688 — Process creation, for the web-client tooling used to drive the Node-RED API
- 4104 — PowerShell script block logging, for Invoke-WebRequest / Invoke-RestMethod against the device
- 4624 / 4625 — Logon success and failure, for accounts used from or against the device address range
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=5156; StartTime=(Get-Date).AddDays(-90)} |
Where-Object { $_.Message -match ':1880|:8080' } |
Select-Object TimeCreated, Id, Message |
Export-Csv -NoTypeInformation -Path .\industrialpi_wfp_5156.csv
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104; StartTime=(Get-Date).AddDays(-90)} |
Where-Object { $_.Message -match '1880|/red/|/flows|industrialpi|revpi' } |
Select-Object TimeCreated, Id, Message |
Export-Csv -NoTypeInformation -Path .\industrialpi_psblock_4104.csv
OT Data Collection: Armis Centrix — inventory the affected devices and every connection into their management ports using ASQ. Set the time window in the UI time-picker; do not embed it in the query body.
in:devices manufacturer:"Pilz"
in:services port:1880
in:ipConnections serverPort:1880
in:ipConnections serverPort:1880 endpointA:(networkLocation:"External")
in:ipConnections serverPort:80,443,1880,8080 endpointB:(device:(manufacturer:"Pilz"))
OT Data Collection: Claroty xDome — Devices > All Devices > Advanced Filters, set Manufacturer = "Pilz" and OS Category = "Linux" to build the device population, then export. For the flow leg use Network > Communication > Communication Analysis: leave Side A empty, set Communication > Port = 1880 (add 80, 443, 8080 as OR rows), set Side B > Manufacturer = "Pilz", and set Time Frame = Past Quarter. Export the result set. Remember the 100,000-row export ceiling and batch by site if the estate is large.
OT Data Collection: Claroty CTD — on-prem CTD retains continuous full PCAP where xDome does not. Pull the packet capture for every session to port 1880 in the hunt window via the CTD event PCAP endpoint and carry it into the offline analysis step below.
OT Data Collection: Dragos Platform — in Assets, scope to Purdue Level 2 and 3 Linux-based industrial computers and gateways to define the hunt population. In the Communications Hub, filter destination port 1880 with any source zone over the hunt window and pivot each session to its asset record and event PCAP. Query the Industrial Protocols QFD and the Server Stats/PCR QFD for any IndustrialPI asset trending toward a producer-consumer ratio near -0.99, which indicates outbound data movement.
OT Data Collection: Nozomi Guardian and Vantage — N2QL queries; note that head without an argument silently returns 10 rows, so bound every exploratory query explicitly.
nodes | where vendor == "Pilz"
nodes | where_link protocol == http | head 200
links | where protocol == "http"
alerts | where time >= days_ago(90) | sort record_created_at asc
OT Data Collection: Tenable OT Security — in Inventory > All Assets, filter on asset type OtWorkstation and IndustrialGateway plus vendor Pilz and export the selection. In Risks > Findings, filter by affected asset to enumerate CVE-2025-41656 and CVE-2025-41648 matches against the discovered firmware and package versions. Programmatic sweeps use the GraphQL endpoint at /graphql (there are no REST /api/v1 query endpoints for OT Security); note that origins is deprecated in favour of networkAreas in v4.7.44.
OT Data Collection: Forescout eyeInspect — in the Command Center asset inventory, filter by Purdue level and role to surface Linux-based gateways and industrial PCs, then use the alerts view filtered to MITRE ATT&CK for ICS Initial Access techniques. Forward the alert and asset data via CEF/syslog to the SIEM for free-text hunting, since eyeInspect has no analyst-facing query language.
SNMP polling — poll the switch port facing each IndustrialPI and the device itself where an SNMP agent is exposed.
snmpwalk -v2c -c <community> <switch_ip> IF-MIB::ifTable
snmpget -v2c -c <community> <switch_ip> IF-MIB::ifHCInOctets.<ifIndex> IF-MIB::ifHCOutOctets.<ifIndex> IF-MIB::ifInErrors.<ifIndex> IF-MIB::ifOutErrors.<ifIndex>
snmpwalk -v3 -l authPriv -u <user> -a SHA-256 -A <authpass> -x AES-256 -X <privpass> <device_ip> system
Repeat the counter poll at 60-second intervals across the hunt window and diff successive values; a sustained rise in ifHCOutOctets on the device-facing port without a corresponding production change is a data-movement signal. Treat any SNMPv1 or SNMPv2c community-string access to an OT asset as a finding in its own right — those transit essentially in the clear.
YARA file-system scan — where a Falcon Linux sensor, Nozomi Arc agent, or vendor-sanctioned shell access exists on an IndustrialPI, scan the Node-RED flow store and user directories for implanted exec flows.
yara -r rules/nodered_exec_flow_implant.yar /root/.node-red/ /home/pi/.node-red/ /opt/ >> /tmp/yara_nodered_hits.txt
Analysis Queries
CrowdStrike Falcon LogScale (CQL) — rarity analysis. Sort ascending so the single-session sources surface first; an engineering workstation that touched the Node-RED port once in 90 days is far more interesting than the SCADA server that touches it hourly.
#event_simpleName = NetworkConnectIP4
| RemotePort = 1880
| groupBy([ComputerName, RemoteAddressIP4], function=count(as=hits), limit=max)
| sort(hits, order=asc, limit=max)
CrowdStrike Falcon LogScale (CQL) — shell and interpreter children of the Node-RED runtime on any Linux host carrying a Falcon sensor. This is the single highest-signal query in the hunt: Node-RED legitimately spawns very few operating system children in a production flow.
#event_simpleName = /ProcessRollup2/ event_platform=Lin
| ParentBaseFileName = /^(node|node-red|nodejs)$/i
| FileName = /^(sh|bash|dash|curl|wget|nc|ncat|python3?|perl|chmod)$/i
| groupBy([ComputerName, UserName, ParentBaseFileName, ImageFileName, CommandLine], function=count(as=hits), limit=max)
| sort(hits, order=desc, limit=max)
CrowdStrike Falcon LogScale (CQL) — reverse-shell command patterns on Linux OT hosts.
#event_simpleName = /ProcessRollup2/ event_platform=Lin
| CommandLine = /(bash\s+-i|\/dev\/tcp\/|nc\s+-e|socat\s+.*exec|python.*socket.*connect)/i
| groupBy([ComputerName, UserName, ImageFileName, CommandLine], function=count(as=hits), limit=max)
| sort(hits, order=desc, limit=max)
Wireshark display filters — applied to the captures collected above.
tcp.port == 1880 && http.request
http.request.uri contains "/flows" || http.request.uri contains "/red/"
http.request.method == "POST" && tcp.dstport == 1880
http.response.code == 200 && tcp.srcport == 1880 && http.content_length > 10000
tshark -r /captures/industrialpi-*.pcap -Y 'http.request && tcp.dstport==1880' -T fields -e frame.time -e ip.src -e ip.dst -e http.request.method -e http.request.uri -e http.user_agent
tshark -r /captures/industrialpi-*.pcap -q -z conv,tcp
Datadog Log Analytics — the analysis counterpart to the CQL rarity query.
source:windows @evt.id:5156 @network.destination.port:1880
// Analytics: Top List view, group by @network.client.ip; sort ascending for rarest-first
// Equivalent to CQL: groupBy([RemoteAddressIP4], function=count()) | sort(_count, order=asc, limit=max)
// time range: last 90 days
Datadog Audit Trail — surface configuration or access changes made through Datadog itself during the hunt window, so a monitoring blind spot is not mistaken for absence of activity.
source:datadog @evt.name:"Access Management"
// Analytics: Table view, group by @usr.email, @action; time range: last 90 days
Datadog CloudTrail integration — for any cloud-hosted historian or data-collection endpoint the IndustrialPI forwards to.
source:cloudtrail @evt.name:(GetObject OR PutObject OR CreateAccessKey) -@network.client.ip:10.* -@network.client.ip:172.16.* -@network.client.ip:192.168.*
// Analytics: Table view, group by @network.client.ip, @userIdentity.arn; time range: last 90 days
Datadog Monitor definition — Hypothesis 1
Type: Log Alert
Query: source:windows @evt.id:5156 @network.destination.port:1880
Evaluation window: last 15 minutes
Alert condition: count > 0
Message: "ALERT: connection observed to Node-RED admin port 1880 on an IndustrialPI-class device — verify against the change ticket queue and escalate if unattributed @ot-soc-oncall"
Prerequisites: Windows Security event log with WFP connection auditing (5156) forwarded to Datadog; OT-segment hosts enrolled in the Datadog Agent
Create via: Monitors > New Monitor > Log Alert OR POST /api/v1/monitors
Windows Event Log PowerShell analysis — connection hunting from the collected 5156 set.
Import-Csv .\industrialpi_wfp_5156.csv |
Group-Object { ($_.Message -split "`n" | Select-String 'Destination Address').ToString().Trim() } |
Sort-Object Count |
Select-Object Count, Name |
Export-Csv -NoTypeInformation -Path .\industrialpi_dest_rarity.csv
OT network and protocol analysis — the IndustrialPI 4 is commonly deployed as a Modbus TCP or OPC UA gateway. Compare the device's protocol profile before and after any suspicious Node-RED session: a gateway that begins issuing Modbus write function codes (5, 6, 15, 16, 22, 23) it never issued in the baseline period, or that begins speaking a protocol absent from its baseline, is a strong Stage 2 indicator. Correlate against historian tag writes and SCADA alarm records for the same interval, and involve plant operations before dispositioning any process-affecting change.
YARA memory scan — scan the Node-RED runtime process for injected or staged payloads. Classic YARA 4.5 takes the PID as a positional argument; there is no -p flag for process scanning (-p sets directory-scan thread count), and YARA-X yr scan cannot scan process memory at all.
pgrep -f node-red
yara rules/industrialpi_reverse_shell_memory.yar <pid> >> /tmp/yara_mem_hits.txt
CrowdStrike Falcon Real Time Response can execute these scans on remote hosts where a Falcon Linux sensor is deployed; use the put and run commands to stage the rule file and invoke YARA, and the get command to retrieve the hit output for offline review.
Hypothesis 2: An unauthenticated actor has bypassed the IndustrialPI webstatus login via the incorrect-type-conversion defect and read or altered device configuration, observable as web sessions reaching authenticated configuration paths with no preceding successful authentication event, followed by configuration state changes with no matching change ticket.
MITRE ATT&CK: Initial Access | T1190 — Exploit Public-Facing Application | the webstatus login bypass is exploited purely by reaching the application over the network. Discovery | T1082 — System Information Discovery | the authenticated configuration surface exposes network, service, and device parameters an attacker enumerates first. ICS | T0836 — Modify Parameter | the bypass grants write access to every device setting, which is the mechanism by which a process-affecting change would be made.
Collection Queries
CrowdStrike Falcon LogScale (CQL) — sessions to the webstatus HTTP and HTTPS interfaces from managed endpoints. Narrow the CIDR list to the subnets that actually host IndustrialPI units before running at scale.
#event_simpleName = NetworkConnectIP4
| in(RemotePort, values=[80, 443, 8080])
| cidr(RemoteAddressIP4, subnet=["10.0.0.0/8","172.16.0.0/12","192.168.0.0/16"])
| groupBy([ComputerName, RemoteAddressIP4, RemotePort], function=count(as=hits), limit=max)
| sort(hits, order=desc, limit=max)
CrowdStrike Falcon LogScale (CQL) — command-line tooling driving the device management interfaces, which distinguishes scripted exploitation from an engineer clicking through a browser.
#event_simpleName = /ProcessRollup2/
| CommandLine = /(curl|wget|invoke-webrequest|invoke-restmethod|python)/i
| CommandLine = /(:1880|\/red\/|\/api\/flows|revpi-webstatus|industrialpi)/i
| groupBy([ComputerName, UserName, ImageFileName, CommandLine], function=count(as=hits), limit=max)
| sort(hits, order=desc, limit=max)
BPF packet capture — capture the webstatus interface conversation in full so the request sequence can be replayed offline. The authentication bypass is visible as a request to a post-login path without a preceding successful login response.
tcpdump -i eth0 -s 0 -G 3600 -w /captures/webstatus-%Y%m%d-%H%M%S.pcap 'host <industrialpi_ip> and (tcp port 80 or tcp port 443 or tcp port 8080)'
Datadog Log Search — proxy, reverse-proxy, or web-gateway logs covering the OT segment.
source:kubernetes @kubernetes.namespace_name:"ot-edge" message:"webstatus"
// Analytics: Table view, group by @kubernetes.pod_name, @http.url_details.path; time range: last 90 days
source:windows @evt.id:5156 @network.destination.port:(80 OR 443 OR 8080)
// Analytics: Table view, group by host, @network.client.ip; time range: last 90 days
Datadog Live Process Monitoring:
command:curl user:root
Datadog data source gap — most environments do not forward web-server access logs from an embedded industrial device, because the device has no log-shipping agent and often no syslog target configured. Where that is the case, packet capture is the only reliable collection path for this hypothesis; record the gap explicitly in the hunt report and treat the PCAP leg as mandatory rather than supplementary.
Windows Event IDs to collect:
- 5156 — permitted connections to ports 80, 443, and 8080 on the device address range
- 4688 — process creation for curl.exe, powershell.exe, and python.exe with device addresses on the command line
- 4104 — PowerShell script block content referencing the device address or webstatus paths
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4688; StartTime=(Get-Date).AddDays(-90)} |
Where-Object { $_.Message -match 'curl|Invoke-WebRequest|Invoke-RestMethod|python' -and $_.Message -match 'webstatus|industrialpi|:8080' } |
Select-Object TimeCreated, Id, Message |
Export-Csv -NoTypeInformation -Path .\webstatus_proc_4688.csv
OT Data Collection: Armis Centrix — the connection leg for the webstatus interface.
in:ipConnections serverPort:80,443,8080 endpointB:(device:(manufacturer:"Pilz"))
in:alerts type:"Multiple Failed Login Attempts"
in:activity type:"Port Scan Detected"
OT Data Collection: Claroty xDome — Communication Analysis with Communication > Port = 80 (OR 443, OR 8080), Side B > Manufacturer = "Pilz", Time Frame = Past Quarter. Cross-check Alerts & Threats > Alerts > All Alerts filtered to Alert Category = Threat Alert and Policy Deviation Alert for the same device set; note that the alert surface has no time-frame selector, so sort by ALERT UPDATED descending and scan, or use the API detected_time filter for a hard window.
OT Data Collection: Dragos Platform — Communications Hub filtered to destination ports 80, 443, and 8080 with destination asset in the IndustrialPI population; triage Notifications filtered to ATT&CK for ICS Initial Access (T0819, T0866) and export the event PCAP for any session that fired an analytic.
OT Data Collection: Nozomi Guardian and Vantage:
links | where protocol == "http"
nodes | where_link protocol == https | head 200
alerts | where mitre_attack ~= "T0819" | sort record_created_at asc
OT Data Collection: Tenable OT Security — Events view filtered to policy violations of the Intrusion Detection and Configuration Change classes for the device population; Risks > Findings filtered by affected asset to confirm which units still carry a vulnerable webstatus package version. Where Code Snapshot or Details Query has been enabled and signed off for this asset class, diff the stored configuration snapshots across the hunt window; a configuration delta with no network-observed engineering session implies local or bypassed access.
OT Data Collection: Forescout eyeInspect — filter the alerts view to the Industrial Threat Library checks covering engineering-protocol abuse and unauthorised web administration, and use ICS Patrol only in low-cadence scoped mode and never against a safety-rated asset without engineering sign-off.
SNMP polling — a webstatus configuration change frequently restarts network services or the device itself. Collect trap-receiver records for the hunt window and flag:
- coldStart, OID 1.3.6.1.6.3.1.1.5.1 — device restart
- warmStart, OID 1.3.6.1.6.3.1.1.5.2 — service restart
- linkDown / linkUp, OIDs 1.3.6.1.6.3.1.1.5.3 and 1.3.6.1.6.3.1.1.5.4 — port flap consistent with a network reconfiguration
- authenticationFailure, OID 1.3.6.1.6.3.1.1.5.5 — unauthorised access attempts against the SNMP agent
snmpwalk -v2c -c <community> <device_ip> system
snmpget -v2c -c <community> <device_ip> SNMPv2-MIB::sysUpTime.0
An unexplained sysUpTime reset on an IndustrialPI is a first-order finding: these devices are not routinely rebooted, and a reboot outside a change window aligns with either a configuration write or an exploitation attempt that destabilised the host.
YARA file-system scan — the webstatus package directory and the device's web root, where a bypassed session would stage content.
yara -r rules/industrialpi_webstatus_tamper.yar /var/www/ /usr/share/revpi-webstatus/ /etc/ >> /tmp/yara_webstatus_hits.txt
Analysis Queries
CrowdStrike Falcon LogScale (CQL) — temporal distribution of web sessions to the device set. Configuration access outside the plant's engineering hours is the discriminator that separates an engineer from an attacker.
#event_simpleName = NetworkConnectIP4
| in(RemotePort, values=[80, 443, 8080])
| formatTime(format="%H", as=hourOfDay)
| groupBy([ComputerName, RemoteAddressIP4, hourOfDay], function=count(as=hits), limit=max)
| sort(hits, order=desc, limit=max)
Wireshark display filters — the definitive test for the authentication bypass is a request to an authenticated path with no preceding successful login response in the same TCP stream.
http.request.uri contains "config" || http.request.uri contains "settings" || http.request.uri contains "network"
http.response.code == 401 || http.response.code == 403
http.request.method == "POST" && http.content_type contains "json"
tshark -r /captures/webstatus-*.pcap -Y 'http' -T fields -e frame.time -e ip.src -e http.request.method -e http.request.uri -e http.response.code
tshark -r /captures/webstatus-*.pcap -q -z http_req,tree
Datadog Log Analytics:
source:windows @evt.id:5156 @network.destination.port:(80 OR 443 OR 8080)
// Analytics: Timeseries view, group by @network.client.ip; time range: last 90 days
// Compare against the plant engineering-hours profile; off-hours spikes are the triage priority
Datadog Audit Trail:
source:datadog @evt.name:Authentication
// Analytics: Table view, group by @usr.email, @evt.outcome; time range: last 90 days
Datadog CloudTrail integration:
source:cloudtrail @evt.name:(ConsoleLogin OR AssumeRole) @errorCode:* -@network.client.ip:10.*
// Analytics: Table view, group by @network.client.ip, @userIdentity.arn; time range: last 90 days
Datadog Monitor definition — Hypothesis 2
Type: Log Alert
Query: source:windows @evt.id:4688 "revpi-webstatus"
Evaluation window: last 30 minutes
Alert condition: count > 0
Message: "ALERT: scripted access to IndustrialPI webstatus observed from a managed endpoint — confirm against change management and escalate if unattributed @ot-soc-oncall"
Prerequisites: Windows Security 4688 with command-line auditing enabled and forwarded to Datadog
Create via: Monitors > New Monitor > Log Alert OR POST /api/v1/monitors
Windows Event Log PowerShell analysis — scheduled task hunting on the peer hosts, since an attacker who has confirmed the bypass typically automates re-entry.
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-TaskScheduler/Operational'; Id=106; StartTime=(Get-Date).AddDays(-90)} |
Select-Object TimeCreated, Id, Message |
Export-Csv -NoTypeInformation -Path .\peer_scheduled_tasks.csv
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4624; StartTime=(Get-Date).AddDays(-90)} |
Where-Object { $_.Message -match 'Logon Type:\s+3|Logon Type:\s+10' } |
Select-Object TimeCreated, Id, Message |
Export-Csv -NoTypeInformation -Path .\peer_network_logons.csv
OT network and protocol analysis — reconcile every configuration-affecting event against the change record. Export the device's protocol and port profile from the OT monitoring platform for a baseline window preceding the earliest suspicious web session and for the window following it, and diff the two. New listening ports, a changed default gateway, a new NTP or DNS target, or a new outbound peer are all reachable through the bypassed configuration interface and all constitute confirmation.
YARA memory scan — where the webstatus process is instrumented, scan its address space for staged shell content.
pgrep -f webstatus
yara rules/industrialpi_reverse_shell_memory.yar <pid> >> /tmp/yara_mem_hits.txt
Hypothesis 3: Following successful exploitation of either defect, the attacker has used the IndustrialPI as a foothold to move laterally into adjacent OT and IT systems or to establish outbound command and control, observable as inbound remote-service sessions and authentications originating from the device address range, new listening services on OT hosts, and credential-access tooling on Windows peers.
MITRE ATT&CK: Lateral Movement | T1021.004 — Remote Services: SSH | the compromised Debian host has native SSH tooling and reaches adjacent OT hosts without crossing a firewall. Credential Access | T1003 — OS Credential Dumping | an attacker who reaches a Windows engineering workstation from the device pivots to credential material to widen access. Command and Control | T1071.001 — Application Layer Protocol: Web Protocols | Node-RED's HTTP request nodes provide a native, low-suspicion egress channel. ICS | T0867 — Lateral Tool Transfer | moving tooling from the compromised gateway onto control-network hosts is the step that converts a device compromise into a process risk.
Collection Queries
CrowdStrike Falcon LogScale (CQL) — inbound remote-service sessions accepted by managed hosts. Correlate the RemoteAddressIP4 values against the IndustrialPI address inventory produced in Hypothesis 1.
#event_simpleName = NetworkReceiveAcceptIP4
| in(LocalPort, values=[22, 445, 3389, 5985, 5986])
| groupBy([ComputerName, RemoteAddressIP4, LocalPort], function=count(as=hits), limit=max)
| sort(hits, order=desc, limit=max)
CrowdStrike Falcon LogScale (CQL) — logon events carrying a source address, for correlation against the same device inventory. UserLogon exposes both RemoteIP and RemoteAddressIP4; use the IPv4-typed field where cidr filtering is wanted.
#event_simpleName = UserLogon
| RemoteAddressIP4 = *
| groupBy([ComputerName, UserName, LogonType, RemoteAddressIP4], function=count(as=hits), limit=max)
| sort(hits, order=desc, limit=max)
BPF packet capture — capture lateral-movement protocols sourced from the device.
tcpdump -i eth0 -s 0 -G 3600 -w /captures/lateral-%Y%m%d-%H%M%S.pcap 'src host <industrialpi_ip> and (tcp port 22 or tcp port 445 or tcp port 3389 or tcp port 5985)'
tcpdump -i eth0 -s 0 -w /captures/egress.pcap 'src host <industrialpi_ip> and not dst net 10.0.0.0/8 and not dst net 172.16.0.0/12 and not dst net 192.168.0.0/16'
Datadog Log Search:
source:windows @evt.id:4624 @network.client.ip:<industrialpi_ip>
// Analytics: Table view, group by host, @usr.name; time range: last 90 days
source:kubernetes message:"Accepted publickey"
// Analytics: Table view, group by @kubernetes.pod_name; time range: last 90 days
Datadog Live Process Monitoring:
command:ssh user:root
command:sshd
Datadog data source gap — SSH authentication on an embedded industrial device is written to the local journal and is rarely forwarded anywhere. Where no syslog target is configured on the IndustrialPI, treat the peer-side collection (Falcon NetworkReceiveAcceptIP4, Windows 4624, and the OT platform flow record) as the authoritative evidence and state in the report that device-side authentication logs were unavailable.
Windows Event IDs to collect:
- 4624 — successful logon, filtered to Logon Type 3 (network) and Logon Type 10 (remote interactive) with a source address in the device range
- 4625 — failed logon, which frequently precedes a successful one during credential spraying from a foothold
- 4688 — process creation, for lateral-movement tooling
- 7045 — service installation, the classic remote-execution artefact
- 4104 — PowerShell script block, for remoting payload content
Get-WinEvent -FilterHashtable @{LogName='System'; Id=7045; StartTime=(Get-Date).AddDays(-90)} |
Select-Object TimeCreated, Id, Message |
Export-Csv -NoTypeInformation -Path .\peer_service_install_7045.csv
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=(Get-Date).AddDays(-90)} |
Group-Object { $_.Properties[5].Value } |
Sort-Object Count -Descending |
Select-Object Count, Name |
Export-Csv -NoTypeInformation -Path .\peer_failed_logon_by_account.csv
OT Data Collection: Armis Centrix — outbound remote-service flows sourced from the device population.
in:ipConnections serverPort:22,23,445,3389 endpointA:(device:(manufacturer:"Pilz"))
in:ipConnections endpointA:(device:(manufacturer:"Pilz")) endpointB:(networkLocation:"External")
OT Data Collection: Claroty xDome — Communication Analysis with Side A > Manufacturer = "Pilz" and the Communication bucket left broad, Time Frame = Past Quarter, to enumerate every peer the device has spoken to. Compare the peer set against the device's documented function; a protocol gateway that has begun speaking SSH or SMB to an engineering workstation is outside its role. Use Network > Network Security > Zones & Policies to confirm whether the observed flow crosses a defined zone boundary.
OT Data Collection: Claroty CTD — export the full PCAP for any cross-zone session sourced from the device and carry it into offline analysis; CTD's continuous full-packet retention is what makes a retrospective lateral-movement question answerable at all.
OT Data Collection: Dragos Platform — Communications Hub with source zone set to the IndustrialPI's Purdue level and destination zone set to any other, protocol filter SSH, SMB, RDP, and VNC, over the hunt window. Pivot each cross-Purdue session to its asset record and event PCAP, and open a Case with the attached Dragos playbook on any confirmed hit. Check the SSH-sessions and DNS QFDs for the same asset.
OT Data Collection: Nozomi Guardian and Vantage:
links | join nodes to ip | join nodes from ip | select from_ip to_ip protocol
nodes | where_link protocol == ssh | head 200
nodes | where last_activity_time > days_ago(90)
Adapt the vendor's TrafficCrossingPurdueLevels, TrafficToPublicInternet, and TrafficOverVNCorRDP queries from the published query library rather than writing these from scratch; several contain environment-specific field names and must be adjusted and re-linted before use.
OT Data Collection: Tenable OT Security — Events view filtered to PLC mode change, project upload or download, and code revision change for the hunt window across the controller population reachable from the device. Any such event without a matching change ticket is a finding regardless of whether the device compromise is confirmed.
OT Data Collection: Forescout eyeInspect — cross-zone violation alerts from eyeSegment's authored matrix, plus Industrial Threat Library checks for engineering-protocol writes outside the maintenance window.
SNMP polling — correlate interface counters on both the device-facing port and the ports facing the pivot targets.
snmpwalk -v2c -c <community> <switch_ip> IF-MIB::ifTable
snmpget -v2c -c <community> <switch_ip> IF-MIB::ifHCOutOctets.<ifIndex> IF-MIB::ifHCInOctets.<ifIndex>
YARA file-system scan — staged tooling on the compromised device and on any pivot target reachable with a sanctioned agent.
yara -r rules/nodered_exec_flow_implant.yar /tmp/ /var/tmp/ /dev/shm/ /home/ >> /tmp/yara_staging_hits.txt
Analysis Queries
CrowdStrike Falcon LogScale (CQL) — rarity of listening services on Linux OT hosts. A port that appears on exactly one host is either a legitimate single-purpose service or an attacker's listener; both need an answer.
#event_simpleName = NetworkListenIP4 event_platform=Lin
| groupBy([ComputerName, LocalPort], function=count(as=hits), limit=max)
| sort(hits, order=asc, limit=max)
CrowdStrike Falcon LogScale (CQL) — credential-access tooling on Windows peers reachable from the device.
#event_simpleName = /ProcessRollup2/ event_platform=Win
| CommandLine = /(sekurlsa|lsadump|comsvcs.*minidump|procdump.*lsass)/i
| groupBy([ComputerName, UserName, ImageFileName, CommandLine], function=count(as=hits), limit=max)
| sort(hits, order=desc, limit=max)
Wireshark display filters — session and beacon analysis on the lateral and egress captures.
ssh || smb2 || rdp
tcp.flags.syn == 1 && tcp.flags.ack == 0 && ip.src == <industrialpi_ip>
http.request && ip.src == <industrialpi_ip>
tshark -r /captures/egress.pcap -q -z conv,tcp
tshark -r /captures/lateral-*.pcap -Y 'tcp.flags.syn==1 && tcp.flags.ack==0' -T fields -e frame.time -e ip.src -e ip.dst -e tcp.dstport
A regular inter-arrival interval on outbound sessions from the device, particularly with low and consistent byte counts, is beaconing. Compute the interval distribution from the conv,tcp output before dismissing periodic traffic as polling; legitimate industrial polling is usually far more frequent and far more consistent in payload size than command-and-control check-in.
Datadog Log Analytics:
source:windows @evt.id:4624 @network.client.ip:<industrialpi_ip>
// Analytics: Timeseries view, group by host, @usr.name; time range: last 90 days
// Any non-zero series here is a finding — an embedded gateway should not be authenticating to Windows hosts
Datadog Audit Trail:
source:datadog @evt.name:"Organization Management"
// Analytics: Table view, group by @usr.email, @action; time range: last 90 days
Datadog CloudTrail integration:
source:cloudtrail @evt.name:(CreateUser OR AttachUserPolicy OR CreateAccessKey) -@network.client.ip:10.* -@network.client.ip:172.16.* -@network.client.ip:192.168.*
// Analytics: Table view, group by @userIdentity.arn, @network.client.ip; time range: last 90 days
Datadog Monitor definition — Hypothesis 3
Type: Log Alert
Query: source:windows @evt.id:4624 @network.client.ip:<industrialpi_ip_range>
Evaluation window: last 15 minutes
Alert condition: count > 0
Message: "ALERT: Windows logon sourced from an IndustrialPI-class embedded device — this device class should never authenticate to Windows hosts; escalate immediately @ot-soc-oncall @ir-team"
Prerequisites: Windows Security 4624 forwarded to Datadog; the IndustrialPI address range recorded as a Datadog reference table or hard-coded in the monitor query
Create via: Monitors > New Monitor > Log Alert OR POST /api/v1/monitors
Windows Event Log PowerShell analysis — script block review on any peer that accepted a session from the device range.
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104; StartTime=(Get-Date).AddDays(-90)} |
Where-Object { $_.Message -match 'FromBase64String|DownloadString|IEX|Invoke-Expression|New-Object Net.Sockets' } |
Select-Object TimeCreated, Id, Message |
Export-Csv -NoTypeInformation -Path .\peer_suspicious_scriptblocks.csv
OT network and protocol analysis — for every confirmed lateral session, establish whether any control action followed. Pull the historian tag write history and the SCADA alarm log for the interval spanning the session plus four hours, and check for setpoint changes, mode transitions, or alarm suppressions with no operator attribution. On EtherNet/IP segments, decode CIP service codes in the captured traffic and flag Set Attribute Single (0x10), Set Attribute List (0x04), and any write to class 0x6B; on Modbus segments, flag function codes 5, 6, 15, 16, 22, and 23 issued from an address that has never issued a write in the baseline period. Any process-affecting finding is escalated jointly to plant operations and incident response, not to the SOC alone.
YARA memory scan — bulk process enumeration on a Linux OT host under investigation.
for p in $(ps -eo pid --no-headers); do yara rules/industrialpi_reverse_shell_memory.yar $p 2>/dev/null; done >> /tmp/yara_bulk_mem_hits.txt
yara rules/credential_dump_tool_memory_artifacts.yar <lsass_pid> >> C:\hunt\yara_cred_hits.txt
On Windows pivot targets, CrowdStrike Custom IOAs can pre-position detection for the same behaviours, and Falcon Real Time Response can stage and execute the YARA binary and rule file on demand; note that scanning LSASS memory requires SeDebugPrivilege.
Threat Actor Profile
Opportunistic financially-motivated actors are the most probable adversary for these vulnerabilities. Sophistication is low to moderate: neither defect requires an exploit chain, custom tooling, or any understanding of the industrial process — a CVSS 10.0 missing-authentication flaw is exploitable with a browser and a text editor. The access path is direct network reachability to the Node-RED port or the webstatus interface, obtained either from an internet-exposed device, a flat plant network reached from a compromised corporate endpoint, or a vendor remote-access session. Typical TTPs are mass scanning for the characteristic Node-RED banner, deployment of a cryptominer or a generic Linux backdoor via an exec flow, and opportunistic credential harvesting; the industrial context is usually incidental to the actor rather than the objective.
Ransomware operators represent the highest-consequence realistic threat. Sophistication is moderate to high, with mature access brokerage, credential-theft tooling, and a well-developed understanding that operational downtime drives payment. For this actor class the IndustrialPI is not the target but the entry point and staging platform: an unauthenticated root-equivalent execution primitive on a Linux host inside the production network is an unusually convenient beachhead, and one that endpoint detection frequently does not cover. Expected TTPs are reconnaissance of the OT segment from the compromised device, credential access on adjacent Windows engineering workstations, and encryption of IT-side systems with deliberate disruption of the operational environment as leverage.
State-aligned actors with an industrial mandate are the least likely but most severe scenario. Sophistication is high, with patient dwell, minimal tooling on disk, and a demonstrated interest in exactly this class of asset — a network-edge Linux device in a control environment that is rarely patched, rarely monitored, and rarely rebooted is an ideal persistence point. Such an actor would use the Node-RED flow store itself as the persistence mechanism rather than dropping a binary, would keep command and control inside HTTP to blend with the device's normal function, and would treat the compromise as pre-positioning rather than as an event to be monetised. Detection depends far more on behavioural anomaly work — a gateway speaking a protocol it has never spoken, an outbound session with no business purpose — than on indicator matching.
Insider and third-party risk deserves separate mention because it is the most common source of unattributed configuration change on this asset class. System integrators and machine builders routinely retain remote access to the equipment they commissioned, and their sessions frequently look identical to an attack in telemetry. Every finding in this hunt should be reconciled against the vendor-access register before escalation, and any vendor session that cannot be tied to a documented work order should be treated as a finding in its own right.
Data Sources Required
Network — full packet capture on the segments hosting IndustrialPI devices, covering TCP 1880, 80, 443, and 8080 inbound and all traffic outbound from the device address range; NetFlow or IPFIX from the distribution and core layers; firewall and NGFW connection logs for any flow crossing a zone boundary; proxy or web-gateway logs where OT-segment traffic is inspected; DNS resolver query logs.
Endpoint — CrowdStrike Falcon telemetry across Windows and Linux hosts in and adjacent to the OT environment, specifically ProcessRollup2 and SyntheticProcessRollup2, NetworkConnectIP4, NetworkReceiveAcceptIP4, NetworkListenIP4, UserLogon, UserLogonFailed2, and ScheduledTaskRegistered; Windows Security, System, PowerShell/Operational, and TaskScheduler/Operational event logs with command-line auditing and script block logging enabled; Sysmon where deployed; Datadog Agent logs and Live Process Monitoring where the Datadog stack is present.
OT and ICS — asset inventory and flow records from every deployed monitoring platform (Claroty xDome, Claroty CTD, Dragos Platform, Nozomi Guardian and Vantage, Armis Centrix, Tenable OT Security, Forescout eyeInspect); process historian tag write history; SCADA alarm and event logs; PLC and controller configuration snapshots or code revision history where the platform captures them; OT platform PCAP exports (Dragos event PCAP, Claroty CTD full capture).
Vendor and device logs — IndustrialPI system journal and authentication log where a syslog target is configured; Node-RED runtime log and the flow store at /root/.node-red/ and /home/pi/.node-red/; webstatus application log; apt history to establish whether and when the webstatus package was updated; SNMP interface counters and trap-receiver records for the device and its access switch port.
Supporting records — the change management ticket queue for the hunt window, the vendor and integrator remote-access register, the plant maintenance schedule, and the engineering-hours profile. Without these the hunt can identify activity but cannot disposition it, and the distinction between an engineer and an attacker on this asset class is almost entirely a question of attribution rather than technique.
Detection Signatures
The following SIGMA rules span process_creation, network_connection, and webserver logsource categories so that detection does not depend on a single telemetry path.
title: Node-RED Runtime Spawning Operating System Shell or Interpreter
id: 7c4e1a92-3b8d-4f16-9e05-2a7d6c81b4f3
status: experimental
description: Detects a shell, network utility, or scripting interpreter executed as a child of the Node-RED runtime, the expected post-exploitation footprint of CVE-2025-41656 on Pilz IndustrialPI 4 and other Node-RED deployments without authentication configured.
references:
- https://certvde.com/en/advisories/VDE-2025-045/
- https://nvd.nist.gov/vuln/detail/CVE-2025-41656
author: 1898 & Co. Threat Hunt Team
date: 2026/08/06
tags:
- attack.execution
- attack.t1059.004
- attack.initial_access
- attack.t1190
logsource:
category: process_creation
product: linux
detection:
selection_parent:
ParentImage|endswith:
- '/node'
- '/nodejs'
- '/node-red'
selection_child:
Image|endswith:
- '/sh'
- '/bash'
- '/dash'
- '/nc'
- '/ncat'
- '/curl'
- '/wget'
- '/python3'
- '/perl'
- '/chmod'
condition: selection_parent and selection_child
falsepositives:
- Node-RED exec nodes used deliberately in a sanctioned flow to invoke a local script; enumerate and allowlist by full command line, not by binary name
- Vendor-supplied flows that shell out during device startup
level: critical
title: Connection to Node-RED Administrative Port from a Non-Engineering Host
id: 3f8a25d1-6c04-4b7e-8d29-51ea9f30c6b8
status: experimental
description: Detects an outbound TCP connection to port 1880, the Node-RED administrative interface, from a Windows endpoint. On a correctly segmented network this port should be reachable only from a defined engineering workstation range.
references:
- https://certvde.com/en/advisories/VDE-2025-045/
author: 1898 & Co. Threat Hunt Team
date: 2026/08/06
tags:
- attack.initial_access
- attack.t1190
- attack.discovery
- attack.t1046
logsource:
category: network_connection
product: windows
detection:
selection:
DestinationPort: 1880
Initiated: 'true'
filter_engineering:
Image|endswith:
- '\chrome.exe'
- '\msedge.exe'
- '\firefox.exe'
condition: selection and not filter_engineering
falsepositives:
- Sanctioned engineering workstations administering Node-RED through a browser; allowlist by host, not by process
- Vulnerability scanners and asset discovery tools sweeping the OT segment
level: high
title: IndustrialPI Webstatus Authenticated Path Reached Without Prior Authentication
id: b2d97e40-8a35-4c61-b7f8-0e63d1a5f29c
status: experimental
description: Detects HTTP requests to IndustrialPI webstatus configuration paths, the observable surface of the CVE-2025-41648 authentication bypass, where a request reaches a post-login path. Correlate the source address against successful authentication responses in the same session before dispositioning.
references:
- https://certvde.com/en/advisories/VDE-2025-039/
- https://nvd.nist.gov/vuln/detail/CVE-2025-41648
author: 1898 & Co. Threat Hunt Team
date: 2026/08/06
tags:
- attack.initial_access
- attack.t1190
- attack.discovery
- attack.t1082
logsource:
category: webserver
detection:
selection_path:
cs-uri-stem|contains:
- '/webstatus'
- '/config'
- '/network'
- '/settings'
selection_agent:
cs-user-agent|contains:
- 'curl'
- 'python-requests'
- 'Go-http-client'
- 'wget'
condition: selection_path and selection_agent
falsepositives:
- Monitoring and health-check systems polling the device status page with a scripted user agent; allowlist by source address
- Configuration management tooling that manages the device declaratively
level: high
title: New Listening Service on a Linux Industrial Gateway
id: 5e1c8b73-9d26-4a08-93f4-c7b25a6e0d1f
status: experimental
description: Detects a process opening a listening socket on a Linux industrial host outside the set of ports the device class is expected to serve. On an IndustrialPI-class gateway the expected listener set is small and stable, so any addition is high signal for an implanted backdoor.
references:
- https://certvde.com/en/advisories/VDE-2025-045/
author: 1898 & Co. Threat Hunt Team
date: 2026/08/06
tags:
- attack.persistence
- attack.t1543
- attack.command_and_control
- attack.t1571
logsource:
category: network_connection
product: linux
detection:
selection:
Initiated: 'false'
filter_expected:
DestinationPort:
- 22
- 80
- 443
- 502
- 1880
- 4840
- 8080
condition: selection and not filter_expected
falsepositives:
- Ephemeral high ports used by legitimate application callbacks; restrict evaluation to ports below 10000 if noise is excessive
- Vendor diagnostic services enabled during a commissioning or maintenance window
level: high
The following Snort and Suricata rules cover the wire signature of both exploitation paths. Local SIDs begin at 1,000,000 per the Suricata local range convention; adjust HOME_NET and the OT_ENGINEERING variable to the environment before deployment.
alert tcp !$OT_ENGINEERING any -> $HOME_NET 1880 (msg:"OT INDUSTRIALPI Node-RED admin interface accessed from non-engineering source - possible CVE-2025-41656 exploitation"; flow:to_server,established; content:"GET"; http_method; content:"/red/"; http_uri; nocase; threshold:type limit, track by_src, count 1, seconds 300; classtype:attempted-admin; reference:cve,2025-41656; reference:url,certvde.com/en/advisories/VDE-2025-045/; metadata:service http; sid:1000001; rev:1;)
alert tcp any any -> $HOME_NET 1880 (msg:"OT INDUSTRIALPI Node-RED flow deployment POST - unauthenticated arbitrary command execution attempt CVE-2025-41656"; flow:to_server,established; content:"POST"; http_method; content:"/flows"; http_uri; nocase; content:"exec"; http_client_body; nocase; detection_filter:track by_src, count 1, seconds 60; classtype:attempted-admin; reference:cve,2025-41656; sid:1000002; rev:1;)
alert tcp any any -> $HOME_NET [80,443,8080] (msg:"OT INDUSTRIALPI webstatus configuration path accessed with scripted user agent - possible CVE-2025-41648 authentication bypass"; flow:to_server,established; content:"/webstatus"; http_uri; nocase; content:"python-requests"; http_user_agent; nocase; threshold:type limit, track by_src, count 1, seconds 300; classtype:web-application-attack; reference:cve,2025-41648; reference:url,certvde.com/en/advisories/VDE-2025-039/; metadata:service http; sid:1000003; rev:1;)
alert tcp $HOME_NET any -> !$HOME_NET any (msg:"OT INDUSTRIALPI embedded gateway initiating outbound session to external network - possible post-exploitation C2"; flow:to_server,established; threshold:type limit, track by_src, count 1, seconds 3600; classtype:policy-violation; reference:cve,2025-41656; sid:1000004; rev:1;)
The first YARA rule targets the Node-RED flow store on disk. Node-RED persists its flows as JSON at a predictable path, and an attacker who exploits CVE-2025-41656 to gain persistence rather than a one-shot command must write an exec-type node into that file. The condition requires both the JSON structure that marks a Node-RED flow file and at least one command-execution indicator, because the flow file itself is expected to exist on every device and matching on its presence alone would fire on every healthy unit. The reverse-shell and download-utility string set is deliberately narrow to keep the rule from matching legitimate flows that invoke a local script by name.
rule NodeRED_Exec_Flow_Implant
{
meta:
description = "Detects a Node-RED flow file containing an exec node with command-execution or reverse-shell content, the persistence artifact of CVE-2025-41656 exploitation on Pilz IndustrialPI 4"
author = "1898 & Co. Threat Hunt Team"
date = "2026-08-06"
reference = "https://certvde.com/en/advisories/VDE-2025-045/"
severity = "critical"
strings:
$flow_marker1 = "\"type\":\"exec\"" ascii nocase // Node-RED exec node declaration
$flow_marker2 = "\"type\":\"function\"" ascii nocase // function node, used to build the command string
$flow_marker3 = "\"wires\":" ascii // structural marker of a Node-RED flow file
$cmd1 = "/dev/tcp/" ascii // bash network redirection reverse shell
$cmd2 = "bash -i" ascii nocase // interactive shell invocation
$cmd3 = "nc -e" ascii nocase // netcat execute-on-connect
$cmd4 = "socat" ascii nocase // socat relay, common reverse-shell transport
$cmd5 = "curl http" ascii nocase // stager download
$cmd6 = "wget http" ascii nocase // stager download
$cmd7 = "chmod +x" ascii nocase // making a downloaded payload executable
$cmd8 = "base64 -d" ascii nocase // encoded payload decode step
$r_crontab = /crontab\s+-|\/etc\/cron\.[a-z]+\// ascii // scheduled persistence written from a flow
condition:
filesize < 20MB
and $flow_marker3
and any of ($flow_marker1, $flow_marker2)
and (2 of ($cmd*) or $r_crontab)
}
The second YARA rule scans process memory rather than disk, because the most likely outcome of exploiting an unauthenticated command-execution primitive on a Linux gateway is an in-memory reverse shell that leaves nothing on the filesystem. The condition is built as a two-part test — a shell or socket artifact plus a connect-back artifact — so that the presence of a shell string in a legitimate process image does not fire on its own. The rule is scoped to Linux process memory and should be run against the Node-RED runtime, the webstatus process, and any process holding an unexpected listening socket identified in the Hypothesis 3 analysis.
rule IndustrialPI_Reverse_Shell_Memory
{
meta:
description = "Detects reverse-shell and connect-back artifacts in the memory of a process on a Linux industrial gateway, the expected in-memory footprint of CVE-2025-41656 post-exploitation"
author = "1898 & Co. Threat Hunt Team"
date = "2026-08-06"
reference = "https://nvd.nist.gov/vuln/detail/CVE-2025-41656"
severity = "critical"
strings:
$shell1 = "/bin/sh" ascii // shell path staged for execve
$shell2 = "/bin/bash" ascii // shell path staged for execve
$sock1 = "socket" ascii fullword // socket syscall wrapper symbol
$sock2 = "connect" ascii fullword // connect syscall wrapper symbol
$sock3 = "dup2" ascii fullword // file-descriptor duplication, the reverse-shell idiom
$cb1 = "/dev/tcp/" ascii // bash network redirection
$cb2 = "sh -i >&" ascii // classic interactive reverse shell fragment
$cb3 = "pty.spawn" ascii // python TTY upgrade after a raw shell
$cb4 = "SOCK_STREAM" ascii // explicit TCP socket construction in an interpreter payload
$miner = "stratum+tcp://" ascii nocase // cryptominer pool URL, the common opportunistic outcome
condition:
($miner)
or (any of ($shell*) and 2 of ($sock*) and any of ($cb*))
or (2 of ($cb*))
}
The third YARA rule is the standing credential-dumping rule, included because Hypothesis 3 involves lateral movement into Windows peers and credential access. It covers the four classic tool families plus a catch-all branch pairing a memory-read API with the LSASS process name and a tool indicator, so that renamed or repacked tooling still matches. Executing it against LSASS requires SeDebugPrivilege. Note the platform scope: the IndustrialPI itself is Linux, and this rule will not match Linux credential-dumping through the /proc filesystem (T1003.007); it is scoped to the Windows engineering workstations and servers reachable from the device, and the Linux-side equivalent is covered by the reverse-shell memory rule above together with the CriticalFileAccessed telemetry on /etc/shadow.
rule Credential_Dump_Tool_Memory_Artifacts
{
meta:
description = "Detects credential-dumping tool artifacts in process memory on Windows pivot targets reached from a compromised industrial gateway"
author = "1898 & Co. Threat Hunt Team"
date = "2026-08-06"
reference = "https://attack.mitre.org/techniques/T1003/"
severity = "critical"
strings:
$mimi1 = "sekurlsa::logonpasswords" ascii wide nocase // mimikatz credential module command
$mimi2 = "lsadump::sam" ascii wide nocase // mimikatz SAM dump command
$mimi3 = "privilege::debug" ascii wide nocase // mimikatz privilege escalation step
$mimi4 = "mimikatz" ascii wide nocase // tool name string
$mimi_hex = { 6D 69 6D 69 6B 61 74 7A } // "mimikatz" in hex, catches simple obfuscation
$wce1 = "wce.exe" ascii wide nocase // Windows Credentials Editor binary name
$gsec = "gsecdump" ascii wide nocase // gsecdump tool name
$comsvcs1 = "MiniDump" ascii wide // comsvcs.dll MiniDump export
$comsvcs2 = "comsvcs" ascii wide nocase // the LOLBIN carrying the export
$lsass = "lsass.exe" ascii wide nocase // the target process
$api1 = "NtReadVirtualMemory" ascii wide // memory-read API
$api2 = "ReadProcessMemory" ascii wide // memory-read API
condition:
any of ($mimi*)
or ($wce1 and $lsass)
or $gsec
or ($comsvcs1 and $comsvcs2 and $lsass)
or (any of ($api*) and $lsass and (any of ($mimi*) or $wce1 or $gsec or $comsvcs1))
}
Indicators of Compromise
Network indicators — any inbound TCP session to port 1880 from a source outside the defined engineering workstation range; any HTTP POST to /flows or /red/ on an IndustrialPI address; any HTTP request to a webstatus configuration path bearing a scripted user agent (curl, wget, python-requests, Go-http-client); any outbound session from an IndustrialPI address to an internet-routable destination; any SSH, SMB, RDP, or WinRM session sourced from an IndustrialPI address; any DNS query from the device address range to a domain outside the organisation's resolver allowlist; regular-interval outbound sessions with consistent low byte counts from the device address range.
Host indicators — a shell, netcat, curl, wget, python, perl, or chmod process whose parent is the Node-RED runtime; a new listening socket on an IndustrialPI outside the expected set of 22, 80, 443, 502, 1880, 4840, and 8080; a modification to /root/.node-red/flows.json or /home/pi/.node-red/flows.json with no corresponding change ticket; a new cron entry or systemd unit on the device; a sysUpTime reset outside a maintenance window; an apt history showing no webstatus upgrade on a device still reporting a version below 2.4.6; a Windows logon event with a source address in the IndustrialPI range; a service installation (event 7045) on a peer host within the window of an inbound session from the device.
OT and operational indicators — the device beginning to speak a protocol absent from its established baseline; Modbus write function codes (5, 6, 15, 16, 22, 23) or CIP Set Attribute services (0x04, 0x10) issued from the device where the baseline shows read-only behaviour; a controller mode change, project download, or code revision on any controller reachable from the device with no matching change record; a historian tag write or setpoint change with no operator attribution within four hours of a suspicious device session; an alarm suppression on a point the device gateways; a sustained rise in ifHCOutOctets on the device-facing switch port without a corresponding production change; a coldStart or warmStart SNMP trap from the device outside a maintenance window; the appearance of the device as a new peer in a zone it has no documented reason to reach.
False Positive Baseline
System integrator and machine builder remote support. The organisations that commissioned the equipment routinely retain access and use exactly the interfaces this hunt monitors. Suppress by reconciling against the vendor-access register and the work-order queue; do not suppress by source address alone, because a compromised integrator is a real access path and an unregistered session from a known integrator address is a finding rather than noise.
Sanctioned Node-RED flow development. Where Node-RED is the intended programming surface for the device, engineers legitimately deploy flows and some of those flows legitimately invoke local scripts through exec nodes. Enumerate the sanctioned flow set, hash the flow files, and suppress by full command line rather than by binary name — suppressing every shell child of Node-RED discards the highest-signal query in the plan.
Vulnerability scanning and asset discovery. Nessus, Tenable OT active queries, Armis Smart Active Querying, Claroty active queries, and Forescout ICS Patrol all touch device management interfaces on a schedule and will generate connections to ports 1880, 80, 443, and 8080. Suppress by scanner source address and correlate against the scan schedule; note that Tenable's Nessus engine must never be pointed at a controller, so a Nessus signature against a PLC is itself a finding.
Monitoring and health-check polling. Uptime monitors, SCADA availability checks, and network management systems poll the device status page continuously. These produce a high, extremely regular volume from a small set of source addresses and are straightforward to characterise and suppress; verify that the polling source is the documented monitoring host and that the requested path is genuinely a status path rather than a configuration path.
Browser-based engineering access from sanctioned workstations. An engineer administering the device through Chrome, Edge, or Firefox from a defined engineering workstation is the expected pattern. Suppress by the host-plus-user pair rather than by process name, and retain the temporal analysis — the same engineer on the same workstation at three in the morning during an unscheduled window is still worth a question.
DHCP churn and device re-addressing. Embedded devices that acquire addresses dynamically will appear as new assets to OT monitoring platforms after every lease change, generating new-device alerts and apparent new peers. Reconcile by MAC address rather than IP before treating an apparent new device as a finding; IP-keyed correlation is unreliable across the whole OT estate.
Backup, patch, and configuration-management cycles. Scheduled apt runs, configuration pushes, and backup agents produce bursts of process execution and network activity on and around the device that resemble post-exploitation staging. Correlate against the maintenance calendar and the change queue before escalation.
Escalation Criteria
The following conditions trigger immediate incident response engagement. Each is stated as an observable, not as an interpretation.
1. Any shell, netcat, curl, wget, python, perl, or chmod process observed as a child of the Node-RED runtime on any IndustrialPI or equivalent device, where the command line cannot be matched to a documented sanctioned flow.
2. Any YARA hit on NodeRED_Exec_Flow_Implant against /root/.node-red/, /home/pi/.node-red/, or any other Node-RED flow store on a device in the hunt population.
3. Any YARA hit on IndustrialPI_Reverse_Shell_Memory against the Node-RED runtime process, the webstatus process, or any process holding an unexpected listening socket on a Linux industrial host.
4. Any YARA hit on Credential_Dump_Tool_Memory_Artifacts against LSASS or any process on a Windows engineering workstation, server, or jump host reachable from an IndustrialPI address.
5. Any successful Windows logon (event 4624) whose source address falls within the IndustrialPI device range, regardless of logon type or account — this device class has no legitimate reason to authenticate to a Windows host.
6. Any outbound session from an IndustrialPI address to an internet-routable destination that is not an explicitly documented vendor or cloud endpoint.
7. Any new listening socket on an IndustrialPI outside the expected port set, or any modification to the Node-RED flow store, cron table, or systemd unit set with no corresponding change ticket.
8. Any Modbus write function code, CIP Set Attribute service, controller mode change, project download, or code revision issued from or temporally correlated with an IndustrialPI session, where no change record exists — escalate jointly to plant operations and incident response, and do not take unilateral containment action on a device in a running process.
9. Any device confirmed to be running webstatus below version 2.4.6 or Firmware Bullseye 2024-08 or earlier while also showing any connection to ports 1880, 80, 443, or 8080 from outside the defined engineering range within the hunt window.
10. Any IndustrialPI or equivalent device found to be reachable from the enterprise network or from an internet-facing path, whether or not exploitation evidence exists — the exposure alone is a CVSS 10.0 unauthenticated remote code execution condition and warrants immediate containment planning.
Hunt Completion Criteria and Reporting
The hunt is complete when four conditions are satisfied. First, every Pilz IndustrialPI and equivalent Raspberry Pi-derived industrial computer in the environment has been enumerated, with its firmware image date and webstatus package version recorded, and the enumeration has been reconciled against both the asset management system and the OT monitoring platforms' passive discovery output. Second, every hypothesis in Section 2 has been executed across the full hunt window on every platform present in the environment, with the outcome recorded per hypothesis per platform. Third, every collection and analysis query has either returned a dispositioned result or has been recorded as unrunnable with the specific reason — an absent data source, an unavailable retention period, or a platform not deployed. Fourth, every candidate finding has been reconciled against the change management queue, the vendor-access register, and the maintenance calendar, and each has been closed as benign, escalated, or explicitly carried forward as unresolved.
A hypothesis that returns no results is only a negative when the underlying data source is confirmed present and populated for the whole window. Where a query returned zero rows, the report must state whether that reflects absence of activity or absence of telemetry. This distinction matters more than usual here: embedded industrial devices are the archetypal blind spot, and a clean result produced by querying telemetry that was never collected from the device is the most dangerous possible output of this hunt.
The hunt report must contain: the scope statement and the exact time window queried per platform; the full device inventory with firmware and package versions and patch status per unit; per-hypothesis findings with supporting query output and the platform each came from; every escalation triggered under Section 8 with its disposition and incident ticket reference; every data source gap identified, with the hypotheses it degraded and the residual risk that remains unaddressed; the false-positive suppressions applied, with the justification and the reviewing analyst for each; recommended detection content to promote to standing coverage, specifically which SIGMA, Snort/Suricata, and YARA rules from Section 5 should be deployed and to which platform; and a remediation status summary covering webstatus package updates, Node-RED authentication enablement, and segmentation changes, with owners and target dates for each device not yet remediated.
Validated hunt content should not be discarded at the end of the engagement. The connection queries for port 1880 and the Node-RED child-process query are strong candidates for CrowdStrike NG-SIEM correlation rules or scheduled searches; the Datadog monitor definitions in Section 2 are written to be deployed as-is; and the Snort/Suricata rules belong on the sensor covering the OT boundary. Where the environment runs Dragos, register the validated hypothesis as a custom analytic through the analytic manager so that it becomes a standing detection rather than a point-in-time exercise.
Advisory IoC Reference
| IOC Type | IOC |
|---|---|
| CVE | CVE-2025-41656 | CVSS v3.1 10.0 | Pilz IndustrialPI 4 (A1000002, A1000003) Firmware Bullseye <= 2024-08 | Node-RED service exposed with no authentication configured by default, allowing an unauthenticated remote attacker to run arbitrary OS commands with privileged rights (CWE-306). |
| CVE | CVE-2025-41648 | CVSS v3.1 9.8 | Pilz IndustrialPI webstatus < 2.4.6 | Incorrect type conversion in the login path lets an unauthenticated remote attacker bypass the web login and read or change every device setting (CWE-704). |
| Threat Actor | None attributed in source material — no named group, APT cluster, or campaign is associated with either CVE as of this advisory. |
| Malware | None named in source material — no malware family, implant, or tool has been publicly tied to exploitation of these CVEs. |
| Network IOC | None published in source material — monitor https://www.cisa.gov/known-exploited-vulnerabilities-catalog and https://certvde.com/en/advisories/ for indicators as exploitation is observed. |
| File IOC | None published in source material — monitor https://certvde.com/en/advisories/VDE-2025-045/ and https://certvde.com/en/advisories/VDE-2025-039/ for vendor-published artifacts. |
| Behavioral | Inbound TCP session to port 1880 (Node-RED admin) on an IndustrialPI-class device from a source outside the defined engineering workstation range. |
| Behavioral | HTTP POST to /flows or GET to /red/ on an IndustrialPI address, indicating flow deployment or admin-interface access. |
| Behavioral | Shell, netcat, curl, wget, python, perl, or chmod process executed as a child of the Node-RED runtime (node / nodejs / node-red). |
| Behavioral | HTTP request to an IndustrialPI webstatus configuration path bearing a scripted user agent (curl, wget, python-requests, Go-http-client). |
| Behavioral | New listening socket on an IndustrialPI outside the expected set of TCP 22, 80, 443, 502, 1880, 4840, 8080. |
| Behavioral | Modification to /root/.node-red/flows.json or /home/pi/.node-red/flows.json with no corresponding change ticket. |
| Behavioral | Windows logon event 4624 whose source address falls within the IndustrialPI device range. |
| Behavioral | SSH, SMB, RDP, or WinRM session sourced from an IndustrialPI address to any adjacent host. |
| Behavioral | Outbound session from an IndustrialPI address to an internet-routable destination that is not a documented vendor or cloud endpoint. |
| Behavioral | Regular-interval outbound sessions with consistent low byte counts from the device address range, consistent with command-and-control beaconing. |
| Behavioral | SNMP coldStart (1.3.6.1.6.3.1.1.5.1) or warmStart (1.3.6.1.6.3.1.1.5.2) trap from an IndustrialPI outside a maintenance window, or an unexplained sysUpTime reset. |
| Behavioral | Sustained rise in IF-MIB::ifHCOutOctets on the switch port facing an IndustrialPI with no corresponding production change. |
| Behavioral | Modbus write function codes 5, 6, 15, 16, 22, 23 or CIP Set Attribute services 0x04 / 0x10 issued from a device whose baseline shows read-only behavior. |
| Behavioral | Controller mode change, project download, or code revision on a controller reachable from an IndustrialPI with no matching change record. |
| Behavioral | Device still reporting webstatus below 2.4.6 or Firmware Bullseye 2024-08 or earlier while showing any management-port connection from outside the engineering range. |