问题大概出现在了 MethodClosure
类上, 该类定义以及方法如下图:
该类的描述为 Represents a method on an object using a closure which can be invoked at any time, 大概意思就是通过构建一个指定对象以及调用方法的Closure
的实例并且可以在任何时候进行调用。上图红色线标记的方法即为触发构建好的对象以及指定方法的函数,我们跟进看看该方法最终是怎么样执行的。
通过该方法的注释可以知道该方法的作用为调用指定对象的指定方法,所以 MethodClosure
类中构造方法中的两个参数的意思为 owner 代表调用方法的对象,method 为调用方法的名字,所以我们可以构造特定了对象从而实现执行特定函数,我们自己定义的对象以及方法最终会调用上图中红色框标记的函数进行执行。
举个例子,例如我们想通过 MethodClosure
实现执行命令的功能,那么代码如下:
MethodClosure mc = new MethodClosure(new java.lang.ProcessBuilder("open","/Applications/Calculator.app"), "start");
mc.call();
> 注:这里调用的call方法最终会调用doCall函数,有兴趣的可以自己去调试。
这样上述代码就可以实现代码执行,关于该函数的功能我们基本上搞明白了,那么我们回过头来想想,难道这个 CVE 就是说了下这个函数可以执行特定代码么?
既然我们知道了如何构建以及触发相关函数从而导致代码的执行,那么我们不妨去找找看看那些函数调用了存在缺陷的函数,通过 eclipse 我们可以很容易看出那些地方调用了 MethodClosure#call()
函数:
如上图所示,我们可以看到 groovy.util.Expando
类的 hashcode
以及 toString
等方法调用了 MethodClosure#call()
函数,到这里从事 Java 的小伙伴们应该比较激动,这里的 hashCode()
方法调用了存在缺陷的函数,hashCode
函数才是这个 CVE 比较核心的地方,首先我们需要知道 hashCode
函数的作用,当两个对象比较是否相等的时候,会调用该对象的 hashCode
以及 equals
方法进行比较,如果这两个方法返回的结果一致,那么认为这两个对象是相等,如果被调用对象没有重写 hashCode
以及 equals
方法,那么会调用父类的默认实现。
这里明白 hashCode
的作用之后,再来说说 HashMap
的 put
方法,该方法的定义如下:
因为 Map 是一种 key-value 类型的数据结构,所以Map集合不允许有重复 key,所以每次在往集合中添加键值对时会去判断 key 是否相等,那么在判断是否相等时会调用 key 的 hashCode
方法,如果我们精心构造一个 groovy.util.Expando
对象作为 Map 集合的key,那么在将对象添加进集合时就会触发 groovy.util.Expando
的 hashCode
方法,从而触发我们的恶意代码。
明白上面的知识后我们再来跟进 groovy.util.Expando#hashCode
方法,看看如何精心构造一个一刻执行恶意代码的对象,如下图:
这里从上图中可以看出调用 getProperties().get("hashCode")
方法从而实现自定义的 hashCode,我们只需要调用 setProperties("hashCode",Expando实例)
去绑定 hashCode 属性对于的实现就行了,这里 hashCode 必须是 Closure
或者其子类才能最终调用 call
函数,MethodClosure
类恰好是 Closure
的子类,所以结合这两个地方,恶意代码就会成功触发。
上面说到过通过调用 Map#put
方法即可触发我们构造好的代码,那么有人可能会问了,那些场景下才会触发 Map 的 put 方法,在反序列化时这样的场景还是存在的,Java 的反序列化类中很可能也是有这样的场景的。
下面给出利用代码: