By RUGERO Tesla (@404Saint).
[*] Target Server: opc.tcp://127.0.0.1:4840
[*] Sending unauthenticated GetEndpointsRequest...
[+] Received 5 exposed endpoints.
Five endpoints from a single unauthenticated GetEndpointsRequest. That was the starting point for this OPC UA laboratory. The response advertised four cryptographically protected configurations and one endpoint using:
SecurityMode: None
SecurityPolicy: None
The server was therefore not enforcing a single security posture at the discovery layer. A client could select the unprotected endpoint even though Sign and SignAndEncrypt configurations were available.
Endpoint discovery showing the five configurations advertised by the laboratory server.
That observation alone was not treated as a downgrade vulnerability. No active downgrade attack was performed. The finding was simpler: an unencrypted endpoint was explicitly available for client selection.
The next step was to use it.
From GetEndpoints to ActivateSession
OPC UA establishes application sessions through a defined sequence:
OpenSecureChannel
↓
CreateSession
↓
ActivateSession
Using the SecurityPolicy=None endpoint, an anonymous identity token was supplied during ActivateSession.
The server accepted the session:
[+] SecureChannel established
[+] Session activation succeeded
SecureChannel State: OPEN (SecurityPolicy#None)
Session State: ACTIVE
Authentication Type: Anonymous
Successful anonymous session establishment over the SecurityPolicy=None endpoint.
At this point the scope of the test changed. Endpoint discovery had established insecure exposure. Session establishment demonstrated that the exposed endpoint was actually usable without an authenticated identity. The address space was the next layer.
Walking the Address Space
OPC UA does not present an industrial control environment as a flat register table.
Its application model is an address space containing Objects, Variables, Methods, Properties, References, and associated metadata.
A recursive traversal was therefore used to enumerate the Object graph and inspect the variables reachable through the anonymous session.
The crawler followed child references and collected:
Browse Name
NodeId
Node Class
AccessLevel
UserAccessLevel
Current Value
Read Result
Relative browse-path resolution was also tested against the standard Server object:
0:Objects
└── 0:Server
└── 0:ServerStatus
└── 0:CurrentTime
which resolved to:
ns=0;i=2258
The traversal itself was not the interesting finding.
Most of the address space did not provide unrestricted access. Several nodes rejected reads or returned authorization-related errors.
That distinction became important when evaluating write permissions.
AccessLevel Wasn't the Final Verdict
The audit did not classify a variable as writable solely because an attribute suggested that it was. Readable variables were tested with actual WriteRequest operations.
The mutations were deliberately small and reversible:
Boolean
False → True
integer
0 → 1
Double
0.0 → 1.0
Int64
0 → 1
example bytestring
b'test123' → b'test123_probe'
Each successful mutation was immediately restored to its original value.
The result was unambiguous:
[CRITICAL VULNERABILITY] Write ACCEPTED on tag Boolean!
Value changed: False -> True
[CRITICAL VULNERABILITY] Write ACCEPTED on tag integer!
Value changed: 0 -> 1
[CRITICAL VULNERABILITY] Write ACCEPTED on tag Double!
Value changed: 0.0 -> 1.0
[CRITICAL VULNERABILITY] Write ACCEPTED on tag Int64!
Value changed: 0 -> 1
[CRITICAL VULNERABILITY] Write ACCEPTED on tag example bytestring!
Value changed: b'test123' -> b'test123_probe'
Recursive address-space enumeration and access-level inspection from the anonymous session.
Successful write mutations against five tested variables. Every mutation was reverted after verification.
This is stronger evidence than simply reporting an exposed AccessLevel.
The server received the write request, accepted it, value changed and the original value was then restored. The finding is therefore specific to effective write authorization: the anonymous session was able to modify the tested variables without presenting an authenticated user identity.
The Security Boundaries Were Not Universally Broken
The write finding made it important to test the other security controls rather than assume that everything was misconfigured.
The endpoint configuration exposed SecurityPolicy=None, but protected configurations were also available:
SignAndEncrypt Aes128_Sha256_RsaOaep
Sign Aes128_Sha256_RsaOaep
SignAndEncrypt Basic256Sha256
Sign Basic256Sha256
An independent X.509 certificate was then generated for the certificate validation test.
It was deliberately unrelated to the earlier test certificate:
Subject:
CN=independent-untrusted-client
Issuer:
CN=independent-untrusted-client
Self-Signed:
True
Application URI:
urn:freeopcua:client:independent-test
The protected connection attempt used:
Basic256Sha256
SignAndEncrypt
The server rejected the certificate:
BadCertificateUriInvalid
Independent self-signed certificate rejected during the protected OPC UA handshake.
Invalid username/password authentication was also rejected with:
BadUserAccessDenied
The resulting security picture was therefore more nuanced than a simple “OPC UA authentication is broken” conclusion.
| Security Boundary | Result |
|---|---|
SecurityPolicy=None advertised |
Insecure exposure |
| Anonymous session | Accepted |
| Address-space traversal | Extensive visibility |
| Tested anonymous writes | Accepted |
| Invalid credentials | Rejected |
| Independent X.509 certificate | Rejected |
| Protected endpoints | Available |
The significant finding was the combination of an anonymously accessible session and effective write permissions on the tested variables.
Seeing the Protocol Instead of Just the API
The packet captures were an important part of the investigation.
For example, the username authentication test produced an ActivateSessionRequest containing a UserNameIdentityToken:
UserName: admin
Password: ...
EncryptionAlgorithm:
http://www.w3.org/2001/04/xmlenc#rsa-oaep
The client reported BadUserAccessDenied, but the PCAP showed how the identity token was actually represented on the wire. That distinction matters in protocol research. A high-level library can tell you that an operation succeeded or failed but a packet capture can show the actual message, security mode, token type, request structure, and server response.
The laboratory therefore combined both:
Python harness
+
OPC UA server
+
Wireshark / PCAP
+
manual protocol inspection
rather than treating the client library as the protocol itself.
The Research Repository
The complete laboratory is available in the repository:
industrial-protocol-labs / opcua-research
It contains the phase-by-phase notes, Python research harnesses, packet captures, screenshots, logs, and reproduction instructions.
opcua-research/
├── captures/
├── logs/
├── notes/
├── screenshots/
└── scripts/
The detailed notes cover:
- OPC UA architecture and security model
- Endpoint discovery
- SecureChannel and session establishment
- Address-space traversal
- Access-level auditing
- Effective write authorization
- X.509 trust-boundary testing
- Laboratory reproduction
The interesting part of this research was not any individual API call. It was following the protocol far enough to determine what an apparently ordinary anonymous session could actually do.
GetEndpoints was only the first request.





Top comments (0)