Skip to content

Add OPC-UA endpoint enumeration auxiliary scanner module - #21829

Open
ethan-thomason wants to merge 1 commit into
rapid7:masterfrom
ethan-thomason:opcua-getendpoints-v2
Open

Add OPC-UA endpoint enumeration auxiliary scanner module#21829
ethan-thomason wants to merge 1 commit into
rapid7:masterfrom
ethan-thomason:opcua-getendpoints-v2

Conversation

@ethan-thomason

Copy link
Copy Markdown
Contributor

Summary

Adds auxiliary/scanner/scada/opcua_endpoint_enum, which enumerates the endpoints an OPC-UA server advertises and reports the security posture of each one.

This follows up #21612 (opcua_enum), which detects OPC-UA servers via the HEL/ACK handshake. This module goes a step further: it opens a secure channel with SecurityPolicy=None and calls GetEndpoints, then reports each endpoint's URL, MessageSecurityMode, SecurityPolicyUri, and accepted UserIdentityToken types, plus the server's ApplicationUri and ProductUri as a fingerprint.

Endpoints accepting the Anonymous identity token over a channel with MessageSecurityMode None are flagged and recorded via report_vuln - any host that can reach the port can connect with no credentials over an unencrypted channel, which is generally enough to read live process data and, depending on node permissions, write it.

Why a separate module rather than an action on opcua_enum

opcua_enum succeeds against servers where this one fails, so it isn't superseded. HEL/ACK is a transport handshake with no security negotiation, while GetEndpoints requires an OpenSecureChannel. The specification requires the discovery endpoint to accept SecurityPolicy=None precisely so clients can learn how to connect, but hardened deployments restrict it. opcua_enum remains the detector that works when the channel is closed.

Verification

Tested against two independent OPC-UA implementations:

node-opcua (7 endpoints, 3 security policy families, includes an unsecured endpoint):

msf6 auxiliary(scanner/scada/opcua_endpoint_enum) > run

[+] 127.0.0.1:4842        - OPC-UA server enumerated - 7 endpoint(s), 1 unauthenticated and unencrypted
[*] 127.0.0.1:4842        -   [0] opc.tcp://190d0c6ea827:4840/UA/BackdraftTest
[*] 127.0.0.1:4842        -       security: None/None  identity: UserName, Certificate, Anonymous
[!] 127.0.0.1:4842        -       endpoint accepts anonymous clients over an unencrypted channel
[*] 127.0.0.1:4842        -   [1] opc.tcp://190d0c6ea827:4840/UA/BackdraftTest
[*] 127.0.0.1:4842        -       security: Basic256Sha256/Sign  identity: UserName, Certificate, Anonymous
...
[*] 127.0.0.1:4842        -   ApplicationUri: urn:190d0c6ea827:NodeOPCUA-Server

The parsed endpoint list matches the server's own startup log line for line, including ordering and the Aes128/Aes256 policy URIs.

Inductive Automation Ignition 8.3.4 (3 endpoints, all Basic256Sha256/SignAndEncrypt, no unsecured endpoint):

[+] 10.10.0.3:62541       - OPC-UA server enumerated - 3 endpoint(s), 0 unauthenticated and unencrypted
[*] 10.10.0.3:62541       -   [0] opc.tcp://bd-83-primary:62541
[*] 10.10.0.3:62541       -       security: Basic256Sha256/SignAndEncrypt  identity: UserName

Worth noting that the endpoint list is returned even though no endpoint offers the None policy - the discovery channel is open by specification regardless of what the server's real endpoints require.

Output was also diffed field by field against an independent Python implementation using the asyncua stack, and matches.

Notes for reviewers

  • No new gem dependencies. Binary framing is hand-built with pack/unpack, following the iec104 style.
  • Reads accumulate until the full message length is received. GetEndpoints responses carry a certificate per endpoint and routinely span TCP segments; the largest observed was 10858 bytes.
  • Chunk reassembly is implemented but has not been exercised in testing - every server tested negotiated a buffer large enough to return a single F chunk. That path is written from Part 6 rather than validated on the wire.
  • Array lengths and all reads are bounds-checked, so a malformed or hostile response raises a parse error rather than allocating without bound.
  • CloseSecureChannel is sent so channels are released rather than left to expire.
  • Docker recipes for both test servers are in the module documentation.

@bwatters-r7 bwatters-r7 self-assigned this Aug 26, 2026
@bwatters-r7 bwatters-r7 added the rn-modules release notes for new or majorly enhanced modules label Aug 26, 2026
@bwatters-r7
bwatters-r7 requested a lite review from Copilot August 26, 2026 22:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 enumerate OPC-UA server endpoints via OpenSecureChannel + GetEndpoints, report each endpoint’s advertised security settings and identity token types, and record weak (anonymous + unencrypted) endpoints as vulns; includes accompanying module documentation.

