orion-visor安全审计报告

14次阅读
没有评论

orion-visor 安全审计报告

审计目标:orion-visor —— Dromara 开源「自动化运维 & 轻量堡垒机平台」
审计范围:后端 Java(Spring Boot 2.7 / MyBatis-Plus / InfluxDB 2.7 / fastjson2 / orion-kit)+ 前端 Vue3,本地 git 仓库(HEAD e5a7ed8)
审计日期:2026-08-12
审计维度:反序列化、命令执行/RCE、SQL 注入、未授权访问、硬编码凭据、XSS、SSRF、越权(含能获取系统/用户/应用数据与权限的全部路径)
方法:源码逐文件审计 + 交叉验证利用链;所有结论均标注文件与行号


0. 总体结论

风险等级 数量 说明
🔴 高危 5 可导致任意主机监控数据泄露、主机凭据解密、默认口令登录、匿名数据投毒、跨用户日志泄露
🟠 中危 5 会话 token 明文落日志、全套默认口令、RSA 私钥入库、CORS/点击劫持、凭据明文传输可重放
🟡 低危 2 延迟鉴权、无类型 JSON 解析(需 DB 写权限)
✅ 已排除 6 SQL 注入、应用层 RCE、反序列化、XSS、SSRF、主机级越权均未发现可利用点

核心风险点:本平台默认部署方式(cp .env.example .env && docker compose up)会使用整套公开已知的密钥/口令(AES 加密密钥、API 暴露 token、MySQL/Redis/InfluxDB 口令),配合默认 admin/admin 与弱 MD5 存储,使得默认安装的安全边界形同虚设;同时监控图表接口存在可绕过主机过滤的 Flux 注入,执行日志接口存在 IDOR 越权下载。


1. 高危漏洞(High)

H1. InfluxDB Flux 查询注入 —— 监控图表接口(跨主机数据泄露 / 任意 Flux 执行)

  • 类型:注入(InfluxDB Flux Injection)→ 未授权数据读取
  • 等级:🔴 高危

位置

  • orion-visor-framework/orion-visor-spring-boot-starter-influxdb/src/main/java/org/dromara/visor/framework/influxdb/core/query/FluxQueryBuilder.java:85-86, 96-97, 108-109, 126-128, 226-235
  • orion-visor-modules/orion-visor-module-monitor/orion-visor-module-monitor-service/src/main/java/org/dromara/visor/module/monitor/service/impl/MonitorHostServiceImpl.java:391-436(getChartSeries)
  • MonitorHostController.java:99 POST /monitor/monitor-host/chart,方法级权限仅 monitor:monitor-host:query(普通监控查询权限)
  • 请求体 MonitorHostChartRequest.java:46-73:agentKeys、measurement、fields、window、range、aggregate 全部为直接透传的用户可控字段

原理

Flux 查询通过 String.format 拼接用户输入,无任何转义/白名单:

// FluxQueryBuilder.java
public FluxQueryBuilder range(String range) {
    query.append(String.format(" |> range(start: %s)", range));   // 裸 token 拼接,无引号包裹
}
public FluxQueryBuilder measurement(String measurement) {
    this.appendFilter(String.format("r[\"_measurement\"] == \"%s\"", measurement));
}
public FluxQueryBuilder fields(Collection<String> fields) {
    conditions.add(String.format("r[\"_field\"] == \"%s\"", field));
}
public FluxQueryBuilder aggregateWindow(String every, String fn) {
    query.append(String.format(" |> aggregateWindow(every: %s, fn: %s)", every, fn));
}

最终组装为:

from(bucket: "metrics")
  |> range(start: <request.range>)
  |> filter(fn: (r) => r["_measurement"] == "<measurement>")
  |> filter(fn: (r) => r["host"] == "<agentKey1>" or ...)   // 主机过滤
  |> filter(fn: (r) => r["_field"] == "<field1>" or ...)
  |> aggregateWindow(every: <window>, fn: <aggregate>, createEmpty: true)
  |> yield()

其中 range 直接作为代码注入点(无引号),measurement/fields 可闭合字符串并注入 // 注释。

