原创 | 浅谈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);
}
@Override
public void debug(String message) {
log.debug(message);
}
@Override
public void debug(String message, Throwable t) {
log.debug(message, t);
}
@Override
public void info(String message) {
log.info(message);
}
@Override
public void info(String message, Throwable t) {
log.info(message, t);
}
@Override
public void warn(String message) {
log.warn(message);
}
@Override
public void warn(String message, Throwable t) {
log.warn(message, t);
}
@Override
public void error(String message) {
log.error(message);
}
@Override
public void error(String message, Throwable t) {
log.error(message, t);
}
@Override
public void fatal(String message) {
log.fatal(message);
}
@Override
public void fatal(String message, Throwable t) {
log.fatal(message, t);
}
@Override
public boolean isDebugEnabled() {
return false;
}
@Override
public boolean isInfoEnabled() {
return false;
}
@Override
public boolean isWarnEnabled() {
return true;
}
@Override
public boolean isErrorEnabled() {
return true;
}
@Override
public boolean isFatalEnabled() {
return false;
}
}
然后实现com.jfinal.log.ILogFactory接口:
public class Log4j2Factory implements ILogFactory{
@Override
public Log getLog(Class<?> clazz) {
// TODO Auto-generated method stub
return new Log4j2Log(clazz);
}
@Override
public Log getLog(String name) {
// TODO Auto-generated method stub
return new Log4j2Log(name);
}
}
最后在JFinalConfig中添加log4j2的配置就完成了:
/**
* 配置常量
*/
@Override
public 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:
控制台也输出了对应的日志信息:
这里以本地执行计算器的方式验证效果,同样是成功触发的: