前言
远古版本JDK反序列化漏洞的关键类AnnotationInvocationHandler,在后续版本中增加了限制,还没有专门研究过。
环境搭建
JDK版本为8u311,此外还需要一些依赖用于分析:
1 2 3 4 5 6 7 8 9 10
| <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函数代理时可以根据内置接口/抽象类调用函数,该版本下对内置接口的限制主要分为三个部分,第一部分如下:
1 2 3 4
| if (var6.getModifiers() == 1025 && !var6.isDefault() && var6.getParameterCount() == 0 && var6.getExceptionTypes().length == 0) { ... }
|
isDefault是个比较复杂的定义:
1 2 3 4 5 6
| public boolean isDefault() { return ((getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC) && getDeclaringClass().isInterface(); }
|
总结起来,要求该内置接口/抽象类里的所有函数都是:
Templates接口就是因为抛出异常被禁用了。
然后是返回类型的限制:
1 2 3
| 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可以写出简单的搜索代码来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @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函数:
1 2 3 4 5 6 7 8 9
| 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。
现在看来这个点只能当一个限制很大的反射点用。
参考