影响 / 利用

  1. 跨主机越权读取监控数据:注入 // 注释掉主机过滤,可读取所有主机的监控指标(本应仅限当前用户授权主机)。示例:

    POST /orion-visor/api/monitor/monitor-host/chart
    X-Auth-Token: <任意有监控查询权限的登录token>
    {
      "agentKeys": ["x"],
      "measurement": "cpu",
      "fields": ["usage"],
      "window": "1m",
      "aggregate": "mean",
      "range": "-1h) |> yield(name: \"pwn\") //"
    }
    

    → 生成 from(bucket:"metrics") |> range(start: -1h) |> yield(name: "pwn") //) |> filter(...),后续过滤全部被注释,返回全量数据。

  2. 以配置的 InfluxDB Token 权限执行任意 Flux:默认部署 INFLUXDB_TOKEN=Data@123456 为 InfluxDB admin 令牌,可读取全部 bucket(含 _monitoring、_tasks、其它业务 bucket)→ 任意数据外带;Flux 的 import "http" 可进一步把数据 POST 到外部服务器(变相 SSRF 外带)。

修复建议

  • measurement/field 做枚举/白名单校验(项目已有 MeasurementEnum 可复用)。
  • range 改为只接受数值时间戳(start/end 已是 Long),移除字符串版 range(String) 或做严格校验。
  • window/aggregate 做枚举白名单(1m/5m/1h、mean/max/min/sum/...)。
  • 对拼入 Flux 的字符串做转义:" → \",\ → \\,换行 → 拒绝。
  • 为 InfluxDB 创建最小权限 token(仅读写 metrics bucket),禁止使用 admin token。

H2. 硬编码默认 AES 加密密钥 —— 全部主机凭据用公开静态密钥加密落库

  • 类型:硬编码密钥 / 静态加密密钥(Encryption with static key)
  • 等级:🔴 高危

位置

  • orion-visor-launch/src/main/resources/application-prod.yaml:93 → secret-key: ${SECRET_KEY:uQeacXV8b3isvKLK}
  • orion-visor-launch/src/main/resources/application.yaml:266 → secret-key: I66AndrKWrwXjtBL(dev 环境,工作模式 ECB / PKCS5_PADDING)
  • .env.example:16、docker-compose.yaml:48、docker-compose-bt.yml:43、docker-compose-testing.yaml:16 均带默认值
  • orion-visor-framework/.../encrypt/core/impl/AesEncryptorImpl.java:78-86:Keys.generatorKey(config.getSecretKey(), ...) 直接用该密钥派生 AES Key
  • orion-visor-modules/orion-visor-module-asset/.../handler/host/config/AbstractHostConfigStrategy.java:66-69:主机密码 AesEncryptUtils.encryptAsString(newPassword) AES 加密后落库

原理

主机资产(SSH 密码、密钥)在库中为 AES 加密存储,但加密密钥是随仓库公开的默认值;README 引导 cp .env.example .env 直接部署,绝大多数用户不会改。且工作模式为 ECB(同一明文→同一密文,无 IV)。

影响

拿到数据库(备份、SQL 注入、运维误操作、内网渗透)即可用公开密钥离线解密所有主机凭据,批量接管目标主机。这是堡垒机类产品的最高危数据。

修复建议

  • 首次启动强制生成随机 SECRET_KEY(无默认值,缺失即拒绝启动)。
  • 切换到 GCM/CTR 等带随机 IV 的模式。
  • 高级别场景用 KMS/密钥管理服务。
  • 历史密文需随密钥轮换重新加密。

H3. 默认管理员 admin/admin + 双重无盐 MD5 密码存储

  • 类型:硬编码账号密码 / 弱口令哈希
  • 等级:🔴 高危

位置

  • sql/init-4-data.sql:4:INSERT INTO system_user (…,'admin','c3284d0f94606de1fd2af172aba15bf3',…),SQL 注释 -- 账号: admin 密码: admin
  • 存储侧:SystemUserServiceImpl.java:122, 316 → Signatures.md5(request.getPassword())
  • 校验侧:AuthenticationServiceImpl.java:229(if (password.length() != Const.MD5_LEN),要求口令已是 32 位 MD5)、:255-256(user.getPassword().equals(Signatures.md5(password)))
  • 前端:orion-visor-ui/src/store/modules/user/index.ts:99 → password: md5(loginForm.password);README 演示账号同为 admin/admin

原理(已用计算验证)

校验链为 存储值 == MD5( MD5(明文) ):

MD5("admin")      = 21232f297a57a5a743894a0e4a801fc3
MD5(MD5("admin")) = c3284d0f94606de1fd2af172aba15bf3   ← 与 init SQL 完全一致

无盐、无密钥拉伸(未使用 BCrypt/Argon2/scrypt)。orion-kit 2.0.6 的 Signatures.md5(String) 已确认为纯 MessageDigest.digest(utf8) 后 hex,无盐。

