原创 | 浅谈Log4j2在JFinal的检测
前言
JFinal 是基于Java 语言的极速 web 开发框架,大量的web应用是基于其进行构建的。同样的JFinal也有自己的日志实现机制。这里尝试找到一种稳定的触发方式来方便log4j2漏洞的排查/运营。
JFinal日志相关
默认使用的日志框架
JFinal使用的是封装了一层的日志框架,可以兼容其余所有日志框架。主要是接口ILogFactory和一个抽象类Log,JFinal源代码中使用的日志工具就是Log的子类,包含:
JdkLog,Log4jLog,Slf4jLog:
以log4j为例,在Log4jLog类中对log4j对应的方法进行了封装:
并且JFinal默认使用的是log4j记录日志,如果引入的依赖中没有log4j的话会使用jdk-log:
public abstract class Log {private static ILogFactory defaultLogFactory = null;static {init();}static void init() {if (defaultLogFactory == null) {try {Class.forName("org.apache.log4j.Logger");Class<?> log4jLogFactoryClass = Class.forName("com.jfinal.log.Log4jLogFactory");defaultLogFactory = (ILogFactory)log4jLogFactoryClass.newInstance(); // return new Log4jLogFactory();} catch (Exception e) {defaultLogFactory = new JdkLogFactory();}}}
也就是说默认情况下是不受Apache Log4j2 漏洞影响的。
在JFinal中使用log4j2
但是log4j 1.x版本已经不再维护了,并且Apache Log4j <= 1.2.17版本受到CVE-2019-17571 影响,所以不排除有使用log4j2的情况。在JFinal中使用log4j2也很简单,只需要模仿Log4jLog类对其进行封装就可以了。
以maven项目为例,首先引入log4j2的相关依赖。然后继承com.jfinal.log.Log类,对Log4j2的相关方法进行封装:
import com.jfinal.log.Log;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;public class Log4j2Log extends Log {private Logger log;public Log4j2Log(Class<?> clazz) {log = LogManager.getLogger(clazz);}public Log4j2Log(String name) {log = LogManager.getLogger(name);}@Overridepublic void debug(String message) {log.debug(message);}@Overridepublic void debug(String message, Throwable t) {log.debug(message, t);}@Overridepublic void info(String message) {log.info(message);}@Overridepublic void info(String message, Throwable t) {log.info(message, t);}@Overridepublic void warn(String message) {log.warn(message);}@Overridepublic void warn(String message, Throwable t) {log.warn(message, t);}@Overridepublic void error(String message) {log.error(message);}@Overridepublic void error(String message, Throwable t) {log.error(message, t);}@Overridepublic void fatal(String message) {log.fatal(message);}@Overridepublic void fatal(String message, Throwable t) {log.fatal(message, t);}@Overridepublic boolean isDebugEnabled() {return false;}@Overridepublic boolean isInfoEnabled() {return false;}@Overridepublic boolean isWarnEnabled() {return true;}@Overridepublic boolean isErrorEnabled() {return true;}@Overridepublic boolean isFatalEnabled() {return false;}}
然后实现com.jfinal.log.ILogFactory接口:
public class Log4j2Factory implements ILogFactory{@Overridepublic Log getLog(Class<?> clazz) {// TODO Auto-generated method stubreturn new Log4j2Log(clazz);}@Overridepublic Log getLog(String name) {// TODO Auto-generated method stubreturn new Log4j2Log(name);}}
最后在JFinalConfig中添加log4j2的配置就完成了:
/*** 配置常量*/@Overridepublic void configConstant(Constants me) {me.setLogFactory(new Log4j2Factory());}
分析验证
引入了漏洞版本的log4j2依赖的话,自然会受到影响,同样的如果想找到一个稳定触发验证的point。思路之一是可以寻找打印日志的地方。
以JFinal4.5为例,因为本质上的实现其实是封装了一个org.apache.logging.log4j.Logger,然后调用log4j2对应的方法。没有spring那么复杂。只需要找到用户可控且调用了com.jfinal.log中对应的日志方法的class就可以了。下面记录具体的过程:
JFinal中定义了一个过滤器JFinalFilter,它是整个框架的入口。在其doFilter方法里可以看到对请求进行了相应的处理:
handler.handle(target, request, response, isHandled); 是整个Filter最核心的方法,通过JFinalFilter的init方法进行获取:
public void init(FilterConfig filterConfig) throws ServletException {if (jfinalConfig == null) {createJFinalConfig(filterConfig.getInitParameter("configClass"));}jfinal.init(jfinalConfig, filterConfig.getServletContext());String contextPath = filterConfig.getServletContext().getContextPath();contextPathLength = (contextPath == null || "/".equals(contextPath) ? 0 : contextPath.length());constants = Config.getConstants();encoding = constants.getEncoding();jfinalConfig.onStart();jfinalConfig.afterJFinalStart();handler = jfinal.getHandler(); // 开始接受请求}
进一步查看其实是通过jfinal类来获取的,返回的是一个actionHandler为首handler chain。
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.handle(target, request, response, isHandled); 实际上会调用ActionHandler的handle方法,这里找到了其中一个log4j2漏洞的触发点:
ActionHandler实际上主要用于处理路由,这里想要触发对应的逻辑只需要访问不存在的Action 就可以了。验证下实际的猜想。
直接在url path中访问相关的poc,发现并没有触发:
public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) {if (target.indexOf('.') != -1) {return ;}......
但是验证后并没有调用log.warn方法,控制台里也没有对应的日志输出:
这里主要跟日志级别有关系。根据上面的分析可以知道,调用的是log.warn(),在触发的位置下断点调试:
首先是log4j2的logIfEnabled方法:
熟悉log4j2漏洞调用链的话应该知道logMessage后具体会调用jndilookup的逻辑,那么问题应该是在isEnabled方法这里了:
跟进isEnabled方法:
继续跟进,在return方法可以看到,这里首选判断日志等级是否为空,同时当前等级要大于等级设置的等级:
boolean filter(Level level, Marker marker, String msg, Throwable t) {Filter filter = this.config.getFilter();if (filter != null) {Result r = filter.filter(this.logger, level, marker, msg, t);if (r != Result.NEUTRAL) {return r == Result.ACCEPT;}}return level != null && this.intLevel >= level.intLevel();}
从调试信息可以看到,当前的等级是200,但是warn的等级是300,不符合判断逻辑,所以并没有调用对应的逻辑(Spring Boot默认的日志级别为INFO,所以不存在类似的问题):
log4j文档里有对应的说明,200对应的是error,也就是error和fatal方法都是可以触发的,但是在jfinal中没找到error和fatal比较直观的触发点:
再次印证猜想,修改对应的文件配置日志的等级。同样的请求之前的poc:
控制台也输出了对应的日志信息:
这里以本地执行计算器的方式验证效果,同样是成功触发的:
