前言
远古版本JDK反序列化漏洞的关键类AnnotationInvocationHandler,在后续版本中增加了限制,还没有专门研究过。
环境搭建
JDK版本为8u311,此外还需要一些依赖用于分析:
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.4</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
限制
AnnotationInvocationHandler类在触发equals函数代理时可以根据内置接口/抽象类调用函数,该版本下对内置接口的限制主要分为三个部分,第一部分如下:
if (var6.getModifiers() == 1025 && !var6.isDefault() && var6.getParameterCount() == 0 && var6.getExceptionTypes().length == 0) {
...
}
isDefault是个比较复杂的定义:
public boolean isDefault() {
// Default methods are public non-abstract instance methods
// declared in an interface.
return ((getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) ==
Modifier.PUBLIC) && getDeclaringClass().isInterface();
}
总结起来,要求该内置接口/抽象类里的所有函数都是:
修饰符为public和abstract
如果是接口,修饰符不能没有abstract和static,而只有public
没有参数
不会抛出异常
Templates接口就是因为抛出异常被禁用了。
然后是返回类型的限制:
if ((!var7.isPrimitive() || var7 == Void.TYPE) && var7 != String.class && var7 != Class.class && !var7.isEnum() && !var7.isAnnotation()) {
...
}
要求函数返回类型为int等primitive或者String、Class、Enum和Annotation,基本可以认定只接受primitive类型和String。
通过ASM可以写出简单的搜索代码来:
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
methodCount += 1;
if (access != 1025) {
throw new IllegalStateException("1024");
}
if (isDefault(access)) {
throw new IllegalStateException("default");
}
if (exceptions != null) {
throw new IllegalStateException("exceptions");
}
Type[] argTypes = Type.getArgumentTypes(descriptor);
if (argTypes.length != 0) {
throw new IllegalStateException("args");
}
Type returnType = Type.getReturnType(descriptor);
int retSize = returnType.getSize();
if (retSize <= 0) {
throw new IllegalStateException("void");
}
String className = returnType.getClassName();
int dot = className.indexOf(".");
if (dot > -1 && !className.equals("java.lang.String")) {
throw new IllegalStateException("return type");
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
发现了比较有意思的类PropertyKey,其子类LiteralNode可序列化,且实现了getPropertyName函数,可以通过动态代理触发其getPropertyName函数:
Map<String, Object> map = new HashMap<>();
map.put("getPropertyName", 1);
InvocationHandler annotationInvocationHandler = (InvocationHandler)Utils.createWithoutConstructor("sun.reflect.annotation.AnnotationInvocationHandler");
Utils.setField(annotationInvocationHandler, "type", PropertyKey.class);
Utils.setField(annotationInvocationHandler, "memberValues", map);
Object proxy = Proxy.newProxyInstance(LiteralNode.ArrayLiteralNode.class.getClassLoader(), LiteralNode.ArrayLiteralNode.class.getInterfaces(), annotationInvocationHandler);
byte[] bytes = serialize(proxy);
Object o = unserialize(bytes);
o.equals(LiteralNode.newInstance(0, 0));
跟下去这条路基本没有东西能用,在要求可序列化的前提下只能触发一个toString。
现在看来这个点只能当一个限制很大的反射点用。