Skip to content

Add Ignition gateway fingerprint scanner (auxiliary/scanner/scada/ignition_statusping)#21273

Open
ethan-thomason wants to merge 2 commits into
rapid7:masterfrom
ethan-thomason:ignition-scada-modules
Open

Add Ignition gateway fingerprint scanner (auxiliary/scanner/scada/ignition_statusping)#21273
ethan-thomason wants to merge 2 commits into
rapid7:masterfrom
ethan-thomason:ignition-scada-modules

Conversation

@ethan-thomason
Copy link
Copy Markdown

Summary

Adds an auxiliary scanner module that fingerprints Inductive Automation Ignition
gateways across all major version families by probing unauthenticated info endpoints.

Details

Ignition's version info endpoint has changed path and format across major releases:

  • 7.9.x — /main/system/gwinfo (key=value)
  • 8.0.x — /system/gwinfo (key=value, per existing inductive_ignition_rce module)
  • 8.1.x — /system/StatusPing (JSON)
  • 8.3.x — /system/gwinfo (key=value, additional fields)

The module probes all three paths in sequence, handles both response formats,
and reports version, run state, OS, Java runtime, and GAN redundancy role to
the MSF database. Complements exploit/multi/scada/inductive_ignition_rce without
duplicating it — targets different version ranges and serves as a fingerprinting
precursor to the 8.1.x CVE modules to follow.

Verification

Tested against:

  • Ignition 7.9.21 (Linux)
  • Ignition 8.1.15 (Linux)
  • Ignition 8.1.17 (Linux)
  • Ignition 8.3.4 (Linux)

References

…ition_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/
@github-actions
Copy link
Copy Markdown

Thanks for your pull request! Before this can be merged, we need the following documentation for your module:

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Metasploit auxiliary scanner module to fingerprint Inductive Automation Ignition gateways by probing version-specific unauthenticated info endpoints, plus accompanying user documentation.

Changes:

  • Introduces auxiliary/scanner/scada/ignition_statusping to probe /system/gwinfo, /system/StatusPing, and /main/system/gwinfo and report discovered gateway details.
  • Adds module documentation describing version/endpoint differences, usage, and example output.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 9 comments.

File Description
modules/auxiliary/scanner/scada/ignition_statusping.rb New HTTP scanner module that probes multiple Ignition info endpoints and reports fingerprint data to console/DB.
documentation/modules/auxiliary/scanner/scada/ignition_statusping.md New documentation covering target behavior, verification steps, options, and scenarios.

Comment on lines +146 to +150
break # found it, don't probe further
end

vprint_status("#{ip}:#{rport} - No Ignition endpoint responded")
end
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module always prints No Ignition endpoint responded after the probe loop, even when a probe succeeds (the break only exits the loop, not run_host). This leads to misleading output for every detected gateway; track whether a match was found and only print this message when none matched, or return immediately after reporting a successful fingerprint.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +4
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New Ruby files in this repo should include # frozen_string_literal: true at the very top of the file (magic comment must be first line, before other comments).

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +29
For 8.1.x CVE modules see auxiliary/scanner/scada/ignition_auth_bypass and
auxiliary/scanner/scada/ignition_deser_check.
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This description references auxiliary modules auxiliary/scanner/scada/ignition_auth_bypass and auxiliary/scanner/scada/ignition_deser_check, but those modules are not present in the codebase. Please remove or update these references (or add the referenced modules in the same PR if intended) to avoid confusing users.

Suggested change
For 8.1.x CVE modules see auxiliary/scanner/scada/ignition_auth_bypass and
auxiliary/scanner/scada/ignition_deser_check.
For 8.1.x, consult the relevant Ignition security research or available
Metasploit modules for the target version.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +85
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
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse_statusping is extracting JSON fields via regex and broad rescue StandardError. Per project guidance, prefer parsing HTTP JSON responses via res.get_json_document (and handle parse failure explicitly) rather than regexing JSON text; this is more robust across formatting/ordering changes and avoids silently masking unexpected errors.

Suggested change
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
document = if body.respond_to?(:get_json_document)
body.get_json_document
else
JSON.parse(body)
end
info = {}
%w[version state role peerAddress os].each do |key|
info[key] = document[key] if document.is_a?(Hash) && document[key]
end
info
rescue JSON::ParserError
{}

Copilot uses AI. Check for mistakes.
Comment on lines +135 to +137
info_str = build_info_string(version, state, os, runtime, role, peer)
print_good("#{ip}:#{rport} - #{info_str}")

Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid printing host info as #{ip}:#{rport} because it doesn't handle IPv6 correctly. Use peer / Rex::Socket.to_authority(ip, rport) consistently for output messages.

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

report_host is intended for OS fingerprint attributes (e.g., os_name like "Linux"/"Windows"). Setting os_name to the product name (Ignition Gateway) will pollute OS reporting in the database. Consider either reporting the actual OS (from the response) in os_name/os_flavor, and storing the Ignition product/version in info or a report_note, or omit report_host entirely and rely on report_service/notes.

Suggested change
report_host(host: ip, os_name: 'Ignition Gateway', os_flavor: version)

Copilot uses AI. Check for mistakes.
host: ip,
port: rport,
proto: 'tcp',
name: 'http',
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reported service is always named http, but this module can be run with SSL enabled (and common Ignition ports include 443/8043). Use ssl ? 'https' : 'http' (or equivalent) so the database service name matches the actual transport.

Suggested change
name: 'http',
name: ssl ? 'https' : 'http',

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +63
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
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation examples use a public IP address (159.203.120.32). Project guidance is to use RFC 5737 TEST-NET ranges (e.g., 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24) for example/non-routable addresses in docs to avoid referencing real hosts.

Suggested change
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
msf6 auxiliary(scanner/scada/ignition_statusping) > set RHOSTS 192.0.2.32
RHOSTS => 192.0.2.32
msf6 auxiliary(scanner/scada/ignition_statusping) > run
[+] 192.0.2.32:8088 - Ignition 7.9.21 | State: RUNNING | OS: Linux | GAN role: Independent

Copilot uses AI. Check for mistakes.
Comment on lines +111 to +131
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
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the StatusPing (JSON) probe path, runtime is never populated, so build_info_string can never include the Java/runtime version for 8.1.x responses. This conflicts with the module description/docs claiming Java runtime is extracted; either parse the runtime field from the JSON response (if present) or adjust the description/docs accordingly.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

4 participants