乐于分享
好东西不私藏

OpenClaw WebSocket共享令牌权限提升漏洞复现&研究

OpenClaw WebSocket共享令牌权限提升漏洞复现&研究

点击蓝字,关注我们

0x0 背景介绍

OреnClаԝ是一款开源的AI智能体平台,能够在本地环境中自主运行,通过自然语言指令直接操作用户计算机完成各类任务,包括文件读写、Shеll命令执行、网页浏览以及与邮件、Slасk、Jirа、GitHub等第三方服务的集成。

该漏洞存在于OреnClаԝ网关的WеbSосkеt连接处理逻辑中。在2026.3.12版本之前,当使用无设备共享令牌或密码认证的后端连接时,系统未能正确验证和限制客户端自行声明的权限范围,攻击者可利用该漏洞,通过获取或构造无设备共享令牌,在WеbSосkеt连接建立时自行声明高权限作用域,从而完全控制目标机器。

漏洞详情
漏洞类型
影响版本
利用复杂度
CVE编号

权限提升

<= 2026.3.11

暂无

攻击效果:

  • 伪造管理员令牌,造成数据泄露,控制服务器。

0x1 环境搭建(Ubuntu24)

1.1-Ubuntu24+Docker搭建配置

老样子,一个简陋的搭建,只是环境搭建,龙虾设置都跳过了(因为我的环境没有GUI ,认证都做不了)

#  **OpenClaw v2026.3.11 **## **1️⃣ 环境准备与源码获取**# 1、创建目录并进入mkdir -p ~/openclaw && cd ~/openclaw# 2、克隆源码 (使用 git 比 zip 更方便切换版本)git clone https://github.com/openclaw/openclaw.git .# 3、切换到v2026.3.11git checkout v2026.3.11# 设置环境变量 (可选,用于自定义 apt 包)export OPENCLAW_HOME_VOLUME="openclaw_home"export OPENCLAW_DOCKER_APT_PACKAGES="ffmpeg build-essential curl jq"## **2️⃣ 系统优化 (推荐:增加 Swap)**如果服务器内存较小(<4GB),建议增加 Swap 防止构建或运行时 OOM。# 1、关闭并删除旧 Swapsudo swapoff -asudo rm -f /swapfile# 2、创建 8GB Swapsudo fallocate -l 8G /swapfilesudo chmod 600 /swapfilesudo mkswap /swapfilesudo swapon /swapfile# 验证free -h## **3️⃣ 运行初始化脚本**./docker-setup.sh

当然了,少不了这个配置,因为它报错!@@!!#%$!@:

Gateway failed to startError: non-loopback Control UI requires gateway.controlUi.allowedOrigins.主要就是OpenClaw 的安全机制检测到通过非本地 IP(即远程服务器 IP)访问,但配置文件中没有明确允许该来源,为了防止 CSRF 攻击,它拒绝启动

所以示例配置文件,当然实际环境谨慎考虑

root@iubuntu:~# cat ~/.openclaw/openclaw.json{  "wizard": {    "lastRunAt""2026-03-17T04:41:47.969Z",    "lastRunVersion""2026.3.11",    "lastRunCommand""onboard",    "lastRunMode""local"  },  "agents": {    "defaults": {      "workspace""/home/node/.openclaw/workspace"    }  },  "tools": {    "profile""coding"  },  "commands": {    "native""auto",    "nativeSkills""auto",    "restart"true,    "ownerDisplay""raw"  },  "session": {    "dmScope""per-channel-peer"  },  "gateway": {    "port"18789,    "mode""local",    "bind""lan",    "auth": {      "mode""token",      "token""b215ee944b8efb33664e3ab7a2ad2b19b304f3da6ea67791237b89366a042674"    },    "controlUi": {// 增加的这里      "dangerouslyAllowHostHeaderOriginFallback"true    },    "tailscale": {      "mode""off",      "resetOnExit"false    },    "nodes": {      "denyCommands": [        "camera.snap",        "camera.clip",        "screen.record",        "contacts.add",        "calendar.add",        "reminders.add",        "sms.send"      ]    }  },  "meta": {    "lastTouchedVersion""2026.3.11",    "lastTouchedAt""2026-03-17T04:41:47.988Z"  }}

0x2 漏洞复现

2.1-手动复现

  • python概念验证

