記憶體馬webshell-MemoryShell
- 前言:你馬沒了
- 利用JavaAgent技術發現并清除系統中的記憶體馬
- 介紹
- 安全行業主要讨論的記憶體馬主要分為以下幾種方式
- 寫入測試
- Servlet API 提供的動态注冊機制
- Filter 記憶體馬
- 使用 ServletContext 添加 Filter 記憶體馬的方法。
- Servlet 記憶體馬
- Listener 記憶體馬
- 應用中可能調用的監聽器如下
前言:你馬沒了
利用JavaAgent技術發現并清除系統中的記憶體馬
Github:https://github.com/su18/MemoryShell
介紹
記憶體馬又名無檔案馬,見名知意,也就是無檔案落地的 webshell 技術,
是由于 webshell 特征識别、防篡改、目錄監控等等
針對 web 應用目錄或伺服器檔案防禦手段的介入,
導緻的檔案 shell 難以寫入和持久而衍生出的一種“概念型”木馬。
這種技術的核心思想非常簡單,一句話就能概括,
那就是對通路路徑映射及相關處理代碼的動态注冊。
這種動态注冊技術來源非常久遠,在安全行業裡也一直是不溫不火的狀态,
直到冰蠍的更新将 java agent 類型的記憶體馬重新帶入大衆視野
并且瞬間火爆起來。這種技術的爆紅除了概念新穎外,
也确實符合時代發展潮流,
現在針對 webshell 的清除和識别已經花樣百出,
大廠研發的使用
分類、機率
等等方式訓練的
機器學習算法模型
,
基于
神經網絡
的
流量
層面的
特征識别手段
,
基本上都花式吊打正常檔案型 webshell。
如果你不會寫,不會繞,還僅僅使用網上下載下傳的 jsp ,那肯定是不行的。
記憶體馬搭上了
冰蠍和反序列化漏洞的快車,快速占領了人們的視野,成為了主流的 webshell 寫入方式。
作為 RASP 技術的使用者,
自然也要來研究和學習一下記憶體馬的思想、原理、添加方式,
并探究較好、較通用的防禦和清除方式。
安全行業主要讨論的記憶體馬主要分為以下幾種方式
• 動态注冊 servlet/filter/listener(使用 servlet-api 的具體實作)
• 動态注冊 interceptor/controller(使用架構如 spring/struts2)
• 動态注冊使用職責鍊設計模式的中間件、架構的實作(例如 Tomcat 的 Pipeline & Valve,Grizzly 的 FilterChain & Filter 等等)
• 使用 java agent 技術寫入位元組碼
寫入測試
Servlet API 提供的動态注冊機制
2013 年,國際大站 p2j 就釋出了這種特性的一種使用方法:

