漏洞发现的背景; 漏洞产生的原因以及Lookup相关源码的分析; Log4j2 官方对漏洞的修复; 项目上如何对 Log4j2 漏洞进行修复;
1、漏洞背景


spring-boot-starter-log4j2/Apache Struts2/Apache Solr/Apache Druid/Apache Flink
2、漏洞原因及Lookup源码分析
Apache Log4j2远程代码执行漏洞由Lookup功能引发。Log4j2在默认情况下会开启Lookup功能,用于将特殊值添加到日志中。此功能中也支持对JNDI的Lookup,但由于Lookup对于加载的JNDI内容未做任何限制,使得攻击者可以通过JNDI注入实现远程加载恶意类到应用中,从而造成RCE(远程代码执行)。
public class Test {private static final Logger logger = LogManager.getLogger(Test.class);public static void main(String[] args) {logger.error("${jndi:ldap://127.0.0.1:8018/test}");}}
AbstractLogger#error(final String message)AbstractLogger#logMessage()AbstractLogger#tryLogMessage()Logger#log()DefaultReliabilityStrategy#log()LoggerConfig#log()AppenderControl#tryCallAppender()PatternLayout#encode()MessagePatternConverter#format()StrSubstitutor#substitute()StrSubstitutor#resolveVariable()Interpolator#lookup()JndiLookup#lookup()JndiManager#lookup()InitialContext#lookup()ldapURLContext#lookup()GenericURLContext#lookup()
Interpolator#lookup()的方法,具体实现如下:
@Overridepublic String lookup(final LogEvent event, String var) {if (var == null) {return null;}// 默认前缀分隔符为 :final int prefixPos = var.indexOf(PREFIX_SEPARATOR);if (prefixPos >= 0) {// 获取的前缀为 jndifinal String prefix = var.substring(0, prefixPos).toLowerCase(Locale.US);// 名称为 ldap://127.0.0.1:8018/testfinal String name = var.substring(prefixPos + 1);// 通过前缀 jndi 在 strLookupMap 集合中查找 StrLookup 实现类,返回的是 JndiLookupfinal StrLookup lookup = strLookupMap.get(prefix);if (lookup instanceof ConfigurationAware) {((ConfigurationAware) lookup).setConfiguration(configuration);}String value = null;// 如果 lookup 不为 null, 则会直接调用 StrLookup 的 lookup() 方法if (lookup != null) {value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);}if (value != null) {return value;}var = var.substring(prefixPos + 1);}if (defaultLookup != null) {return event == null ? defaultLookup.lookup(var) : defaultLookup.lookup(event, var);}return null;}
Interpolator#lookup()方法源码的分析,可得知,最终会调用
JndiLookup#lookup(),其源码如下:
@Overridepublic String lookup(final LogEvent event, final String key) {if (key == null) {return null;}final String jndiName = convertJndiName(key);try (final JndiManager jndiManager = JndiManager.getDefaultManager()) {return Objects.toString(jndiManager.lookup(jndiName), null);} catch (final NamingException e) {LOGGER.warn(LOOKUP, "Error looking up JNDI resource [{}].", jndiName, e);return null;}}
JndiLookup#lookup()方法中,最终会调用
JndiManager#lookup()方法,其源码如下:
public <T> T lookup(final String name) throws NamingException {return (T) this.context.lookup(name);}
Interpolator#lookup()、
JndiLookup#lookup()和
JndiManager#lookup()源码分析可知,整个调用的过程并没有对输入的参数 name 进行合法性校验,而是直接进行调用,这样就会给攻击者可乘之机,恶意地执行非法的远程调用。
3、漏洞修复
log4j-2.15.1-rc1(截至 2021-12-12),而且在最近的几天时间里,Log4j2 连续发布了好几个版本,发布记录如下:

log4j-core.jarlog4j-core/src/main/java/org/apache/logging/log4j/core/lookup/Interpolator.javalog4j-core/src/main/java/org/apache/logging/log4j/core/net/JndiManager.javalog4j-core/src/main/java/org/apache/logging/log4j/core/selector/JndiContextSelector.javalog4j-jms.jarlog4j-jms/src/main/java/org/apache/logging/log4j/jms/appender/JmsManager.java
3.1 Interpolator的修复
Interpolator的两个构造方法进行了修改,具体如下:
public Interpolator(final StrLookup defaultLookup, final List<String> pluginPackages)
;public Interpolator(final Map<String, String> properties)
#Interpolator(final StrLookup defaultLookup, final List<String> pluginPackages)中,增加了对 JNDI 是否开启的判断,具体如下:
public Interpolator(final StrLookup defaultLookup, final List<String> pluginPackages) {this.defaultLookup = defaultLookup == null ? new MapLookup(new HashMap<String, String>()) : defaultLookup;final PluginManager manager = new PluginManager(CATEGORY);manager.collectPlugins(pluginPackages);final Map<String, PluginType<?>> plugins = manager.getPlugins();for (final Map.Entry<String, PluginType<?>> entry : plugins.entrySet()) {try {final Class<? extends StrLookup> clazz = entry.getValue().getPluginClass().asSubclass(StrLookup.class);// 判断JNDI是否开启,如果在 JndiManager 中未开启,则不添加 JndiLookup 至 strLookupMap 集合中if (!clazz.getName().equals(JndiLookup.class.getName()) || JndiManager.isIsJndiEnabled()) {strLookupMap.put(entry.getKey().toLowerCase(), ReflectionUtil.instantiate(clazz));}} catch (final Throwable t) {handleError(entry.getKey(), t);}}}
对于 public Interpolator(final Map<String, String> properties)
构造方法,同样增加了对 JNDI 是否开启的判断,具体如下:
public Interpolator(final Map<String, String> properties) {this.defaultLookup = new MapLookup(properties == null ? new HashMap<String, String>() : properties);// TODO: this ought to use the PluginManagerstrLookupMap.put("log4j", new Log4jLookup());strLookupMap.put("sys", new SystemPropertiesLookup());strLookupMap.put("env", new EnvironmentLookup());strLookupMap.put("main", MainMapLookup.MAIN_SINGLETON);strLookupMap.put("marker", new MarkerLookup());strLookupMap.put("java", new JavaLookup());strLookupMap.put("lower", new LowerLookup());strLookupMap.put("upper", new UpperLookup());// JNDI// 通过 JndiManager 判断 JNDI 是否开启,如果未开启,则不添加 JndiLookup 至 strLookupMap 集合中if (JndiManager.isIsJndiEnabled()) {try {// [LOG4J2-703] We might be on AndroidstrLookupMap.put(LOOKUP_KEY_JNDI,Loader.newCheckedInstanceOf("org.apache.logging.log4j.core.lookup.JndiLookup", StrLookup.class));} catch (final LinkageError | Exception e) {handleError(LOOKUP_KEY_JNDI, e);}}//...省略其他代码}
3.2 JndiManager的修复
对于 JndiManager
类,主要的修改有以下的几个地方:
增加了 isIsJndiEnabled()
方法,用于判断 JNDI 是否开启,默认为 false,可以通过log4j2.enableJndi
属性进行设置;public static boolean isIsJndiEnabled() {return PropertiesUtil.getProperties().getBooleanProperty("log4j2.enableJndi", false);}增加了
JndiManager(final String name)
构造方法;private JndiManager(final String name) {super(null, name);this.context = null;this.allowedProtocols = null;this.allowedClasses = null;this.allowedHosts = null;}修改了
releaseSub(final long timeout, final TimeUnit timeUnit)
方法;@Overrideprotected boolean releaseSub(final long timeout, final TimeUnit timeUnit) {// if 判断是新增的if (context != null) {return JndiCloser.closeSilently(this.context);}return JndiCloser.closeSilently(this.context);}修改了 lookup(final String name)
方法,这个方法变化相对来说是最大的,首先增加了对context
的判断,如果context
为空,直接返回null
,其次增加了对协议的校验,如果是 ldap 和 ldaps 协议,还会再次验证 ldap server 主机地址是否为被允许的,以及验证属性类是否是被允许的,如果所有的验证都通过了,才会调用this.context.lookup(name)
方法;public synchronized <T> T lookup(final String name) throws NamingException {// 如果 context 为空,直接返回 nullif (context == null) {return null;}try {URI uri = new URI(name);if (uri.getScheme() != null) {// 如果不是允许的协议,直接给出警告并返回 nullif (!allowedProtocols.contains(uri.getScheme().toLowerCase(Locale.ROOT))) {LOGGER.warn("Log4j JNDI does not allow protocol {}", uri.getScheme());return null;}// 下面的是针对 ldap 和 ldaps 协议的判断if (LDAP.equalsIgnoreCase(uri.getScheme()) || LDAPS.equalsIgnoreCase(uri.getScheme())) {// 首先验证是否为允许的 ldap server 主机地址,如果不是,直接返回 nullif (!allowedHosts.contains(uri.getHost())) {LOGGER.warn("Attempt to access ldap server not in allowed list");return null;}Attributes attributes = this.context.getAttributes(name);if (attributes != null) {// In testing the "key" for attributes seems to be lowercase while the attribute id is// camelcase, but that may just be true for the test LDAP used here. This copies the Attributes// to a Map ignoring the "key" and using the Attribute's id as the key in the Map so it matches// the Java schema.Map<String, Attribute> attributeMap = new HashMap<>();NamingEnumeration<? extends Attribute> enumeration = attributes.getAll();while (enumeration.hasMore()) {Attribute attribute = enumeration.next();attributeMap.put(attribute.getID(), attribute);}Attribute classNameAttr = attributeMap.get(CLASS_NAME);if (attributeMap.get(SERIALIZED_DATA) != null) {if (classNameAttr != null) {String className = classNameAttr.get().toString();//if (!allowedClasses.contains(className)) {LOGGER.warn("Deserialization of {} is not allowed", className);return null;}} else {LOGGER.warn("No class name provided for {}", name);return null;}} else if (attributeMap.get(REFERENCE_ADDRESS) != null|| attributeMap.get(OBJECT_FACTORY) != null) {LOGGER.warn("Referenceable class is not allowed for {}", name);return null;}}}}} catch (URISyntaxException ex) {LOGGER.warn("Invalid JNDI URI - {}", name);return null;}return (T) this.context.lookup(name);}内部静态类
JndiManagerFactory
的createManager(final String name, final Properties data)
方法的变化,增加了对 JNDI 是否开启的判断,如下:@Overridepublic JndiManager createManager(final String name, final Properties data) {// 增加了对 JNDI 是否开启判断,如果开启了 JNDI,则会从属性数据中读取允许的主机、类和协议相关信息// 这些信息在调用 lookup 方法的时候会进行验证if (isIsJndiEnabled()) {String hosts = data != null ? data.getProperty(ALLOWED_HOSTS) : null;String classes = data != null ? data.getProperty(ALLOWED_CLASSES) : null;String protocols = data != null ? data.getProperty(ALLOWED_PROTOCOLS) : null;List<String> allowedHosts = new ArrayList<>();List<String> allowedClasses = new ArrayList<>();List<String> allowedProtocols = new ArrayList<>();addAll(hosts, allowedHosts, permanentAllowedHosts, ALLOWED_HOSTS, data);addAll(classes, allowedClasses, permanentAllowedClasses, ALLOWED_CLASSES, data);addAll(protocols, allowedProtocols, permanentAllowedProtocols, ALLOWED_PROTOCOLS, data);try {return new JndiManager(name, new InitialDirContext(data), allowedHosts, allowedClasses,allowedProtocols);} catch (final NamingException e) {LOGGER.error("Error creating JNDI InitialContext.", e);return null;}} else {return new JndiManager(name);}}
3.3 JndiContextSelector的修复
JndiContextSelector类而言,主要是增加了一个构造方法,在构造方法中,会对 JNDI 是否开启进行验证,如果未开启,则会直接抛出异常,具体方法如下:
public JndiContextSelector() {if (!JndiManager.isIsJndiEnabled()) {throw new IllegalStateException("JNDI must be enabled by setting log4j2.enableJndi=true");}}
3.4 JmsManager的修复
JmsManager类而言,主要修改了
createManager(final String name, final JmsManagerConfiguration data)方法,增加了对 JNDI 是否开启的判断,如下:
@Overridepublic JmsManager createManager(final String name, final JmsManagerConfiguration data) {if (JndiManager.isIsJndiEnabled()) {try {return new JmsManager(name, data);} catch (final Exception e) {logger().error("Error creating JmsManager using JmsManagerConfiguration [{}]", data, e);return null;}} else {logger().error("JNDI has not been enabled. The log4j2.enableJndi property must be set to true");return null;}}
4、项目上Log4j2漏洞修复
4.1 Log4j2版本排查
借助于IDE 工具,查看 pom.xml 的依赖树,然后进行搜索,找出 log4j2 的使用版本; 借助于 Maven 命令,输出所有的依赖树结构,然后通过依赖树结构进行搜索查找;


mvn dependency:tree
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ log4j-demo ---[INFO] com.example:log4j-demo:jar:0.0.1-SNAPSHOT[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.6.1:compile[INFO] | +- org.springframework.boot:spring-boot-starter:jar:2.6.1:compile[INFO] | | +- org.springframework.boot:spring-boot:jar:2.6.1:compile[INFO] | | +- org.springframework.boot:spring-boot-autoconfigure:jar:2.6.1:compile[INFO] | | +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile[INFO] | | \- org.yaml:snakeyaml:jar:1.29:compile............[INFO] +- org.springframework.boot:spring-boot-starter-log4j2:jar:2.6.1:compile[INFO] | +- org.apache.logging.log4j:log4j-slf4j-impl:jar:2.14.1:compile[INFO] | | +- org.slf4j:slf4j-api:jar:1.7.32:compile[INFO] | | \- org.apache.logging.log4j:log4j-api:jar:2.14.1:compile[INFO] | +- org.apache.logging.log4j:log4j-core:jar:2.14.1:compile[INFO] | +- org.apache.logging.log4j:log4j-jul:jar:2.14.1:compile[INFO] | \- org.slf4j:jul-to-slf4j:jar:1.7.32:compile............
log4j-core-2.14.1版本的是
spring-boot-starter-log4j2:jar。
4.2 项目上漏洞修复
设置 jvm 参数 -Dlog4j2.formatMsgNoLookups=true 日志设置 log4j2.formatMsgNoLookups=True 设置系统环境变量 FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS = true 关闭对应的应用程序的网络外网连接,并且禁止主动外连
JndiLookup类
zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class
log4j-core版本,然后单独引入最新的
log4j-core版本,示例如下:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j2</artifactId><!-- 排除spring-boot-starter-log4j2中引入的 log4j-core.2.14.1 版本 --><exclusions><exclusion><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId></exclusion></exclusions></dependency><!-- 单独引入当前最新的 log4j-core.2.15.0 版本 --><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId><version>2.15.0</version></dependency>
log4j-core版本,单独引入
log4j-core的最新版本即可实现升级修复,可能在升级了
log4j-core核心后,其他的 log4j2 版本还是之前的,比如
log4j-api和
log4j-jul等等,这些都可以按照上面的方式进行升级。
除了直接在 pom.xml 文件中升级 log4j-core 版本,还有一种不太推荐的方式就是获取最新的 log4j-core-2.15.0.jar,然后手动安装到本地Maven仓库中,安装的时候指定版本为之前的版本号,并把历史版本的 jar删除掉,这种方式可以不用修改 pom.xml 文件,直接重新对应用打包即可。
文章转载自然笑,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




