剖析Java内存马1

前文

内存马是一种仅在内存中执行,不写入硬盘的恶意程序,其能够隐蔽地进行攻击,同时躲避常规的病毒检测手段。是当今攻防模式下常见的攻击形式,最常见的就是java内存马,常分为以下几个部分:传统web型、spring系列框架型、中间件型、其他内存马、Agent型内存马等,常见的四个类型为:Listener型、Filter型、Servlet型以及Agent型,本文主要深度剖析一下前三种类型

Servlet

我们先了解一下Servlet容器架构。Tomcat 设计了 4 种容器,分别是 Engine、Host、Context 和 Wrapper,这4种容器不是相互独立的关系,而是父子关系,逐层包含,如下图所示:

一个Service最多只能有一个Engine,Engine表示引擎,用来管理多个虚拟主机的;Host代表就是一个虚拟主机,可以给Tomcat配置多个虚拟主机,一个虚拟主机下面可以部署多个Web应用;一个Context就表示一个Web应用,Web应用中会有多个Servlet,Wrapper就表示一个Servlet。这一点可以从Tomcat的配置文件server.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
<?xml version="1.0" encoding="UTF-8"?>

<!-- 顶层组件,可以包含多个Service -->
<Server port="8005" shutdown="SHUTDOWN">

<!-- 顶层组件,可以包含一个Engine,多个连接器 -->
<Service name="Catalina">
<!-- HTTP协议的连接器 -->
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<!-- AJP协议的连接器 -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
<!-- 一个Engine组件处理Service中的所有请求 -->
<Engine name="Catalina" defaultHost="localhost">
<!-- 处理特定的Host下的请求,可以包含多个Context -->
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t &quot;%r&quot; %s %b" />
<context></context> <!-- 为特定的Web应用处理所有的请求 -->
</Host>
</Engine>
</Service>
</Server>

如果说我们要访问ht tps://xxxx:8080/user/list,那tomcat是如何实现请求定位到具体的servlet的呢?为此tomcat设计了Mapper,其中保存了容器组件与访问路径的映射关系,步骤为:

1
2
3
4
1、根据协议和端口号选定Service和Engine
2、根据域名选定Host
3、根据url路径找到Context组件
4、根据url路径找到Wrapper(Servlet)

我们先设计一个简单的Servlet(tomcat9.0.96)
Main.java

HelloServlet.java

我们在org.apache.catalina.core.StandardWrapper#setServletClass处打断点进行调试

并追踪它的上层调用位置,我们可以发现为org.apache.catalina.startup.ContextConfig#configureContext

追踪configureContext,查看代码

概括一下上述的代码,也就是servlet初始化的六步

1
2
3
4
5
6
创建Wapper对象;
设置Servlet的LoadOnStartUp的值;
设置Servlet的名称;
设置Servlet的class
将配置好的Wrapper添加到Context中;
将url和servlet类做映射

那servlet是如何进行装载的呢,我们在org.apache.catalina.core.StandardWrapper#loadServlet这里打下断点进行调试,重点关注一下startInternal

可以清晰看到,装载的顺序为:Listener、Filter、Servlet

并且我们可以发现,调用了org.apache.catalina.core.StandardContext#loadOnStartup,跟进该方法

先创建一个TreeMap,然后遍历传入的Container数组,将每个Servlet的loadOnStartup值作为键,将对应的Wrapper对象存储在相应的列表中;如果这个loadOnStartup值是负数,除非你请求访问它,否则就不会加载;如果是非负数,那么就按照这个loadOnStartup的升序的顺序来加载,这就是servlet装载流程

Filter

同理,我们先了解一下Filter容器,Filter容器是用于对请求和响应进行过滤和处理的,我用PPT潦草画个图

filter可以理解为一道门,客户端的请求在经过filter会经过servlet,那么如果我们动态创建一个filter并且将其放在最前面,我们的filter就会最先执行,当我们在filter中添加恶意代码,就可以实现命令执行,从而形成内存马。先写个简单的Filter(tomcat9.0.96)

效果就是跑起来后,控制台输出Filter初始化start,当我们访问/test正确路由的时候,控制台继续输出Filter路由成功访问,当我们结束tomcat的时候,会触发destroy方法,从而输出Filter已结束

我们在上面的demo中的doFilter函数这里下断点进行调试,并跟进org.apache.catalina.core.StandardWrapperValve#invoke

继续跟进变量filterChain,找到定义处的代码:

1
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

查看createFilterChain方法

代码有点长,不再一一截了,直接粘过来

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
public static ApplicationFilterChain createFilterChain(ServletRequest request, Wrapper wrapper, Servlet servlet) {
if (servlet == null) {
return null;
} else {
ApplicationFilterChain filterChain = null;
if (request instanceof Request) {
Request req = (Request)request;
if (Globals.IS_SECURITY_ENABLED) {
filterChain = new ApplicationFilterChain();
} else {
filterChain = (ApplicationFilterChain)req.getFilterChain();
if (filterChain == null) {
filterChain = new ApplicationFilterChain();
req.setFilterChain(filterChain);
}
}
} else {
filterChain = new ApplicationFilterChain();
}

filterChain.setServlet(servlet);
filterChain.setServletSupportsAsync(wrapper.isAsyncSupported());
StandardContext context = (StandardContext)wrapper.getParent();
FilterMap[] filterMaps = context.findFilterMaps();
if (filterMaps != null && filterMaps.length != 0) {
DispatcherType dispatcher = (DispatcherType)request.getAttribute("org.apache.catalina.core.DISPATCHER_TYPE");
String requestPath = null;
Object attribute = request.getAttribute("org.apache.catalina.core.DISPATCHER_REQUEST_PATH");
if (attribute != null) {
requestPath = attribute.toString();
}

String servletName = wrapper.getName();
FilterMap[] var10 = filterMaps;
int var11 = filterMaps.length;

int var12;
FilterMap filterMap;
ApplicationFilterConfig filterConfig;
for(var12 = 0; var12 < var11; ++var12) {
filterMap = var10[var12];
if (matchDispatcher(filterMap, dispatcher) && matchFiltersURL(filterMap, requestPath)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig != null) {
filterChain.addFilter(filterConfig);
}
}
}

var10 = filterMaps;
var11 = filterMaps.length;

for(var12 = 0; var12 < var11; ++var12) {
filterMap = var10[var12];
if (matchDispatcher(filterMap, dispatcher) && matchFiltersServlet(filterMap, servletName)) {
filterConfig = (ApplicationFilterConfig)context.findFilterConfig(filterMap.getFilterName());
if (filterConfig != null) {
filterChain.addFilter(filterConfig);
}
}
}

return filterChain;
} else {
return filterChain;
}
}
}

这段代码是用于创建过滤器链的静态方法,它的主要功能是根据请求、包装器和Servlet的状态,构建一个包含适当过滤器的ApplicationFilterChain对象,首先检查是否为空值,如果传入的Servlet为null,直接返回null,表示没有可处理的请求,然后检查请求是否是Request的实例,如果是,则根据安全设置或已存在的过滤器链进行创建,然后将Servlet和异步支持设置到过滤器链中,再从wrapper中获取StandardContext,并查找相关的过滤器映射,再通过循环检查过滤器映射,根据请求的调度类型和路径进行过滤器的匹配,于此同时使用matchDispatcher和matchFiltersURL等方法来检查过滤器是否适用于当前请求,在所有匹配的过滤器都添加后,返回构建好的过滤器链

回过头来,再跟进刚才的filterChain.doFilter方法(org.apache.catalina.core.ApplicationFilterChain#doFilter)

可以看到调用了internalDoFilter方法,跟进该方法可以发现会依次拿到filterConfig和filter

但要是想打入内存马,也就是要动态地创建一个Filter,刚才我们发现在createFilterChain那个函数里面有两个关键点:org.apache.catalina.core.StandardContext#findFilterMaps和org.apache.catalina.core.StandardContext#findFilterConfig

二者的实现代码

这样一来,查找到现有的context,然后往里面插入我们自定义的恶意过滤器映射和过滤器配置,就可以实现动态添加过滤器了,但是如何添加filterMap和filterConfig? 尝试搜索addFilterMap关键词,发现StandardContext中有两个相关的方法

addFilterMap是在一组映射末尾添加新的我们自定义的新映射;而addFilterMapBefore则会自动把我们创建的filterMap自动排到第一位,而且addFilterMapBefore函数中第一步是先执行validateFilterMap这个函数,继续跟进

filterName和filterDef,必须对应得到,因此我们需要自定义filterDef并把它加入到filterDefs,这就需要用到addFilterDef方法

当我们继续去看filterConfig如何添加时,通过搜索,发现找不到类似上面的addFilterConfig这种,但是有filterStart和filterStop这两个方法

ok, 也就是说,只能通过反射的方法去获取相关属性并添加进去

Listener

我们最后再来说一下Listenner,还是先看看我用PPT画的潦草图

Listener是最先被加载的,所以动态注册一个恶意的Listener,就又可以形成一种内存马了。常见的listener有:ServletContextListener、ServletRequestListener、HttpSessionListener、HttpSessionAttributeListener。其中,ServletRequestListener是最适合做内存马的,利用条件很低:只要访问服务就可触发

我们先写一个简单的Listener(tomcat9.0.96)

我们在如下图所示的两个地方打断点进行调试

通过上图,可以看到org.apache.catalina.core.StandardContext#listenerStart方法的调用,跟进查看代码,可以发现主要干两个事情:一是通过findApplicationListeners找到这些Listerner的名字;二是实例化这些listener

接着就是分类放置,我们的ServletRequestListener被放在了eventListeners里面,分类放置之后,还做了一个动作:eventListeners.addAll(Arrays.asList(getApplicationEventListeners()));

Arrays.asList就是将数组转换为列表;eventListeners.addAll就是将括号里面的内容添加到之前实例化的监听器列表eventListeners中。至于括号里面的这个getApplicationEventListeners方法,我们跟进查看代码

就是把applicationEventListenersList转换成一个包含任意类型对象的数组,我们完全可以使其变成包含各种类型的应用程序事件监听器的数组,从而进行利用。而Listener可以有两个来源,一是根据web.xml文件或者@WebListener注解实例化得到的Listener;二就是applicationEventListenersList中的Listener,如果想打内存马的话,前者肯定不合适,更适合开发人员使用,因此我们需要找找有没有类似之前我们用到的addFilterConfig这种函数,通过查找,找到了一个方法叫addApplicationEventListener,在StandardContext.java里面

跟进该方法,查看代码,正好符合我们利用内存马的需求

1
2
3
public void addApplicationEventListener(Object listener) {
applicationEventListenersList.add(listener);
}

【参考文章】
https://blog.nowcoder.net/n/0c4b545949344aa0b313f22df9ac2c09
https://www.maishuren.top/archives/tomcat-zhong-servlet-rong-qi-de-she-ji-yuan-li
https://mp.weixin.qq.com/s/hdqwsYtBN_IpaH2DGZLPoA


剖析Java内存马1
http://example.com/2024/10/23/剖析Java内存马1/
作者
liuty
发布于
2024年10月23日
许可协议