log4j2漏洞解决方案:log4j2转换为logback

背景

Apache Log4j2是项目中常用的Java日志框架。在2021年11月,阿里云安全团队向Apache官方报告了Apache Log4j2远程代码执行漏洞。在Log4j 2.15.0-rc2 版本之后,问题解决。但是部分项目由于历史代码原因,直接使用的Log4j2依赖包的类,在升级log4j2版本时,代码出现冲突,在这种情况下,我们可以log4j2转换为logback,在log4j2依赖不变更的情况下,解决安全问题。

解决方案

log4j2转换为logback

可以通过log4j-to-slf4j工具,将log4j2转换为logback。log4j-to-slf4j 允许将编码到Log4j2 API的应用程序路由到SLF4J。使用这个适配器可能会导致一些性能损失,因为在将Log4j2消息传递给SLF4J之前,必须对它们进行格式化。

适用情况(同时满足以下场景):

  1. 无法进行log4j2升级
  2. 依赖log4j2的模块为子模块,或者是第三方依赖
  • maven依赖
    <dependencies>
        <dependency>
            <groupId>com.vhicool.testgroupId>
            <artifactId>commonartifactId>
            <version>1.0-SNAPSHOTversion>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.logging.log4jgroupId>
                    <artifactId>log4j-coreartifactId>
                exclusion>
                <exclusion>
                    <groupId>org.apache.logging.log4jgroupId>
                    <artifactId>log4j-apiartifactId>
                exclusion>
            exclusions>
        dependency>
        <dependency>
            <groupId>org.apache.logging.log4jgroupId>
            <artifactId>log4j-to-slf4jartifactId>
            <version>2.11.2version>
        dependency>
        <dependency>
            <groupId>ch.qos.logbackgroupId>
            <artifactId>logback-classicartifactId>
            <version>1.2.3version>
        dependency>
        <dependency>
            <groupId>ch.qos.logbackgroupId>
            <artifactId>logback-coreartifactId>
            <version>1.2.3version>
        dependency>
    dependencies>
  • 测试类
public class Main {
	public static void main(String[] args) {
       //LoggerUtil为com.vhicool.test:test-common包中的工具类
		LoggerUtil.info("${jndi:ldap://127.0.0.1:8080/Log4jRCE}");
	}
}

LoggerUtil.java

public class LoggerUtil {
	private static final Logger logger = LogManager.getLogger(LoggerUtil.class);

	public static void info(String msg){
        System.out.println(logger.getClass());
		logger.info(msg);
	}
}
  • 输出
Logger : class org.apache.logging.slf4j.SLF4JLogger
[main] INFO com.vhicoo.test.common.LoggerUtil - ${jndi:ldap://127.0.0.1:8080/Log4jRCE}
  • 结论
    从结果可以看出,经转换后,日志最终使用org.apache.logging.slf4j.SLF4JLogger输出。同时也没有触发日志注入的问题。

问题复现

当前模块:com.vhicool.test:test-common:1.0-SNAPSHOT

  • maven依赖
    log4f版本为2.0-2.15
    <properties>
        <log4j-version>2.8.2log4j-version>
    properties>
    <dependencies>
        <dependency>
            <groupId>org.apache.logging.log4jgroupId>
            <artifactId>log4j-coreartifactId>
            <version>${log4j-version}version>
        dependency>
        <dependency>
            <groupId>org.apache.logging.log4jgroupId>
            <artifactId>log4j-apiartifactId>
            <version>${log4j-version}version>
        dependency>
    dependencies>

  • log4j配置文件

<Configuration status="WARN">
	<Appenders>
		<Console name="Console" target="SYSTEM_OUT" follow="true">
			<PatternLayout pattern="%-5p %-60c %x %X{orderId} - %m%n" />
		Console>
	Appenders>
	<Loggers>
		<Root level="info">
			<AppenderRef ref="Console" />
		Root>
	Loggers>
Configuration>
  • 测试方法

日志输出时,实际会调用远程地址ldap://127.0.0.1:8080/Log4jRCE,本身这个地址端口不存在,因此会出现网络异常。

public class Main {
	private static final Logger logger = LogManager.getLogger(Main.class);
	public static void main(String[] args) {
        System.out.println("Logger : "+logger.getClass());
		logger.info("${jndi:ldap://127.0.0.1:8080/Log4jRCE}");
	}
}
  • 输出
Logger : class org.apache.logging.log4j.core.Logger

main WARN Error looking up JNDI resource [ldap://127.0.0.1:8080/Log4jRCE]. javax.naming.CommunicationException: 127.0.0.1:8080 [Root exception is java.net.ConnectException: Connection refused (Connection refused)]
	at com.sun.jndi.ldap.Connection.<init>(Connection.java:238)
	at com.sun.jndi.ldap.LdapClient.<init>(LdapClient.java:137)
	at com.sun.jndi.ldap.LdapClient.getInstance(LdapClient.java:1609)
	at com.sun.jndi.ldap.LdapCtx.connect(LdapCtx.java:2749)
	at com.sun.jndi.ldap.LdapCtx.<init>(LdapCtx.java:319)
...

  • 结论
    通过log4j2输出的日志,会解析${jndi:}中的变量为网络连接,并调用。其中打印日志类为:org.apache.logging.log4j.core.Logger

分析

为什么会导致日志jndi注入的问题

使用log4j2输出日志,如果日志输出串中,包含${}时,log4j会解析变量值的内部方法,内部方法的格式为$prefix:$key。默认支持的内部方法$prefix有:date, ctx, main, env, sys, sd, java, marker, jndi, jvmrunargs, bundle, map, log4j

  • 调用链:调用入口 org.apache.logging.log4j.core.Logger#logMessage
Logger PatternLayout Interpolator JndiLookup InitialContext 输出日志 将日志翻译成文本 获取字符串变量($prefix:$key)解析器 解析字符串变量值$key 查找JNDI资源的值 Logger PatternLayout Interpolator JndiLookup InitialContext

$prefix是jndi时,JndiLookup解析字符串变量$key为JNDI执行语义。例如$key=ldap://127.0.0.1:8080/Log4jRCE的JNDI解析流程如下:

InitialContext#look($key)首先根据$key对应的创建对象的工厂ObjectFactory,查找逻辑:
(1). 解析 k e y 获 取 key获取 keyschema:ldap
(2). 生成ObjectFactory类全限定名:com.sun.jndi.url.ldap.ldapURLContextFactory,生成格式如下:

    "com.sun.jndi.url."+$schema+"."+$schema+"URLContextFactory"
InitialContext ldapURLContextFactory ldapURLContext LdapCtx 生成ldapURLContext ldapURLContext lookup:获取当前命名空间对象 调用ldap服务,实现远程调用 InitialContext ldapURLContextFactory ldapURLContext LdapCtx

你可能感兴趣的:(java工具,maven,log4j2,logback,log4j,flink)