前言

远古版本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。

现在看来这个点只能当一个限制很大的反射点用。


参考


Web Java 反序列化

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

Hessian JDK反序列化漏洞
Fastjson 1.2.80漏洞