Add OPC-UA endpoint enumeration auxiliary scanner module - #21829
Add OPC-UA endpoint enumeration auxiliary scanner module#21829ethan-thomason wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
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_enumimplementing OPC-UA TCP framing, chunk reassembly, and binary decoding forGetEndpoints. - 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.
| def unencrypted?(endpoint) | ||
| endpoint[:security_mode_name] == 'None' || endpoint[:security_policy_name] == 'None' | ||
| end |
| ### Setting Up a Test Server | ||
|
|
||
| Any OPC-UA server will exercise the module. Two convenient options: | ||
|
|
||
| **Inductive Automation Ignition (Docker)** |
| ## 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. |
There was a problem hiding this comment.
Also, run msftidy_docs.rb
bwatters-r7
left a comment
There was a problem hiding this comment.
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.
| skip_string if (encoding & 0x40).positive? | ||
| skip(4) if (encoding & 0x80).positive? |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
|
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? |
7b83d7a to
d23c70b
Compare
Done |
d23c70b to
9710f8d
Compare
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. |
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 withSecurityPolicy=Noneand callsGetEndpoints, then reports each endpoint's URL,MessageSecurityMode,SecurityPolicyUri, and acceptedUserIdentityTokentypes, plus the server'sApplicationUriandProductUrias a fingerprint.Endpoints accepting the Anonymous identity token over a channel with
MessageSecurityModeNone are flagged and recorded viareport_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_enumsucceeds against servers where this one fails, so it isn't superseded. HEL/ACK is a transport handshake with no security negotiation, whileGetEndpointsrequires anOpenSecureChannel. The specification requires the discovery endpoint to acceptSecurityPolicy=Noneprecisely so clients can learn how to connect, but hardened deployments restrict it.opcua_enumremains 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):
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):
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
asyncuastack, and matches.Notes for reviewers
pack/unpack, following theiec104style.GetEndpointsresponses carry a certificate per endpoint and routinely span TCP segments; the largest observed was 10858 bytes.Fchunk. That path is written from Part 6 rather than validated on the wire.CloseSecureChannelis sent so channels are released rather than left to expire.