Changes:

  • Introduces auxiliary/scanner/scada/opcua_endpoint_enum implementing OPC-UA TCP framing, chunk reassembly, and binary decoding for GetEndpoints.
  • Reports endpoint security posture (policy/mode, identity token types) to console and Metasploit DB (report_service, report_note, report_vuln).
  • Adds module documentation with usage, port notes, test setup guidance, and scenarios.

Impact Analysis:

  • Blast radius: low; new auxiliary module and new documentation only.
  • Data and contract effects: low; adds new service/note/vuln records in DB for hosts scanned, no schema changes visible in diff.
  • Rollback and test focus: rollback is deletion/revert of new files; focus validation on weak-endpoint classification logic and documentation accuracy.

Reviewed changes

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

File Description
modules/auxiliary/scanner/scada/opcua_endpoint_enum.rb New OPC-UA endpoint enumeration scanner with binary transport parsing and weak-endpoint reporting.
documentation/modules/auxiliary/scanner/scada/opcua_endpoint_enum.md New user-facing documentation covering behavior, options, setup, and example runs.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +542 to +544
def unencrypted?(endpoint)
endpoint[:security_mode_name] == 'None' || endpoint[:security_policy_name] == 'None'
end
Comment on lines +64 to +68
### Setting Up a Test Server

Any OPC-UA server will exercise the module. Two convenient options:

**Inductive Automation Ignition (Docker)**
Comment on lines +1 to +6
## Vulnerable Application

This module enumerates the endpoints advertised by an OPC-UA server over the
OPC-UA TCP binary transport (`opc.tcp://`). OPC-UA (IEC 62541) is the dominant
interoperability standard in industrial automation and is exposed by PLCs, SCADA
platforms, historians, and gateway products.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also, run msftidy_docs.rb

@bwatters-r7 bwatters-r7 added the group-review PRs flagged to get a group review during our weekly module hacking meeting. label Aug 26, 2026

@bwatters-r7 bwatters-r7 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd very much like to see the parsing stuff in it's own library, especially since I'd also like to see rspec testing to verify it.

Comment on lines +187 to +188
skip_string if (encoding & 0x40).positive?
skip(4) if (encoding & 0x80).positive?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there any chance these are reversed?
I read 0x80 means NamespaceUri is next and 0x40 means ServerIndex is next?
Citation: https://reference.opcfoundation.org/specs/OPC-10000-6/5.2.2.9

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, and you read it exactly right. Part 6 §5.2.2.9 has NamespaceUri = 0x80 and ServerIndex = 0x40, so a set 0x80 means a String follows and 0x40 means a UInt32. I had the two actions mapped to the wrong bits - skipping a string on 0x40 and four bytes on 0x80, the reverse of what it should be. Now fixed:

skip_string if (encoding & 0x80).positive?    # NamespaceUri (String)
skip(4) if (encoding & 0x40).positive?        # ServerIndex (UInt32)

It slipped through because every server I tested returns endpoint NodeIds with neither flag set (namespace 0, no server index), so that branch never executed in the lab - exactly the kind of untested path a spec read catches and a live test doesn't. Appreciate you going to the reference.

I also checked the merged opcua_enum for the same pattern - it only does the HEL/ACK handshake and never parses NodeIds, so nothing there needs the correction. This was contained to this PR.

Done: re-ran msftidy_docs.rb (clean), rebased onto current master to pick up yesterday's CI fix, and force-pushed. Should be a clean single commit on a current base now.

@bwatters-r7

Copy link
Copy Markdown
Contributor

One last note- I think we found the bug from yesterday that was causing the test issues and landed a fix for it just after you put up this PR. Could I ask you to please rebase this PR when you make those changes so we can avoid the CI issues we had yesterday?

@ethan-thomason
ethan-thomason force-pushed the opcua-getendpoints-v2 branch 2 times, most recently from 7b83d7a to d23c70b Compare August 26, 2026 23:40
@ethan-thomason

Copy link
Copy Markdown
Contributor Author

One last note- I think we found the bug from yesterday that was causing the test issues and landed a fix for it just after you put up this PR. Could I ask you to please rebase this PR when you make those changes so we can avoid the CI issues we had yesterday?

Done

@ethan-thomason

Copy link
Copy Markdown
Contributor Author

I'd very much like to see the parsing stuff in it's own library, especially since I'd also like to see rspec testing to verify it.

Happy to do that. Would you rather it land in this PR, or as a follow-up that extracts the parser from both opcua_enum and opcua_endpoint_enum together? The library only makes sense covering both, and that means touching the already-merged one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

group-review PRs flagged to get a group review during our weekly module hacking meeting. rn-modules release notes for new or majorly enhanced modules

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

3 participants