影响

  1. 默认 admin/admin 直接登录。
  2. 数据库泄露 → 双重 MD5 可离线爆破(字典/彩虹表复杂度与单次 MD5 等价,管理员口令无盐极易命中)。
  3. 关键:后端把 MD5(明文) 当作口令本身接受(再做一次 MD5 比对)。攻击者只需拿到任意用户的单次 MD5(流量嗅探、其它系统同口令泄露、日志),无需知道明文即可直接登录。

修复建议

  • 改用 BCrypt/Argon2id 带盐哈希存储。
  • 登录改为「服务端挑战 + 一次性 nonce」签名,使网络中的凭据不可重放。
  • 强制首次登录改密(update_password_status 已有该字段,建议强制)。

H4. 硬编码 API_EXPOSE_TOKEN + 弱认证的匿名 Agent 上报接口

  • 类型:硬编码凭据 / 未授权访问(仅靠静态请求头鉴权)
  • 等级:🔴 高危(默认部署下)

位置

  • orion-visor-launch/src/main/resources/application-prod.yaml:86 → token: ${API_EXPOSE_TOKEN:pmqeHOyZaumHm0Wt}
  • orion-visor-launch/src/main/resources/application.yaml:185-188 → header X-Server-Token,token 96QEPoOChlMfAEPn(dev)
  • ExposeApiAspect / ExposeApiConfig:校验逻辑为 config.getToken().equals(request.getHeader(config.getHeader()))
  • MonitorAgentEndpointController.java:POST /monitor/agent-endpoint/metrics、POST /monitor/agent-endpoint/sync-host-meta 均标注 @ExposeApi(等效 @PermitAll,登录态都不是必须)

原理

@ExposeApi 接口对未登录请求开放,唯一防线是与仓库同分的静态 token。默认部署不改 API_EXPOSE_TOKEN 时,任何网络可达者可携带公开的 X-Server-Token: pmqeHOyZaumHm0Wt 调用。

影响

  1. 监控数据投毒:伪造任意 agentKey 上报指标写入 InfluxDB → 篡改图表、触发/压制告警,干扰运维决策。
  2. sync-host-meta 在掌握 agentKey 时可更新主机元数据。
  3. token 固定无轮换,泄露一次永久有效。

修复建议

  • 每部署随机生成 API_EXPOSE_TOKEN,无默认值。
  • Agent 与 Server 间使用 mTLS 或带时间戳的 HMAC 签名,防止重放。
  • metrics 上报后由服务端校验 agentKey 对应主机是否启用。

H5. 批量执行日志 下载/尾随 越权(IDOR)

  • 类型:越权访问 / 不安全的直接对象引用(IDOR)
  • 等级:🔴 高危

位置

  • orion-visor-modules/orion-visor-module-exec/.../controller/ExecCommandLogController.java:174-178(GET /exec/exec-command-log/tail?id=..)、:184-187(GET /exec/exec-command-log/download?id=..)
  • 权限:两者均为 @ss.hasAnyPermission('exec:exec-command-log:query', 'exec:exec-command:exec')
  • ExecLogServiceImpl.java:391-411 downloadLogFile:仅 execHostLogDAO.selectById(id) 后直接读文件返回,无主机授权 / 无归属校验;getExecLogTailToken 同理

原理

对比批量执行(ExecCommandServiceImpl)、计划任务(ExecJobServiceImpl.checkHostPermission)、批量上传(UploadTaskServiceImpl.checkHostPermission)均会通过 getUserAuthorizedEnabledHostId 过滤主机,而执行日志下载/尾随漏掉了同样的数据级校验:id 是自增主键,可遍历。

影响

持有 exec:exec-command:exec(批量执行权限,一般运维即拥有)或 exec:exec-command-log:query 的用户,可遍历 id 下载/实时尾随其它用户(含管理员)在未被授权主机上执行的命令日志(含命令全文与输出)→ 横向+纵向越权的敏感信息泄露。

修复建议

  • downloadLogFile / getExecLogTailToken 中校验 hostLog.getHostId() ∈ assetAuthorizedDataService.getUserAuthorizedHostId(当前用户id)。
  • 记录日志归属人并校验 execLog.getUserId() == 当前用户 或管理员。

2. 中危漏洞(Medium)

M1. Terminal accessToken / transferToken 明文日志

  • 类型:敏感信息泄露(日志)
  • 位置:TerminalAccessInterceptor.java(log.info("...accessToken: {}", accessToken))、TerminalTransferInterceptor.java:64(log.info("...transferToken: {}", transferToken))
  • 原理:WebSocket 单次使用鉴权 token(UUIds.random19() 生成,存 Redis)在握手阶段明文打日志。
  • 影响:日志泄露时可抢在会话失效前劫持在线终端/文件传输会话,在目标主机上执行任意命令(等同被劫持用户的权限)。
  • 修复:不记录 token,或仅打码尾号;日志脱敏统一处理。

