4. OPC UA匿名登录与节点遍历测试

OPC UA匿名登录与节点遍历测试 学习卡片

OPC UA 匿名登录与节点遍历测试

OPC UA 服务器通常通过 SecurityPolicy 与 UserTokenPolicy 控制访问。当服务器允许 Anonymous 用户令牌且未对地址空间做权限裁剪时,任何能访问 4840 端口的客户端都可以读取过程变量。本例使用 opcua-asyncio 客户端完成匿名连接、地址空间遍历与关键变量读取,并据此评估未授权数据暴露风险。

测试前提:目标 OPC UA 服务器已开启匿名访问(Anonymous 令牌策略),且测试方已获得授权。若服务器仅允许用户名/密码或证书认证,匿名连接会返回 BadIdentityTokenInvalid 或 BadSecurityPolicyRejected,此时应记录为“匿名访问被拒绝”,而非继续尝试绕过。

连接与遍历实现

核心流程分三步:建立匿名会话 → 从 Objects 节点递归遍历 → 对命中变量节点执行 read_value。以下脚本可直接运行,关键参数在顶部集中定义。

import asyncio
from asyncua import Client, ua

ENDPOINT = "opc.tcp://192.168.10.20:4840"
MAX_DEPTH = 4
KEYWORDS = ("temp", "press", "flow", "level", "speed", "rpm", "valve")

async def walk(node, depth, hits):
    if depth > MAX_DEPTH:
        return
    for child in await node.get_children():
        try:
            bn = await child.read_browse_name()
            name = bn.Name
            if any(k in name.lower() for k in KEYWORDS):
                try:
                    val = await child.read_value()
                    hits.append((child.nodeid.to_string(), name, val))
                except ua.UaStatusCodeError as e:
                    hits.append((child.nodeid.to_string(), name, f"<read denied: {e.code}>"))
            await walk(child, depth + 1, hits)
        except ua.UaStatusCodeError:
            continue

async def main():
    async with Client(url=ENDPOINT) as client:
        # 匿名登录:不设置 user 与 password
        print("session:", client.session)
        objects = client.get_objects_node()
        hits = []
        await walk(objects, 0, hits)
        print(f"matched variables: {len(hits)}")
        for nid, name, val in hits:
            print(f"{nid:<40} {name:<24} {val}")

asyncio.run(main())

脚本中 Client(url=...) 未传入 user/password,即使用匿名令牌;get_objects_node() 返回 Objects 文件夹作为遍历根;MAX_DEPTH 限制递归深度,避免在大型地址空间中无限展开;KEYWORDS 用于筛选过程变量,减少无关节点输出。

结果判读与风险分级

运行后观察三类信号:会话是否建立、命中变量数量、读取是否被拒绝。若匿名会话成功且能读到过程变量,说明服务器未对匿名用户做地址空间裁剪,属于未授权数据暴露;若会话建立但读取返回 BadUserAccessDenied,说明认证层放行但节点级权限生效,风险较低;若匿名连接直接失败,则匿名访问已被禁用。

opcua-asyncio 客户端 Client(url=...) 无 user / password Anonymous 令牌 opc.tcp://…:4840 OPC UA 服务器 SecurityPolicy: None/Basic256 UserTokenPolicy: Anonymous AddressSpace: Objects 根 CreateSession ActivateSession 地址空间遍历 get_children() 递归 MAX_DEPTH = 4 read_browse_name() 匹配 temp/press/flow/level… read_value() 读取 匿名会话建立成功 继续遍历与读取 匿名被拒绝 BadIdentityTokenInvalid 会话成功但读取被拒 BadUserAccessDenied 读到过程变量 未授权数据暴露 OK 拒绝 部分放行 暴露

风险分级与处置建议

观察结果风险等级处置建议
匿名会话成功,读到过程变量高禁用匿名令牌,或对匿名用户裁剪地址空间,仅暴露必要只读节点
匿名会话成功,读取返回 BadUserAccessDenied中保留节点级权限,同时审计匿名会话是否可枚举节点结构
匿名连接被拒绝低维持现有认证策略,定期复核 UserTokenPolicy 配置
遍历深度过大或节点数量过多时,脚本可能长时间运行并产生大量请求。建议先用 MAX_DEPTH=2 试探,确认服务器响应正常后再逐步加深;对生产服务器应控制请求频率,避免影响正常通信。

验证要点

  • 确认 Client 构造时未传入 user 与 password,否则测的是凭据登录而非匿名访问。
  • 记录 client.session 是否成功建立,作为匿名策略是否放行的第一判据。
  • 对命中变量逐一核对 nodeid 与 BrowseName,避免把服务器自身诊断节点误判为过程变量。
  • 若读取返回状态码异常,保留原始 UaStatusCodeError.code,用于区分“认证拒绝”与“节点权限拒绝”。