orion-visor 安全审计报告
审计目标:orion-visor —— Dromara 开源「自动化运维 & 轻量堡垒机平台」
审计范围:后端 Java(Spring Boot 2.7 / MyBatis-Plus / InfluxDB 2.7 / fastjson2 / orion-kit)+ 前端 Vue3,本地 git 仓库(HEADe5a7ed8)
审计日期: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-235orion-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:99POST /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 可闭合字符串并注入 // 注释。
影响 / 利用
-
跨主机越权读取监控数据:注入
//注释掉主机过滤,可读取所有主机的监控指标(本应仅限当前用户授权主机)。示例: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(...),后续过滤全部被注释,返回全量数据。 -
以配置的 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(仅读写
metricsbucket),禁止使用 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 Keyorion-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,无盐。
影响
- 默认
admin/admin直接登录。 - 数据库泄露 → 双重 MD5 可离线爆破(字典/彩虹表复杂度与单次 MD5 等价,管理员口令无盐极易命中)。
- 关键:后端把
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→ headerX-Server-Token,token96QEPoOChlMfAEPn(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 调用。
影响
- 监控数据投毒:伪造任意
agentKey上报指标写入 InfluxDB → 篡改图表、触发/压制告警,干扰运维决策。 sync-host-meta在掌握agentKey时可更新主机元数据。- 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-411downloadLogFile:仅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.yamlMYSQL_PASSWORD=Data@123456、MYSQL_ROOT_PASSWORD=Data@123456REDIS_PASSWORD=Data@123456INFLUXDB_TOKEN=Data@123456、INFLUXDB_ADMIN_PASSWORD=Data@123456GUACD_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. 修复优先级建议
- 立即:H2(随机化 SECRET_KEY + 切换 GCM)、H3(BCrypt/Argon2 + 强制改密 + 服务端 nonce 防重放)、H4(随机化 API_EXPOSE_TOKEN / mTLS)。
- 尽快:H1(Flux 白名单 + 最小 InfluxDB 令牌)、H5(日志下载/尾随补主机授权校验)、M2(部署随机口令)。
- 计划:M1(token 脱敏日志)、M3(动态 RSA 密钥)、M4(CORS 白名单 + frameOptions)、M5(HTTPS)。
- 建议新增:操作审计中记录 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 实现。