https://github.com/Kai-One001/cve-/blob/main/OpenClaw_WebSocket_Token_Privilege_Escalation.py

2.2-是不是好奇token哪里来

我一开始也在纳闷,复现肯定是以本次漏洞为主,但是正常是什么样子,我注意到这个漏洞,信息泄露(原理会大概分析下默认密钥获取章节

https://cvepremium.circl.lu/vuln/GHSA-7H7G-X2PX-94HJ

2.3-复现流量特征 (PCAP)

  • 普通权限调用(注意是WеbSосkеt协议哦

  • 伪造后调用


0x3 漏洞原理分析

3.1 [入口定位] 从 WebSocket 升级到 connect 握手

在这次分析中呢一直在思考:作用域信息究竟是在何处被"最终采信"的?因为感觉好飘渺,也只能顺着 WebSocket 数据流一路往下追。

  • 浏览src/gateway/server-http.ts 可以看到,HTTP 层在处理请求时升级为 WebSocket 的流量:

// 612:620:src/gateway/server-http.tsasync function handleRequest(req:IncomingMessage, res:ServerResponse){ setDefaultSecurityHeaders(res,{ strictTransportSecurity: strictTransportSecurityHeader,});// Don't interfere with WebSocket upgrades; ws handles the 'upgrade' event.if(String(req.headers.upgrade ??"").toLowerCase()==="websocket"){return;}}
  • 也就是说,所有 WebSocket协议层的逻辑都不在这里,而是在ws 的升级回调中完成

继续沿着命名线索,在src/gateway/server-runtime-state.ts中找到了 WebSocket 服务端初始化逻辑,而真正处理单条连接握手与后续消息的核心,则集中在:

src/gateway/server/ws-connection/message-handler.ts 的 attachGatewayWsMessageHandler 函数
  • 在该函数中,socket.on("message")对首帧的处理是整个漏洞链条的开端:

// 363:401:src/gateway/server/ws-connection/message-handler.tssocket.on("message", async (data)=>{if(isClosed()){return;}const text = rawDataToString(data);...const client = getClient();if(!client){// Handshake must be a normal request:// { type:"req", method:"connect", params: ConnectParams }.const isRequestFrame = validateRequestFrame(parsed);if(!isRequestFrame || parsed.method !=="connect"||!validateConnectParams(parsed.params)){... close(1008, closeReason);return;}const frame = parsed;const connectParams = frame.paramsasConnectParams;...
  • 户端声明的 role 与 scopes 首次进入系统是 WebSocketconnect请求的参数中。

3.2 [关键节点] 角色与作用域的采信与清洗逻辑

顺着 connectParams 的使用继续往下,可以看到角色与作用域被直接写回连接状态:

// 480:496:src/gateway/server/ws-connection/message-handler.tsconst roleRaw = connectParams.role ??"operator";const role = parseGatewayRole(roleRaw);if(!role){...}// Default-deny: scopes must be explicit. Empty/missing scopes means no permissions.// Note: If the client does not present a device identity, we can't bind scopes to a paired// device/token, so we will clear scopes after auth to avoid self-declared permissions.let scopes =Array.isArray(connectParams.scopes)? connectParams.scopes :[];connectParams.role = role;connectParams.scopes = scopes;

注意到注释含义:  

"如果客户端没有提供设备身份,就无法把作用域和设备/令牌绑定,所以在认证后应该清空这些自声明的作用域,以避免权限伪造。"

也就是说,预期安全边界是:

  • 没有设备身份device的连接,哪怕提供了共享令牌/密码,也不应该直接采信客户端 scopes 中的高危作用域

  • 接下来,重点寻找"清空作用域"的实现代码

在同一函数中出现了一个专门的工具函数:

// 628:633:src/gateway/server/ws-connection/message-handler.tsconst clearUnboundScopes =()=>{if(scopes.length >0&&!controlUiAuthPolicy.allowBypass &&!sharedAuthOk){ scopes =[]; connectParams.scopes = scopes;}};

这一段逻辑蕴含着漏洞的核心条件:

  • 只有在!sharedAuthOk且未配置 Control UI旁路 的情况下才会清空作用域

  • 也就是,一旦sharedAuthOk === true(共享令牌/密码验证通过),即使没有设备身份,也不会清空客户端自声明的作用域

配合前面的设计注释,可以看到一个明显的反差:

  • 设计上希望"无设备身份 → 不信任客户端自带作用域";

  • 实现上却变成了"只要共享认证成功sharedAuthOk,就默认信任客户端作用域"

  • 这就会造成"服务器端没有绑定的情况下,仍保留客户端声明的作用域"的直接根源。

3.3 [认证行为] 共享密钥如何被判定为 sharedAuthOk

看一下 sharedAuthOk的计算方式。相关代码集中在 

// 75:88:src/gateway/server/ws-connection/auth-context.tsexport async function resolveConnectAuthState(params:{ resolvedAuth:ResolvedGatewayAuth; connectAuth:HandshakeConnectAuth|null|undefined; hasDeviceIdentity:boolean;...}):Promise<ConnectAuthState>{const sharedConnectAuth = resolveSharedConnectAuth(params.connectAuth);const sharedAuthProvided =Boolean(sharedConnectAuth);const{ token: deviceTokenCandidate, source: deviceTokenCandidateSource }=params.hasDeviceIdentity ? resolveDeviceTokenCandidate(params.connectAuth):{};  ...  let authResult: GatewayAuthResult = await authorizeWsControlUiGatewayConnect({    auth: params.resolvedAuth,    connectAuth: sharedConnectAuth,    ...    rateLimitScope: AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET,  });  ...  const sharedAuthResult =    sharedConnectAuth &&    (await authorizeHttpGatewayConnect({      auth: { ...params.resolvedAuth, allowTailscalefalse },      connectAuth: sharedConnectAuth,      ...      rateLimitScope: AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET,    }));  ...  const sharedAuthOk =    (sharedAuthResult?.ok === true &&      (sharedAuthResult.method === "token" || sharedAuthResult.method === "password")) ||    (authResult.ok && authResult.method === "trusted-proxy");
  • sharedAuthOk只要满足"共享令牌/密码校验通过(token/password)"或"可信代理(trusted-proxy)"认证成功,就会被置为true

  • 这个值随后被传回message-handler.ts,直接参与是否调用clearUnboundScopes()的决策

进一步查看共享令牌/密码本身的比对逻辑,在 src/gateway/auth.ts 的 authorizeGatewayConnect 中:

// 448:480:src/gateway/auth.tsif (auth.mode === "token") {  ...  if (!connectAuth?.token) {    return { ok: false, reason: "token_missing" };  }  if (!safeEqualSecret(connectAuth.token, auth.token)) {    limiter?.recordFailure(ip, rateLimitScope);    return { ok: false, reason: "token_mismatch" };  }  limiter?.reset(ip, rateLimitScope);  return { ok: true, method: "token" };}if (auth.mode === "password") {  ...  if (!password) {    return { ok: false, reason: "password_missing" };  }  if (!safeEqualSecret(password, auth.password)) {    limiter?.recordFailure(ip, rateLimitScope);    return { ok: false, reason: "password_mismatch" };  }  limiter?.reset(ip, rateLimitScope);  return { ok: true, method: "password" };}
  • 一旦客户端持有共享密钥,并通过上述校验,即被视为sharedAuthOk = true

  • 而在clearUnboundScopes的条件中,正是用!sharedAuthOk来决定是否清空自声明作用域。这就形成了一个不易察觉的逻辑组合:

  • 共享密钥设计的初衷是"允许后端/自动化客户端在无需设备的情况下接入网关";

  • 但在当前实现中,它居然"解除了对作用域清洗的保护措施"

默认密钥获取:

在查询时发现这个漏洞,意思是暴露了长期有效的共享网关凭证

https://cvepremium.circl.lu/vuln/GHSA-7H7G-X2PX-94H#### 环境是否需要配置- 需要:网关必须启用共享认证(`gateway.auth.mode=token` 或 `password`),并且实际配置了 `gateway.auth.token` / `gateway.auth.password`(或对应环境变量)。- 不需要:不需要任何设备、也不需要先完成一次配对审批;这个问题的核心是“配对用的 setup code 本身携带了长期有效的共享凭证”。#### Step 1:生成配对设置码(受害者侧)在主机上执行:openclaw qr --setup-code-only它会输出一段 setup code(同时 `openclaw qr` 默认还会渲染 ASCII 二维码)。#### Step 2:模拟泄露载荷(攻击者侧拿到 setup code)泄露来源可以是聊天记录、日志、截图、复制的二维码 payload 等;攻击者只需要拿到那串 setup code。#### Step 3:从 setup code 还原共享凭证(攻击者侧)### 原理简述(为什么这属于“泄露即永续可用”)- 设计预期边界:配对码/二维码更像“一次性引导”(短期、可撤销、最好不可逆),泄露风险应可控。- 实际实现:`openclaw qr` 生成的 setup code 不是一次性 token,而是把长期共享凭证(`gateway.auth.token/password`)直接打包进 payload,再用 Base64URL 做了一层可逆编码。- 因此:任何获得该 setup code/二维码内容的人,都能离线恢复共享凭证,并在之后任意时间重放使用(直到你轮换 token/password)。

setup code的本质是Base64URL(JSON(payload)),直接解码即可恢复JSON,其中包含:

  • url:网关 WS 地址

  • token或 password:共享网关凭证(长期有效)

//setup-code.tsLines 351-355export function encodePairingSetupCode(payload: PairingSetupPayload): string {  const json = JSON.stringify(payload);  const base64 = Buffer.from(json, "utf8").toString("base64");  return base64.replace(/\+/g"-").replace(/\//g"_").replace(/=+$/g"");}//以及 payload 明确把 token/password 塞进去://setup-code.tsLines 387-396return {  oktrue,  payload: {    url: urlResult.url,    token: auth.token,    password: auth.password,  },  authLabel: auth.label,  urlSource: urlResult.source ?? "unknown",};

 Step 4:重放使用共享凭证登录网关(攻击者侧)

攻击者用解码出来的 url + token/password 直接走网关认证(HTTP 或 WebSocket 都可,取决于你的调用面),即可在“原本预期的一次性配对流程”之外反复登录。

3.4 [缺失绑定] 无设备身份场景下的作用域残留

知道clearUnboundScopes 的触发条件后,还需要看它在"没有设备身份"时的调用位置:

// 634:687:src/gateway/server/ws-connection/message-handler.tsconst handleMissingDeviceIdentity = (): boolean => {  if(!device) {    clearUnboundScopes();  }  const trustedProxyAuthOk isTrustedProxyControlUiOperatorAuth({    isControlUi,    role,    authMode: resolvedAuth.mode,    authOk,    authMethod,  });  const decision evaluateMissingDeviceIdentity({    hasDeviceIdentityBoolean(device),    role,    isControlUi,    controlUiAuthPolicy,    trustedProxyAuthOk,    sharedAuthOk,    authOk,    hasSharedAuth,    isLocalClient,  });  if(decision.kind === "allow") {    return true;  }  ...};if(!handleMissingDeviceIdentity()) {  return;}
  • !device,先调用clearUnboundScopes()

  • 然后基于一系列条件(包括sharedAuthOk)调用 evaluateMissingDeviceIdentity决定是否允许无设备继续连接。

结合之前对 clearUnboundScopes 的分析,我们可以推导出完整条件:

当 无设备身份 (!device) 且 sharedAuthOk === true 时:•clearUnboundScopes 被调用,但其内部条件 !sharedAuthOk 不满足;•因此 scopes 不会被清空,保留客户端自声明的所有作用域。如果后续的 evaluateMissingDeviceIdentity 返回 kind"allow"(例如本地后端客户端、自身策略允许跳过设备绑定),握手继续进行,连接进入 connected 状态。

此时,从"预期设计"角度看:

•缺失绑定:作用域没有与任何具体设备、配对记录或可信 UI 路径绑定;•却被保留:代码仍然原样保留客户端在 connectParams.scopes 中声明的所有值。

这正是漏洞描述中"没有服务器端绑定的情况下,仍能保留客户端声明的作用域"的表现。

3.5 [最终落点] 网关方法授权如何完全信任连接状态中的作用域

有了"客户端作用域如何残留"的前半截,还需要确认这些作用域是否真的被用于控制面方法授权。继续沿着调用链往后,在 src/gateway/server-methods.ts 的 authorizeGatewayMethod 函数中,可以看到对角色与作用域的最终校验逻辑:

// 40:52:src/gateway/server-methods.tsif (!client?.connect) {  return null;}if (method === "health") {  return null;}const roleRaw = client.connect.role ?? "operator";const role = parseGatewayRole(roleRaw);if (!role) {  return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${roleRaw}`);}const scopes = client.connect.scopes ?? [];if (!isRoleAuthorizedForMethod(role, method)) {  return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`);}if (role === "node") {  return null;}if (scopes.includes(ADMIN_SCOPE)) {  return null;}const scopeAuth = authorizeOperatorScopesForMethod(methodscopes);if (!scopeAuth.allowed) {  return errorShape(ErrorCodes.INVALID_REQUEST, `missing scope: ${scopeAuth.missingScope}`);}return null;
  • scopes完全来源于client.connect.scopes—— 即握手阶段设置的值

  • 若 scopes中包含operator.admin(ADMIN_SCOPE),则直接允许所有管理员方法,不再做更多限制;
  • 对于其他受限方法,也仅通过authorizeOperatorScopesForMethod 在同一作用域列表中做集合判断。
换句话说,一旦我们在握手阶段成功让 

client.connect.scopes中包含operator.admin,并成功通过上文分析的认证与设备策略检查,整个后续控制面调用就会把这一作用域视为"既成事实"

结合前面的分析链路,可以总结出实际生效的危险路径:

1.客户端通过 WebSocket connect 握手声明:•role: "operator"•scopes: ["operator.admin"]•auth.token 或 auth.password 为合法共享密钥。2.authorizeGatewayConnect 认为共享密钥正确,sharedAuthOk = true3.因为 sharedAuthOk === true,clearUnboundScopes 不会清空任何作用域。4.若环境满足本地后端自接入的条件(shouldSkipBackendSelfPairing 返回 true),系统允许无设备身份继续连接。5.连接建立后,authorizeGatewayMethod 完全信任 client.connect.scopes,将该连接视为拥有 operator.admin 全权限的网关操作者。

其中,"注入点"与"爆发点"可以标注为:

  • 注入点:connect握手中的scopes: ["operator.admin"]
  • 爆发点:authorizeGatewayMethod中对client.connect.scopes的信任。

3.6 [最大危害] 从共享密钥到完全控制网关

在上述调用链下,如果攻击者能够满足以下条件:

•拥有某个用于后端/自动化的共享密钥(gateway.auth.token 或 gateway.auth.password);•能以本地或被视为本地(loopback + 特定代理配置)的形式连接 WebSocket 网关入口;•能构造合法协议格式的 connect 帧并宣称自身为 operator 角色。

那么,他可以在无需任何设备配对、无需 Control UI 显式信任的前提下,获得等同于管理员的作用域集合,包括但不限于:

operator.admin:增删改查网关配置(config.*)、触发系统事件(system-event)、重置/删除会话(sessions.*)、管理定时任务(cron.*)、触发在线更新(update.*)等;operator.pairing:操控设备/节点配对、颁发/吊销设备令牌;operator.write:主动驱动代理发送消息、执行工具调用等;operator.approvals:篡改或伪造执行审批链路。

从风险场景角度,可以猜想出几类高危结果(原理上):

•横向控制:在多设备、多节点部署中,通过管理员作用域批量修改节点配置、注入恶意模型或工具调用;•数据破坏与窃取:通过 sessions.*、logs.*、chat.history 等接口窃取历史会话与日志;通过 agents.files.* 操作上传/替换关键 Prompt 或工具脚本;•持久化后门:利用配置和定时任务接口(如 config.*、cron.*)植入长期存活的恶意任务,甚至借助外部工具集成实现 RCE 级别的攻击链。

不过重点是OpenClaw网关本身扮演的是"多渠道 AI 中枢"的角色,上述任一结果都意味着攻击者可以在充分了解系统协议的前提下,对整个 AI 消息链路及其背后资源进行深度操控,这在环境中就会有些危害。


0x4 修复建议

1、升级最新版本将组件升级最新版本
https://github.com/openclaw/openclaw
2、临时防护措施
  • 作用域绑定强制化:在clearUnboundScopes中,去除对sharedAuthOk 的依赖,改为只要"无设备身份、且不处于显式信任的Control UI / 可信代理上下文",就一律清空客户端自声明作用域
  • 更新Token生成新的强随机Token

  • 权限最小化检查所有生成的Token,确保它们只拥有完成任务所需的最小权限


      /**好消息,水培成功!坏消息,土壤没准备**/
      [水培成功图,xixi]

      免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。