Skip to content

未授权远程代码执行:Groovy 脚本注入 #1168

@jackieya

Description

@jackieya

未授权远程代码执行:Groovy 脚本注入

漏洞概述

项目 内容
漏洞类型 Pre-Auth Remote Code Execution (RCE)
危害等级 🔴 严重 (Critical)
影响版本 PowerJob v5.1.0 ~ v5.1.2 (全部 v5.1.x 版本,含 latest)
前置条件 无需任何认证,默认配置即可利用
攻击入口 /openApi/addWorkflowNode + /openApi/saveWorkflow + /openApi/runWorkflow

漏洞根因

PowerJob 存在两个独立的安全缺陷,组合利用可实现未授权远程代码执行:

缺陷 1: OpenAPI 默认无鉴权

OpenApiInterceptor.javaenableOpenApiAuth 默认为 false

@Value("${oms.auth.openapi.enable:false}")
private boolean enableOpenApiAuth;

导致 /openApi/* 下所有端点(包括 addWorkflowNodesaveWorkflowrunWorkflow默认完全不需要任何认证即可访问。
相关issue:#1128

缺陷 2: Groovy 脚本引擎无沙箱(核心漏洞)

GroovyEvaluator.java 使用 ScriptEngine.eval() 直接执行传入的表达式,无任何沙箱限制:

public class GroovyEvaluator implements Evaluator {
    private static final ScriptEngine ENGINE = new ScriptEngineManager().getEngineByName("groovy");

    @Override
    public Object evaluate(String expression, Object input) {
        Bindings bindings = ENGINE.createBindings();
        bindings.put("context", input);
        return ENGINE.eval(expression, bindings);  // ← 直接执行,无沙箱
    }
}

DecisionNodeHandler.javanodeParams 来自用户输入,被直接传给 GroovyEvaluator

public class DecisionNodeHandler implements ControlNodeHandler {
    private final GroovyEvaluator groovyEvaluator = new GroovyEvaluator();

    @Override
    public void handle(PEWorkflowDAG.Node node, ...) {
        String script = node.getNodeParams();  // ← 用户完全可控,无过滤
        result = groovyEvaluator.evaluate(script, wfContext);  // ← 直接执行
    }
}

nodeParams 被直接拼接到 Groovy 脚本引擎中:

用户输入 (nodeParams)
  → POST /openApi/addWorkflowNode
  → SaveWorkflowNodeRequest.nodeParams
  → 存入数据库 WorkflowNodeInfoDO.nodeParams
  → POST /openApi/runWorkflow 触发工作流
  → DecisionNodeHandler.handle()
  → node.getNodeParams()
  → groovyEvaluator.evaluate(script, wfContext)
  → ScriptEngine.eval(expression)  // 在 Server JVM 中执行
  → Runtime.exec()  // 任意命令执行

全链路无任何过滤、校验或沙箱隔离

攻击链

攻击者 (无需认证)
    │
    ├── Step 1: POST /openApi/fetchAllJob → 探测有效 appId(默认 appId=1)
    ├── Step 2: POST /openApi/saveJob → 创建虚拟 Job(用作 DAG 下游分支)
    ├── Step 3: POST /openApi/addWorkflowNode → 创建 3 个节点:
    │       ├─ DECISION 节点 (type=2): nodeParams = 恶意 Groovy 脚本
    │       ├─ JOB 节点 (true 分支)
    │       └─ JOB 节点 (false 分支)
    ├── Step 4: POST /openApi/saveWorkflow → 创建 DAG
    │       DECISION ──true──→ JOB1
    │                ──false─→ JOB2
    └── Step 5: POST /openApi/runWorkflow → 触发工作流
         │
         ▼ Server 处理 DECISION 节点
    DecisionNodeHandler.handle()
         │   groovyEvaluator.evaluate("Runtime.getRuntime().exec(...)")
         ▼ ScriptEngine.eval()
    RCE ✅ (Server JVM, root 权限)

本地复现

复现截图

Image

修复建议

  1. oms.auth.openapi.enable 默认值由 false 改为 true(涉及 application.propertiesOpenApiInterceptor.java 中的 @Value 注解默认值)。
  2. GroovyEvaluator 增加沙箱限制(使用 SecureASTCustomizer 禁止调用 RuntimeProcessBuilderSystem 等危险类)
  3. nodeParams 进行危险关键字校验(拒绝包含 RuntimeexecProcessBuilder 等的脚本)

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions