JavaWeb 內存馬一周目通關攻略


前言

最近學習研究一下目前業內主流的 JavaWeb 內存馬實現方式,並探究完美的查和殺的方法。這個課題早就想研究,后來把它計划到了反序列化中的子項,但是現在要給 RASP 加功能,所以就先拿出來寫了。本篇博客除了基礎性知識研究記錄,將會給出初步的內存馬查找的思路及簡單代碼,完整具體查殺的代碼將由於商業性原因不會開源,但是歡迎師傅們在相關思路上進行討論。

本文前幾章是基礎知識學習和研究記錄,如果你對內存馬很熟悉,可以選擇跳過,直接看后面對攻防思路的討論。

本文的部分實現已上傳至 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. 使用 ServletContainerInitializer 的 onStartup 方法在初始化時注冊(非動態,后面會描述)。

本節只討論使用 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 中的注釋。

在應用中可能調用的監聽器如下:

  • ServletContextListener:用於監聽整個 Servlet 上下文(創建、銷毀)
  • ServletContextAttributeListener:對 Servlet 上下文屬性進行監聽(增刪改屬性)
  • ServletRequestListener:對 Request 請求進行監聽(創建、銷毀)
  • ServletRequestAttributeListener:對 Request 屬性進行監聽(增刪改屬性)
  • javax.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 卸載掉。

這樣我們就有了一個真正的動態后門,只有用的時候才回去注冊它,用完就刪。平常使用掃內存馬的軟件也根本掃不出來。這個例子也是我突然拍腦袋想出來的,可能實際意義並不大,但是可以看出 Listener 內存馬的危害性和玩法的變化要大於 Filter/Servlet 內存馬的。

控制器、攔截器、管道

在上一章節中,我們分析了 Servlet API 中提供的能夠利用實現內存馬的一些點。總結來說:

  • Servlet :在用戶請求路徑與處理類映射之處,添加一個指定路徑的指定處理類;
  • Filter:在用戶處理類之前的,用來對請求進行額外處理提供額外功能的類;
  • Listener:在 Filter 之外的監聽進程。

那么除了 Servlet API ,其實在常用的框架、組件、中間件的實現中,只要采用了類似的設計思想和設計模式的位置,都可以、逐漸或正在被發掘出來做為內存馬的相關實現。

本章就是其中的幾個例子。

Spring Controller 內存馬

Servlet 能做內存馬,Controller 當然也能做,不過 SpringMVC 可以在運行時動態添加 Controller 嗎?答案是肯定的。在動態注冊 Servlet 時,注冊了兩個東西,一個是 Servlet 的本身實現,一個 Servlet 與 URL 的映射 Servlet-Mapping,在注冊 Controller 時,也同樣需要注冊兩個東西,一個是 Controller,一個是 RequestMapping 映射。這里使用 spring-webmvc-5.2.3 進行調試。

所謂 Spring Controller 的動態注冊,就是對 RequestMappingHandlerMapping 注入的過程,如果你對 SpringMVC 比較了解,可以直接看這篇文章然后再看我的注入代碼,如果比較關注整個流程,可以接着向下看。

首先來看兩個類:

  • RequestMappingInfo:一個封裝類,對一次 http 請求中的相關信息進行封裝。
  • HandlerMethod:對 Controller 的處理請求方法的封裝,里面包含了該方法所屬的 bean、method、參數等對象。

SpringMVC 初始化時,在每個容器的 bean 構造方法、屬性設置之后,將會使用 InitializingBean 的 afterPropertiesSet 方法進行 Bean 的初始化操作,其中實現類 RequestMappingHandlerMapping 用來處理具有 @Controller 注解類中的方法級別的 @RequestMapping 以及 RequestMappingInfo 實例的創建。看一下具體的是怎么創建的。

它的 afterPropertiesSet 方法初始化了 RequestMappingInfo.BuilderConfiguration 這個配置類,然后調用了其父類 AbstractHandlerMethodMapping 的 afterPropertiesSet 方法。

這個方法調用了 initHandlerMethods 方法,首先獲取了 Spring 中注冊的 Bean,然后循環遍歷,調用 processCandidateBean 方法處理 Bean。

processCandidateBean 方法

isHandler 方法判斷當前 bean 定義是否帶有 Controller 或 RequestMapping 注解。

detectHandlerMethods 查找 handler methods 並注冊。

這部分有兩個關鍵功能,一個是 getMappingForMethod 方法根據 handler method 創建RequestMappingInfo 對象,一個是 registerHandlerMethod 方法將 handler method 與訪問的 創建 RequestMappingInfo 進行相關映射。

這里我們看到,是調用了 MappingRegistry 的 register 方法,這個方法將一些關鍵信息進行包裝、處理和儲存。

關鍵信息儲存位置如下:

以上就是整個注冊流程,那當一次請求進來時的查找流程呢?在 AbstractHandlerMethodMapping 的 lookupHandlerMethod 方法:

  • 在 MappingRegistry.urlLookup 中獲取直接匹配的 RequestMappingInfos
  • 如果沒有,則遍歷所有的 MappingRegistry.mappingLookup 中保存的 RequestMappingInfos
  • 獲取最佳匹配的 RequestMappingInfo 對應的 HandlerMethod

上述的流程和較詳細的流程描述在這篇文章中可以查看,由於我這里使用的版本與之不同,所以一些代碼和細節可能不同。

那接下來就是動態注冊 Controller 了,LandGrey 師傅在他的文章中列舉了幾種可用來添加的接口,其實本章上都是調用之前我們提到的 MappingRegistry 的 register 方法。

和 Servlet 的添加較為類似的是,重點需要添加的就是訪問 url 與 RequestMappingInfo 的映射,以及是 RequestMappingInfo 與 HandlerMethod 的映射。

這里我不會使用 LandGrey 師傅提到的接口,而是直接使用 MappingRegistry 的 register 方法來添加,當然,同樣可以通過自己實現邏輯,通過反射直接寫進重要位置,不使用 Spring 提供的接口。

正常訪問 indexController

動態添加 Controller

訪問添加的 Controller

這里注意的是,在不同版本中,參數名、調用細節都有不同。

Spring Interceptor 內存馬

這里的描述的 Intercepor 是指 Spring 中的攔截器,它是 Spring 使用 AOP 對 Filter 思想的令一種實現,在其他框架如 Struts2 中也有攔截器思想的相關實現。不過這里將僅僅使用 Spring 中的攔截器進行研究。Intercepor 主要是針對 Controller 進行攔截。

Intercepor 是在什么時候調用的呢?又配置儲存在哪呢?這部分比較簡單,直接用文字來描述一下這個過程:

  • Spring MVC 使用 DispatcherServlet 的 doDispatch 方法進入自己的處理邏輯;
  • 通過 getHandler 方法,循環遍歷 handlerMappings 屬性,匹配獲取本次請求的 HandlerMapping;
  • 通過 HandlerMapping 的 getHandler 方法,遍歷 this.adaptedInterceptors 中的所有 HandlerInterceptor 類實例,加入到 HandlerExecutionChain 的 interceptorList 中;
  • 調用 HandlerExecutionChain 的 applyPreHandle 方法,遍歷其中的 HandlerInterceptor 實例並調用其 preHandle 方法執行攔截器邏輯。

通過這次流程我們就清晰了,攔截器本身需要是 HandlerInterceptor 實例,儲存在 AbstractHandlerMapping 的 adaptedInterceptors 中。寫入非常簡單,直接上例子。

正常訪問

添加攔截器

再次訪問

Tomcat Valve 內存馬

Tomcat 在處理一個請求調用邏輯時,是如何處理和傳遞 Request 和 Respone 對象的呢?為了整體架構的每個組件的可伸縮性和可擴展性,Tomcat 使用了職責鏈模式來實現客戶端請求的處理。在 Tomcat 中定義了兩個接口:Pipeline(管道)和 Valve(閥)。這兩個接口名字很好的詮釋了處理模式:數據流就像是流經管道的水一樣,經過管道上個一個個閥門。

Pipeline 中會有一個最基礎的 Valve(basic),它始終位於末端(最后執行),封裝了具體的請求處理和輸出響應的過程。Pipeline 提供了 addValve 方法,可以添加新 Valve 在 basic 之前,並按照添加順序執行。

Tomcat 每個層級的容器(Engine、Host、Context、Wrapper),都有基礎的 Valve 實現(StandardEngineValve、StandardHostValve、StandardContextValve、StandardWrapperValve),他們同時維護了一個 Pipeline 實例(StandardPipeline),也就是說,我們可以在任何層級的容器上針對請求處理進行擴展。這四個 Valve 的基礎實現都繼承了 ValveBase。這個類幫我們實現了生命接口及MBean 接口,使我們只需專注閥門的邏輯處理即可。

先來簡單看一下接口的定義,org.apache.catalina.Pipeline 的定義如下:

org.apache.catalina.Valve 的定義如下:

具體實現的代碼邏輯在這篇文章描述的比較好。

Tomcat 中 Pipeline 僅有一個實現 StandardPipeline,存放在 ContainerBase 的 pipeline 屬性中,並且 ContainerBase 提供 addValve 方法調用 StandardPipeline 的 addValve 方法添加。

Tomcat 中四個層級的容器都繼承了 ContainerBase ,所以在哪個層級的容器的標准實現上添加自定義的 Valve 均可。

添加后,將會在 org.apache.catalina.connector.CoyoteAdapter 的 service 方法中調用 Valve 的 invoke 方法。

這里我們只要自己寫一個 Valve 的實現類,為了方便也可以直接使用 ValveBase 實現類。里面的 invoke 方法加入我們的惡意代碼,由於可以拿到 Request 和 Response 方法,所以也可以做一些參數上的處理或者回顯。然后使用 StandardContext 中的 pipeline 屬性的 addValve 方法進行注冊。

首先正常訪問:

動態添加自定義惡意 Valve,會先調用 response 寫入字符串

再次訪問出現效果:

如果對管道和閥的定義理解困難的話,按照 FilterChain 和 Filter 的關系來理解也可。

GlassFish Grizzly Filter 內存馬

GlassFish 使用 grizzly 組件來完成 NIO 的工作,類似 Tomcat 中的 connector 組件。在 HTTP 下,grizzly 負責解析和序列化 HTTP 請求/響應,grizzly 有職責鏈設計模式的體現,提供了 Filter 和 FilterChain 等接口及實現,就可以被用來寫入內存馬。

中間的實現過程有較多難點和細節,這里不占篇幅分析,感興趣的師傅自行觀看代碼實現。我們直接來看一下效果,添加之后隨便訪問頁面,發現結果被成功寫回:

基於字節碼修改的字節碼

Java Agent 內存馬

哈哈哈哈哈哈哈哈哈 java agent 內存馬哈哈哈哈哈哈哈哈哈哈哈哈哈哈,又有誰能夠想到, java agent 技術能被用來寫內存馬嗎?哈哈哈哈哈哈哈哈哈哈哈哈哈哈隔,花里胡哨。

Java Agent 技術我這里不再介紹,我寫過一篇學習筆記,總體來說就是可以使用 Instrumentation 提供的 retransform 或 redefine 來動態修改 JVM 中 class 的一種字節碼增強技術,可以直接理解為,這是 JVM 層面的一個攔截器。這里直接來看一下內存馬的實現。

首先是冰蠍作者 rebeyond 師傅,他的項目提出了這種想法,在這個項目中,他 hook 了 Tomcat 的 ApplicationFilterChain 的 internalDoFilter方法。

使用 javassist 在其中插入了自己的判斷邏輯,也就是項目的 ReadMe 中 usage 中提供的一些邏輯,

也就是說在 Tomcat 調用 ApplicationFilterChain 對請求調用 filter 鏈處理之前加入惡意邏輯。

師傅在冰蠍中同樣加入了內存馬的功能的實現,調用代碼位置 net.rebeyond.behinder.payload.java.MemShell,目前對於 Servlet 和 Filter 還是空實現,可能是后續要加的功能?

agent 端在 net/rebeyond/behinder/resource/tools 中,應該是根據不同的類型會上傳不同的注入包。

