From d83c3fe22bd7229c59c26ab091872304afe96e94 Mon Sep 17 00:00:00 2001 From: ethan-thomason Date: Sun, 12 Apr 2026 09:59:48 -0400 Subject: [PATCH 1/2] Add Ignition gateway fingerprint scanner (auxiliary/scanner/scada/ignition_statusping) Fingerprints Inductive Automation Ignition gateways across all major versions by probing version-specific unauthenticated info endpoints. Tested against: - Ignition 7.9.21 (/main/system/gwinfo, key=value) - Ignition 8.1.15 (/system/StatusPing, JSON) - Ignition 8.1.17 (/system/StatusPing, JSON) - Ignition 8.3.4 (/system/gwinfo, key=value) Reference: https://ethomason.com/posts/fingerprinting-ignition-gateways/ --- .../scanner/scada/ignition_statusping.rb | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 modules/auxiliary/scanner/scada/ignition_statusping.rb diff --git a/modules/auxiliary/scanner/scada/ignition_statusping.rb b/modules/auxiliary/scanner/scada/ignition_statusping.rb new file mode 100644 index 0000000000000..7f57fd86faa66 --- /dev/null +++ b/modules/auxiliary/scanner/scada/ignition_statusping.rb @@ -0,0 +1,151 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Auxiliary + include Msf::Exploit::Remote::HttpClient + include Msf::Auxiliary::Scanner + include Msf::Auxiliary::Report + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Inductive Automation Ignition Gateway Fingerprint', + 'Description' => %q{ + Fingerprints Inductive Automation Ignition gateways across all major versions + by probing version-specific info endpoints. Extracts version, run state, OS, + Java runtime, and GAN redundancy role without authentication. + + Endpoint and format by version: + 7.9.x — /main/system/gwinfo (key=value) + 8.0.x — /system/gwinfo (key=value) + 8.1.x — /system/StatusPing (JSON) + 8.3.x — /system/gwinfo (key=value, includes RuntimeVersion/RequireSsl) + + For 8.0.x exploitation see exploit/multi/scada/inductive_ignition_rce. + For 8.1.x CVE modules see auxiliary/scanner/scada/ignition_auth_bypass and + auxiliary/scanner/scada/ignition_deser_check. + }, + 'Author' => ['Ethan Thomason '], + 'License' => MSF_LICENSE, + 'References' => [ + ['URL', 'https://ethomason.com/posts/fingerprinting-ignition-gateways/'], + ], + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [IOC_IN_LOGS], + 'Reliability' => [] + } + ) + ) + register_options([Opt::RPORT(8088)]) + end + + # Parse key=value format used by 7.9.x, 8.0.x, 8.3.x + # e.g. ContextStatus=RUNNING;Version=8.3.4;OS=Linux;RuntimeVersion=17.0.17 + def parse_gwinfo(body) + info = {} + body.split(';').each do |pair| + k, v = pair.split('=', 2) + info[k.strip] = v.to_s.strip if k + end + info + end + + # Parse JSON format used by 8.1.x StatusPing + def parse_statusping(body) + info = {} + info['version'] = begin + body.match(/"version"\s*:\s*"([^"]+)"/)[1] + rescue StandardError + nil + end + info['state'] = begin + body.match(/"state"\s*:\s*"([^"]+)"/)[1] + rescue StandardError + nil + end + info['role'] = begin + body.match(/"role"\s*:\s*"([^"]+)"/)[1] + rescue StandardError + nil + end + info['peerAddress'] = begin + body.match(/"peerAddress"\s*:\s*"([^"]+)"/)[1] + rescue StandardError + nil + end + info['os'] = begin + body.match(/"os"\s*:\s*"([^"]+)"/)[1] + rescue StandardError + nil + end + info.compact + end + + def build_info_string(version, state, os, runtime, role, peer) + parts = ["Ignition #{version}"] + parts << "State: #{state}" if state + parts << "OS: #{os}" if os + parts << "Java: #{runtime}" if runtime + parts << "GAN role: #{role}" if role + parts << "Peer: #{peer}" if peer && role && role !~ /independent/i + parts.join(' | ') + end + + def run_host(ip) + # Probe order: gwinfo covers 7.9/8.0/8.3, StatusPing covers 8.1 + probes = [ + { uri: '/system/gwinfo', format: :kvp }, + { uri: '/system/StatusPing', format: :json }, + { uri: '/main/system/gwinfo', format: :kvp }, + ] + + probes.each do |probe| + res = send_request_cgi({ 'method' => 'GET', 'uri' => probe[:uri] }) + next unless res && res.code == 200 + next if res.body.strip.empty? + + version = state = os = runtime = role = peer = nil + + if probe[:format] == :kvp + d = parse_gwinfo(res.body) + version = d['Version'] + state = d['ContextStatus'] + os = d['OS'] + runtime = d['RuntimeVersion'] + role = d['RedundancyStatus'] + # gwinfo doesn't expose peer address directly + elsif probe[:format] == :json + # Skip if this is just the minimal 8.3.x StatusPing stub + next if res.body.strip == '{"state":"RUNNING"}' + + d = parse_statusping(res.body) + version = d['version'] + state = d['state'] + os = d['os'] + role = d['role'] + peer = d['peerAddress'] + end + + next unless version + + info_str = build_info_string(version, state, os, runtime, role, peer) + print_good("#{ip}:#{rport} - #{info_str}") + + report_host(host: ip, os_name: 'Ignition Gateway', os_flavor: version) + report_service( + host: ip, + port: rport, + proto: 'tcp', + name: 'http', + info: info_str + ) + break # found it, don't probe further + end + + vprint_status("#{ip}:#{rport} - No Ignition endpoint responded") + end +end From c429a246dfe8c6c8b27374c6224977b5f6526754 Mon Sep 17 00:00:00 2001 From: ethan-thomason Date: Mon, 13 Apr 2026 08:06:23 -0400 Subject: [PATCH 2/2] Add documentation for ignition_statusping module --- .../scanner/scada/ignition_statusping.md | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 documentation/modules/auxiliary/scanner/scada/ignition_statusping.md diff --git a/documentation/modules/auxiliary/scanner/scada/ignition_statusping.md b/documentation/modules/auxiliary/scanner/scada/ignition_statusping.md new file mode 100644 index 0000000000000..55acee9a485d1 --- /dev/null +++ b/documentation/modules/auxiliary/scanner/scada/ignition_statusping.md @@ -0,0 +1,90 @@ +## Vulnerable Application + +Inductive Automation Ignition is a widely deployed SCADA platform used in critical +infrastructure worldwide. This module targets an unauthenticated information disclosure +present across all major Ignition versions — the gateway exposes version, run state, +OS, Java runtime, and GAN redundancy topology without any credentials. + +Ignition can be downloaded from [inductiveautomation.com](https://inductiveautomation.com/downloads/ignition). +A free 2-hour trial license is available and resets on restart, which is sufficient +for testing purposes. + +The endpoint and response format differ by version: + +| Version | Endpoint | Format | +|---|---|---| +| 7.9.x | `/main/system/gwinfo` | semicolon-delimited key=value | +| 8.0.x | `/system/gwinfo` | semicolon-delimited key=value | +| 8.1.x | `/system/StatusPing` | JSON | +| 8.3.x | `/system/gwinfo` | semicolon-delimited key=value | + +This module has been tested against the following Ignition versions on Linux: + +* 7.9.21 +* 8.1.15 +* 8.1.17 +* 8.3.4 + +8.0.x behavior is inferred from the source of the existing +`exploit/multi/scada/inductive_ignition_rce` module, which uses `/system/gwinfo` +for version detection prior to exploitation. + +## Verification Steps + +1. Install Ignition (any version 7.9+) and complete initial gateway commissioning +2. Start msfconsole +3. `use auxiliary/scanner/scada/ignition_statusping` +4. `set RHOSTS ` +5. `run` +6. The module should return gateway version, state, OS, Java runtime, and GAN role + +## Options + +### RHOSTS + +The target host(s) or CIDR range to scan. Supports standard MSF RHOSTS syntax +including comma-separated IPs and CIDR notation (e.g. `10.10.0.0/24`). + +### RPORT + +The Ignition gateway HTTP port. Default: `8088`. Ignition can be configured to run +on alternate ports — common alternatives include `80`, `443`, `8043`. + +## Scenarios + +### Ignition 7.9.21 — Single host + +``` +msf6 > use auxiliary/scanner/scada/ignition_statusping +msf6 auxiliary(scanner/scada/ignition_statusping) > set RHOSTS 159.203.120.32 +RHOSTS => 159.203.120.32 +msf6 auxiliary(scanner/scada/ignition_statusping) > run + +[+] 159.203.120.32:8088 - Ignition 7.9.21 | State: RUNNING | OS: Linux | GAN role: Independent +[*] Scanned 1 of 1 hosts (100% complete) +[*] Auxiliary module execution completed +``` + +### Mixed version network scan — CIDR range + +The following shows a scan across a /24 containing gateways at multiple versions, +including an 8.3.4 GAN redundancy pair (Master + Backup): + +``` +msf6 > use auxiliary/scanner/scada/ignition_statusping +msf6 auxiliary(scanner/scada/ignition_statusping) > set RHOSTS 10.10.0.0/24 +RHOSTS => 10.10.0.0/24 +msf6 auxiliary(scanner/scada/ignition_statusping) > run + +[+] 10.10.0.3:8088 - Ignition 8.3.4 | State: RUNNING | OS: Linux | Java: 17.0.17 | GAN role: Master +[+] 10.10.0.4:8088 - Ignition 8.3.4 | State: RUNNING | OS: Linux | Java: 17.0.17 | GAN role: Backup +[+] 10.10.0.7:8088 - Ignition 7.9.21 | State: RUNNING | OS: Linux | GAN role: Independent +[+] 10.10.0.8:8088 - Ignition 8.1.15 | State: RUNNING | OS: Linux | Java: 11.0.14.1 | GAN role: Independent +[*] Scanned 256 of 256 hosts (100% complete) +[*] Auxiliary module execution completed +``` + +The GAN role output (Master/Backup) identifies redundancy pairs and reveals the network +topology of the Ignition deployment without authentication. This complements +`exploit/multi/scada/inductive_ignition_rce` (CVE-2020-10644), which targets 8.0.x only, +by extending fingerprinting coverage to 7.9.x, 8.1.x, and 8.3.x.