M2. 部署配置硬编码全套默认口令

  • 类型:硬编码凭据
  • 位置:.env.example、docker-compose.yaml、docker-compose-bt.yml、docker-compose-testing.yaml
    • MYSQL_PASSWORD=Data@123456、MYSQL_ROOT_PASSWORD=Data@123456
    • REDIS_PASSWORD=Data@123456
    • INFLUXDB_TOKEN=Data@123456、INFLUXDB_ADMIN_PASSWORD=Data@123456
    • GUACD_SSH_PASSWORD=guacd
  • 影响:默认部署下 MySQL/Redis/InfluxDB/guacd 全部使用公开口令;服务端口暴露即可直连数据库/Redis(Redis 未鉴权即读写 → 可覆盖 token、注入缓存)。README 明确引导 cp .env.example .env,故默认部署=全部默认口令。
  • 修复:提供 docker compose config 生成随机口令的脚本,缺失环境变量即启动失败。

M3. RSA 私钥明文入库(init SQL 分发)

  • 类型:硬编码密钥
  • 位置:sql/init-4-data.sql:27 → system_setting 表写入 encrypt.private-key(完整 PKCS#8 DER base64 RSA 私钥);配套 :26 为公钥
  • 原理:该私钥由 RsaDecryptorImpl.java:49,55 加载,用于解密前端 RSA 加密上送的参数(登录口令、主机密码等)。私钥随初始化 SQL 公开,所有部署共用同一把。
  • 影响:获得数据库备份/注入即可得到私钥,解密所有经 RSA 保护的参数。
  • 修复:首次启动动态生成密钥对;私钥用 SECRET_KEY 加密后落库,公钥独立下发。

M4. CORS 任意来源 + 允许凭证 + 禁用 X-Frame-Options

  • 类型:配置缺陷(CORS / 点击劫持)
  • 位置:OrionWebAutoConfiguration.java:161-169(setAllowCredentials(true) + addAllowedOriginPattern("*"),注册到 /**);Security 配置 frameOptions().disable()
  • 影响:任意源可跨域携带凭证发起请求;当前认证走请求头 token(非 Cookie),CSRF 风险被弱化,但该配置对未来任何 Cookie 会话/SSO 都是隐患;frameOptions 关闭 → 页面可被嵌入 iframe 实施点击劫持。
  • 修复:Origin 白名单化;开启 X-Frame-Options/CSP frame-ancestors。

M5. 登录凭据(MD5(明文))HTTP 明文传输且可重放

  • 类型:传输层缺陷 / 凭据可重放
  • 位置:docker-compose.yaml:12(${SERVICE_PORT:-1081}:80,默认无 TLS,nginx 无 ssl 配置);前端登录发送 md5(明文)(store/modules/user/index.ts:99)
  • 原理:后端把 MD5(明文) 当口令接受(见 H3),意味着网络中的该值本身就是可登录凭据;默认 HTTP 下可被中间人/旁路直接捕获。
  • 影响:抓包一次 → 永久接管该账号(无需还原明文)。
  • 修复:全站 HTTPS;服务端下发随机 nonce,前端签名后上送,防重放。

3. 低危漏洞 / 观察项

  • L1. Terminal accessToken 创建时不立即校验主机授权:TerminalServiceImpl.getTerminalAccessToken 生成 token 时未校验 hostId 是否在授权集合,延迟到 connect 时由 HostConnectServiceImpl.validHostAuthorized(HostConnectServiceImpl.java:398)兜底。当前不可利用,建议在发 token 前即校验,缩小 token 泄露面。
  • L2. MyBatis JSON 列无类型反序列化:JSONObjectTypeHandler.java:74 / JSONArrayTypeHandler.java:74 对 DB JSON 列执行 JSON.parseObject(value)(无类型)。fastjson2 默认未启用 autoType,需先获得 DB 写权限才能注入恶意 JSON,当前风险可控,仍建议对该类 JSON 列内容做类型校验或改用内置 Map 解析。

4. 已排查未见问题(确认安全 / 缓解到位)

类别 结论 依据
SQL 注入 ✅ 未见可利用点 全库唯一 ${} 位于 DataGroupMapper.xml:42(sort ${condition}),但 condition 仅来自 DataGroupServiceImpl.java:139,164 硬编码的 ">="/">",不可控;SqlUtils.limit 仅接受 Number;其余 Mapper 均 #{} 预编译
命令执行 / 应用层 RCE ✅ 目标主机均授权过滤 ExecCommandServiceImpl.execCommand、ExecJobServiceImpl.checkHostPermission、UploadTaskServiceImpl.checkHostPermission 均经 getUserAuthorizedEnabledHostId 白名单过滤;SSH 会话目标是连接配置而非可注入命令
反序列化 ✅ 低风险 fastjson2 仅类型化 parseObject(s, Class)(RedisStrings/RedisMaps/RedisLists 等),未发现 autoType/safeMode 关闭;无 ObjectInputStream/XMLDecoder/fastjson1;fastjson2 版本为较新依赖
XSS ✅ 已缓解 前端 utils/index.ts:219 cleanXss(转义 &<>'")与 replaceHtmlTag(:232)统一处理富文本;终端/日志按纯文本渲染
SSRF ✅ 未发现 全库无 RestTemplate/HttpClient/URL.openConnection 处理用户 URL;唯一出站为 SSH/RDP/VNC 会话与 InfluxDB 官方客户端,目标均经校验
主机级越权 ✅ 已缓解 终端连接 HostConnectServiceImpl.validHostAuthorized、exec/job/upload 均有数据级主机过滤(详见 H5 例外)
全局未授权访问 ✅ 兜底齐全 anyRequest().authenticated() + @PreAuthorize 方法级;@PermitAll 仅限登录/刷新 token/agent 上报(弱认证见 H4)/SFTP 下载(单次 transferToken 保护)

5. 利用链速览

默认部署 (cp .env.example .env)
    │
    ├─ H2  AES SECRET_KEY 公开  →  拿 DB 即解密全部主机 SSH 口令 → 接管全部资产
    ├─ H3  admin/admin 默认口令  →  直接登录
    ├─ H4  API_EXPOSE_TOKEN 公开 →  未授权上报 → 监控数据投毒 / 主机元数据篡改
    └─ M2  MySQL/Redis/InfluxDB 默认口令 → 直连数据库 → 同上解密链

已登录低权限用户
    ├─ H5  遍历 /exec/exec-command-log/download|tail?id= → 下载他人命令日志(含输出)
    ├─ H1  POST /monitor/monitor-host/chart + Flux 注入 → 读取所有主机监控数据
    └─ M5  抓包获取 md5(明文) → 直接登录任意账号(凭据可重放)

6. 修复优先级建议

  1. 立即:H2(随机化 SECRET_KEY + 切换 GCM)、H3(BCrypt/Argon2 + 强制改密 + 服务端 nonce 防重放)、H4(随机化 API_EXPOSE_TOKEN / mTLS)。
  2. 尽快:H1(Flux 白名单 + 最小 InfluxDB 令牌)、H5(日志下载/尾随补主机授权校验)、M2(部署随机口令)。
  3. 计划:M1(token 脱敏日志)、M3(动态 RSA 密钥)、M4(CORS 白名单 + frameOptions)、M5(HTTPS)。
  4. 建议新增:操作审计中记录 token 使用、日志系统集中脱敏;定期密钥轮换机制。

附:关键文件索引

文件 相关漏洞
FluxQueryBuilder.java:85-235 H1
MonitorHostServiceImpl.java:391-436 H1
application-prod.yaml:86,93 / application.yaml:185-188,266 H2 / H4 / M3
.env.example / docker-compose*.yaml H2 / H4 / M2
sql/init-4-data.sql:4,26,27 H3 / M3
SystemUserServiceImpl.java:122,316 / AuthenticationServiceImpl.java:229,255 H3
orion-visor-ui/src/store/modules/user/index.ts:99 H3 / M5
ExposeApiAspect / ExposeApiConfig / MonitorAgentEndpointController.java H4
ExecCommandLogController.java:174-187 / ExecLogServiceImpl.java:391-411 H5
TerminalAccessInterceptor / TerminalTransferInterceptor.java:64 M1
AesEncryptorImpl.java:78-86 / AbstractHostConfigStrategy.java:66-69 H2
OrionWebAutoConfiguration.java:161-169 M4
JSONObjectTypeHandler.java:74 / JSONArrayTypeHandler.java:74 L2

说明:UUIds.random19() 的随机源强度因 orion-kit jar 未随仓库分发未能核实,若其基于 SecureRandom 则安全,建议确认 orion-kit 2.0.6 实现。

正文完
 0
评论(没有评论)