但是這次不再 Hook Tomcat 的方法,而是選擇 Hook 了 Servlet-API 中更具有通用性的 javax.servlet.http.HttpServlet 的 service 方法,如果檢測出是 Weblogic,則選擇 Hook weblogic.servlet.internal.ServletStubImpl 方法。

那么說到這里,使用插樁技術的 RASP、IAST 的使用者一下就可以明白:如果都能做到這一步了,能玩的就太多了。能下的 Hook 點太多,能玩的姿勢也太多了。

比如,在 memshell-inject 項目中,我模仿冰蠍的實現方式,hook 了 HttpServletRequest 實現類的 getQueryString 方法,在方法返回時修改返回內容進行測試。

正常訪問,頁面獲取當前請求的 QueryString 並打印:

attach 測試使用的 agent:

再次訪問:

只能說,將這種功能提供給滲透人員,師傅膽子是真的大,用冰蠍的大多數人還是不清楚原理的,但是小手一點,不知道有授權還是沒有的站,JVM 里的字節碼就被改了。等各種魔改版冰蠍泛濫后,一些安全防御能力較弱的站將會被 agent 改的體無完膚,再配合加持的持久化技術,不知道會被玩成什么樣。

但是既然思路已經開放出來了,攻與防的對抗一定的要有的,作為 RASP 防御方,我一定是要使用魔法來打敗魔法,請看下一章的攻防思路及技巧。

補充:順便看一下同樣流行的 webshell 管理工具哥斯拉 的內存馬是怎么實現的,截止到文章編寫時,發布的 release 版本為 v3.03-godzilla。

可以看到是提供了兩種內存馬的寫入,一種是寫入 Servlet,一種是寫入 Listener。

但是兩種寫入方式都是針對 Tomcat 的,並不具有通用性,我並沒有用過這款工具,只是簡單翻了一下代碼,沒有找到其他發現,如果有,請聯系我修改文章。

補充2:再來看下 SpringBoot 持久化 WebShell ZhouYu,在 zhouyu.core.init.WriteShellTransformer 中,可以看到是選擇了 hook org.springframework.web.servlet.DispatcherServlet 的 doService 方法。

此項目中涉及到 jar 包中的 class 替換,以及清空后續的 ClassFileTransformer 的持久化思路。

攻防思路及技巧

那么以上就是目前安全行業內主要討論的內存馬添加方式,再加上我找到的一些實現,由於這里主要關注的是內存馬的添加,所以關於上下文獲取、關鍵類的定位等技術細節都略過了。

對於一些內存馬的實現,由於篇幅的原因,在本篇博客中僅示范了在 Tomcat 下的效果,在項目代碼中,還有在研究測試中遇到的其他實現,感興趣的師傅請 clone 代碼仔細觀看。

從接下來根據上面的鋪墊開始探究查殺的具體思路和實現。正所謂未知攻,焉知防?在這一章節中,重點將討論對於目前對於內存馬的查殺與攻防思路的碰撞。對於各種類型內存馬的查殺,各位師傅已經給出了自己的相關思路,我這里根據自己的想法和經驗,再結合 RASP 技術的相關實現進行討論。

Dynamic Add

動態注冊,首先,在 Servlet-API 中我們討論的對於 Servlet,Filter,Listener 的實現,在理論上 API 中是提供添加接口的,但是在之前的分析中提到過,只有程序初始化時可以用,程序運行時會拋異常,可以通過修改 state 值來進行繞過。

此外,在寫入過程中,對於某些中間件自己實現的 Context 對象也提供了添加 Servlet/Filter/Listener 的以及相關 mapping 的方法,以 RASP 的防御思路來說,就是 Hook 這些 add/set 方法,在程序運行中禁止動態添加。

但是這種情況分分鍾被繞過,因為在我的示例代碼中,有大部分是沒有調用這些方法,而是直接將對象寫到儲存的 maps/lists/arrays 中的,可以繞過添加方法調用的 hook 點。而且,這種 Hook 防御各個中間件的實現也不一樣,不能通用,需要適配。

除了 Servlet-API,也可以使用中間件中的一些實現,來完成內存馬的寫入操作,我這里僅僅在 GlassFish 中找了一點,而實際上還可以被挖掘出很多。