Servlet、Listener、Filter
由 javax.servlet.ServletContext 去加載
無論是使用 xml 配置檔案
還是使用 Annotation 注解配置
均由 Web 容器進行初始化,
讀取其中的配置屬性,然後向容器中進行注冊。Servlet 3.0 API 允許使 ServletContext 用動态進行注冊
在 Web 容器初始化的時候(即建立ServletContext 對象的時候)進行動态注冊。
可以看到 ServletContext 提供了 add*/create* 方法來實作動态注冊的功能。
在不同的容器中,實作有所不同,這裡僅以 Tomcat 為例調試,其他中間件在代碼中有部分實作。
Filter 記憶體馬
Filter 我們稱之為過濾器,
是 Java 中最常見也最實用的技術之一,
通常被用來
處理靜态 web 資源
、
通路權限控制
、
記錄日志
等附加功能等等。
一次請求進入到伺服器後,将先由 Filter 對使用者請求進行
預處理
,再交給 Servlet。
通常情況下,Filter 配置在
配置檔案
和
注解
中,在其他代碼中如果想要完成注冊,主要有以下幾種方式:
1. 使用 ServletContext 的 addFilter/createFilter 方法注冊;
2. 使用 ServletContextListener 的 contextInitialized 方法在伺服器啟動時注冊
(将會在 Listener 中進行描述);
3.
使用 ServletContext 添加 Filter 記憶體馬的方法。
看一下 createFilter 方法
按照注釋,這個類用來在調用
addFilter
向 ServletContext
執行個體化
一個指定的 Filter 類。
這個類還約定了一個事情,那就是
如果這個 ServletContext 傳遞給 ServletContextListener 的 ServletContextListener.contextInitialized 方法,
該方法 即 未在 web.xml 或 web-fragment.xml 中聲明,
也未使用 javax.servlet.annotation.WebListener 進行注釋,
則會抛出 UnsupportedOperationException 異常,這個約定其實是非常重要的一點。
接下來看 addFilter 方法
ServletContext 中有三個重載方法,
分别接收字元串類型的 filterName
以及 Filter 對象/className 字元串/Filter 子類的 Class 對象,
提供不同場景下添加 filter 的功能,
這些方法均傳回 FilterRegistration.Dynamic 實際上就是 FilterRegistration 對象。
addFilter 方法實際上就是動态添加 filter 的最核心和關鍵的方法,
但是這個類中同樣約定了 UnsupportedOperationException 異常。
由于 Servlet API 隻是提供接口定義,
具體的實作還要看具體的容器,
那我們首先以 Tomcat 7.0.96 為例,看一下具體的實作細節。
相關實作方法在 org.apache.catalina.core.ApplicationContext#addFilter 中。
可以看到,這個方法建立了一個 FilterDef 對象,
将 filterName、filterClass、filter 對象初始化進去,
使用 StandardContext 的 addFilterDef 方法将建立的 FilterDef 儲存在了 StandardContext 中的一個 Hashmap filterDefs 中,
然後 new 了一個 ApplicationFilterRegistration 對象并且傳回,
并沒有将這個 Filter 放到 FilterChain 中,
單純調用這個方法不會完成自定義 Filter 的注冊。
并且這個方法判斷了一個狀态标記,如果程式以及處于運作狀态中,則不能添加 Filter。
直接操縱 FilterChain 呢?
FilterChain 在 Tomcat 中的實作是 org.apache.catalina.core.ApplicationFilterChain,
這個類提供了一個 addFilter 方法添加 Filter,
這個方法接受一個 ApplicationFilterConfig 對象,将其放在 this.filters 中。
答案是可以,但是沒用,因為對于每次請求需要執行的 FilterChain 都是動态取得的。
那Tomcat 是如何處理一次請求對應的 FilterChain 的呢?
在 ApplicationFilterFactory 的 createFilterChain 方法中,可以看到流程如下:
• 在 context 中擷取 filterMaps,并周遊比對 url 位址和請求是否比對;
• 如果比對則在 context 中根據 filterMaps 中的 filterName 查找對應的 filterConfig;
• 如果擷取到 filterConfig,則将其加入到 filterChain 中
• 後續将會循環 filterChain 中的全部 filterConfig,通過 getFilter 方法擷取 Filter 并執行 Filter 的 doFilter 方法。
通過上述流程可以知道,每次請求的 FilterChain 是動态比對擷取和生成的,
如果想添加一個 Filter ,需要在 StandardContext 中 filterMaps 中添加 FilterMap,
在 filterConfigs 中添加 ApplicationFilterConfig。這樣程式建立時就可以找到添加的 Filter 了。
在之前的 ApplicationContext 的 addFilter 中将 filter 初始化存在了 StandardContext 的 filterDefs 中,那後面又是如何添加在其他參數中的呢?
在 StandardContext 的 filterStart 方法中生成了 filterConfigs
在 ApplicationFilterRegistration 的 addMappingForUrlPatterns 中生成了 filterMaps。
而這兩者的資訊都是從 filterDefs 中的對象擷取的。
在了解了上述邏輯後,在應用程式中動态的添加一個 filter 的思路就清晰了:
• 調用 ApplicationContext 的 addFilter 方法建立 filterDefs 對象,需要反射修改應用程式的運作狀态,加完之後再改回來;
• 調用 StandardContext 的 filterStart 方法生成 filterConfigs;
• 調用 ApplicationFilterRegistration 的 addMappingForUrlPatterns 生成 filterMaps;
• 為了相容某些特殊情況,将我們加入的 filter 放在 filterMaps 的第一位,
可以自己修改 HashMap 中的順序,
也可以在自己調用 StandardContext 的 addFilterMapBefore 直接加在 filterMaps 的第一位。
基于以上思路的實作在 threedr3am 師傅的 文章 中有實作代碼,
既然知道了需要修改的關鍵位置,那就沒有必要調用方法去改,直接用反射加進去就好了,
其中中間還有很多小細節可以變化。
寫一個 demo 模拟一下動态添加一個 filter 的過程。
首先我們有一個 IndexServlet,如果請求參數有 id 的話,則列印在頁面上。
現在我們想實作在程式運作過程中動态添加一個 filter ,
提供将 id 參數的數字值 + 3 的功能(随便瞎想的功能。)
具體代碼放在了 org.su18.memshell.web.servlet.AddTomcatFilterServlet 中
普通通路時,會将 id 的值列印出來
通路添加 filter。
再次通路,id 參數會被加三。
Servlet 記憶體馬
Servlet 是 Server Applet(伺服器端小程式)的縮寫,用來讀取用戶端發送的資料,處理并傳回結果。也是最常見的 Java 技術之一。
與 Filter 相同,本小節也僅僅讨論使用 ServletContext 的相關方法添加 Servlet。
還是首先來看一下實作類 ApplicationContext 的 addServlet 方法。
與上一小節看到的 addFilter 方法十分類似。
那麼我們面臨同樣的問題,在一次通路到達 Tomcat 時,是如何比對到具體的 Servlet 的?
這個過程簡單一點,隻有兩部走:
• ApplicationServletRegistration 的 addMapping 方法調用 StandardContext#addServletMapping 方法,
在 mapper 中添加 URL 路徑與 Wrapper 對象的映射(Wrapper 通過 this.children 中根據 name 擷取)
• 同時在 servletMappings 中添加 URL 路徑與 name 的映射。
直接調用相關方法進行添加,當然是用反射直接寫入也可以,有一些邏輯較為複雜。
測試代碼在 org.su18.memshell.web.servlet.AddTomcatServlet 中,
通路這個 servlet 會在程式中生成一個新的 Servlet :/su18。
看一下效果。
Listener 記憶體馬
Servlet 和 Filter 是程式員常接觸的兩個技術,
是以在網絡上對于之前兩小節的讨論較多,
對于 Listener 的讨論較少。但實際上這個點還是有很多師傅關注到了。
Listener 可以譯為監聽器,監聽器用來監聽對象或者流程的建立與銷毀,通過 Listener,可以自動觸發一些操作,
是以依靠它也可以完成記憶體馬的實作。
先來了解一下 Listener 是幹什麼的,看一下 Servlet API 中的注釋。
應用中可能調用的監聽器如下
.servlet.http.HttpSessionListener:對 Session 整體狀态的監聽
• javax.servlet.http.HttpSessionAttributeListener:對 Session 屬性的監聽
可以看到 Listener 也是為一次通路的請求或生命周期進行服務的,
在上述每個不同的接口中,都提供了不同的方法,用來在監聽的對象發生改變時進行觸發。
而這些類接口,實際上都是 java.util.EventListener 的子接口。
這裡我們看到,在 ServletRequestListener 接口中,提供了兩個方法在 request 請求建立和銷毀時進行處理,比較适合我們用來做記憶體馬。
除了這個 Listener,其他的 Listener 在某些情況下也可以觸發作為記憶體馬的實作.
ServletRequestListener 提供兩個方法:
requestInitialized 和 requestDestroyed,
兩個方法均接收 ServletRequestEvent 作為參數,
ServletRequestEvent 中又儲存了 ServletContext 對象和 ServletRequest 對象,
是以在通路請求過程中我們可以在 request 建立和銷毀時實作自己的惡意代碼,完成記憶體馬的實作。
Tomcat 中 EventListeners 存放在
StandardContext 的 applicationEventListenersObjects 屬性中,
同樣可以使用 StandardContext 的相關 add 方法添加。
我們還是實作一個簡單的功能,在 requestDestroyed 方法中擷取 response 對象,
向頁面原本輸出多寫出一個字元串。正常通路時:
添加 Listener,可以看到,
由于我們是在 requestDestroyed 中植入惡意邏輯,那麼在本次請求中就已經生效了:
通路之前的路徑也生效了:
除了 EventListener,Tomcat 還存在了一個 LifecycleListener ,
當然也肯定有可以用來觸發的實作類,但是用起來一定是不如 ServletRequestListener
由于在 ServletRequestListener 中可以擷取到 ServletRequestEvent,
這其中又存了很多東西,ServletContext/StandardContext 都可以擷取到,那玩法就變得更多了。
在 requestInitialized 中監聽,如果通路到了某個特定的 URL,
或這次請求中包含某些特征(可以拿到 request 對象,随便怎麼定義),
則新起一個線程去 StandardContext 中注冊一個 Filter,可以實作某些惡意功能。
在 requestDestroyed 中再起一個新線程 sleep 一定時間後将我們添加的 Filter 解除安裝掉。
有了一個真正的動态後門,隻有用的時候才回去注冊它,用完就删。