所以,在防御功能的實現中,我們需要重點關注的就是這些具有職責鏈設計對象實現的儲存和緩存位置。

ClassLoader

考慮到內存馬通常是配合反序列化等類型的漏洞進行注入,那如果程序里注冊的 Servlet/Filter/Listener 是一些高危的 ClassLoader 加載進來的,是不是就說明這個類本身危險很大的?

答案是肯定的,但是同樣是可以被繞過的,例如注入 class 到當前線程中,然后實例化注入內存馬。

DumpClass

在 c0ny1 師傅的內存馬查殺項目中,使用了 dumpclass 功能,將 filterMaps 中的所有 filterMap 遍歷出來,然后提供了 dumpclass,很顯然,如果獲得目標類的 class 反編譯代碼,加入人為判斷的模式,就可以知道 filter 代碼中是否有惡意操作了。在 copagent 中也使用了類似的功能。這種檢查思路作為一款查殺工具完全沒有問題,但如果作為一款產品來說就有利有弊了。

Class File

內存馬最大的特點就是儲存在內存,無文件落地,那也就代表了這個類對應的 ClassLoader 目錄下沒有對應的 class 文件,這種思路也在 c0ny1 和 jweny 師傅的文章中提到了。

但是這種思路能否被繞過呢?能否讓一個無文件落地的內存馬在使用其 ClassLoader 的 getResource 方法時不返回空呢?答案在 findResource 中。

但無論如何這是一個非常好的思路,可以借鑒。

Mbeans

有師傅提出了利用 VisualVM 來監控 Mbeans 來檢測內存馬的思路,原理是在注冊類似 Filter 的時候會觸發 registerJMX 的操作來注冊 mbean,不過 mbean 的注冊只是為了方便資源的管理,並不影響功能,所以攻擊者植入內存 Webshell 之后,完全可以通過執行 Java 代碼來卸載掉這個 mbean 來隱藏自己。

以上描述來自長亭 Litch1 師傅的文章,師傅也在他的文章中給出了注銷 Tomcat Filter 注冊的 mbeans 的代碼,可以看出使用這種方式有檢測出部分內存馬的可能,但是不通用也無法覆蓋全面,我這里不會采用這種方式,將不會進行討論。

Retransform/Redefine

如何查殺使用 JavaAgent 技術修改字節碼的內存馬?寬字節安全給出了相關文章 進行討論。

寫入 Agent 馬后如何不被別人干掉?threedr3am 師傅的項目使用了阻止后續 javaagent 加載的方式,防止webshell 被查殺。

Agent 馬的攻防還在進行中,那么對其防御,肯定是要用魔法來打敗魔法,這里不廢話,請直接看實現。

查殺內存馬

在這一章節中,將會給出我對於查殺內存馬的相關思考,以及我實現的查殺代碼的相關測試記錄。

查找目前服務器上的內存馬

結合之前師傅們提出的多個維度的判斷方式,配合 JavaAgent 技術來進行內存馬的查殺。

防御內存馬的添加與訪問

對應接口 Hook,以監聽者模式監控系統關鍵屬性。

殺掉目前存在的內存馬

對於非Agent馬兩種思路:

  • 從系統中移除該對象。(推薦)
  • 訪問時拋異常(或跳過調用),中斷此次調用。

對於Agent馬:retransform。

處理想要持久化的內存馬

根據網上的一些持久化思路,包括 ServletContainerInitializer/@Filter/@Servlet/addShutdownHook/startUpClass 等等,有針對性的進行查殺和防御。

參考

LandGrey 師傅的項目 copagent 使用 javaagent 技術獲得了全部的類,判斷其包名、實現類名、接口名、注解來提取關鍵類,並根據類是否在磁盤上有資源鏈接對象、類中是否包含惡意行為關鍵字來判斷其是否為內存馬。

在這里我參考了其項目,在 retransform 時做了同樣的事,並在此基礎上添加了一定的防御功能:同時 retransform 了惡意類,並在其關鍵調用代碼調用時處理自己的邏輯。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM